Merge pull request #4 from STEMuli-Tx/feature/unlock-screen

Feature/unlock screen
pull/15/head^2
mrwadepro 2025-04-11 12:39:45 +07:00 committed by GitHub
commit 30886c0c70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
100 changed files with 3962 additions and 611 deletions

@ -0,0 +1 @@
uid://dvutrxphi6utu

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cu0g4lm7gaxpf"
path="res://.godot/imported/Starter Kit City Builder.apple-touch-icon.png-54e47c754919415f807e136dd0205656.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://builds/html/Starter Kit City Builder.apple-touch-icon.png"
dest_files=["res://.godot/imported/Starter Kit City Builder.apple-touch-icon.png-54e47c754919415f807e136dd0205656.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

@ -0,0 +1,213 @@
/**************************************************************************/
/* audio.worklet.js */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
class RingBuffer {
constructor(p_buffer, p_state, p_threads) {
this.buffer = p_buffer;
this.avail = p_state;
this.threads = p_threads;
this.rpos = 0;
this.wpos = 0;
}
data_left() {
return this.threads ? Atomics.load(this.avail, 0) : this.avail;
}
space_left() {
return this.buffer.length - this.data_left();
}
read(output) {
const size = this.buffer.length;
let from = 0;
let to_write = output.length;
if (this.rpos + to_write > size) {
const high = size - this.rpos;
output.set(this.buffer.subarray(this.rpos, size));
from = high;
to_write -= high;
this.rpos = 0;
}
if (to_write) {
output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
}
this.rpos += to_write;
if (this.threads) {
Atomics.add(this.avail, 0, -output.length);
Atomics.notify(this.avail, 0);
} else {
this.avail -= output.length;
}
}
write(p_buffer) {
const to_write = p_buffer.length;
const mw = this.buffer.length - this.wpos;
if (mw >= to_write) {
this.buffer.set(p_buffer, this.wpos);
this.wpos += to_write;
if (mw === to_write) {
this.wpos = 0;
}
} else {
const high = p_buffer.subarray(0, mw);
const low = p_buffer.subarray(mw);
this.buffer.set(high, this.wpos);
this.buffer.set(low);
this.wpos = low.length;
}
if (this.threads) {
Atomics.add(this.avail, 0, to_write);
Atomics.notify(this.avail, 0);
} else {
this.avail += to_write;
}
}
}
class GodotProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.threads = false;
this.running = true;
this.lock = null;
this.notifier = null;
this.output = null;
this.output_buffer = new Float32Array();
this.input = null;
this.input_buffer = new Float32Array();
this.port.onmessage = (event) => {
const cmd = event.data['cmd'];
const data = event.data['data'];
this.parse_message(cmd, data);
};
}
process_notify() {
if (this.notifier) {
Atomics.add(this.notifier, 0, 1);
Atomics.notify(this.notifier, 0);
}
}
parse_message(p_cmd, p_data) {
if (p_cmd === 'start' && p_data) {
const state = p_data[0];
let idx = 0;
this.threads = true;
this.lock = state.subarray(idx, ++idx);
this.notifier = state.subarray(idx, ++idx);
const avail_in = state.subarray(idx, ++idx);
const avail_out = state.subarray(idx, ++idx);
this.input = new RingBuffer(p_data[1], avail_in, true);
this.output = new RingBuffer(p_data[2], avail_out, true);
} else if (p_cmd === 'stop') {
this.running = false;
this.output = null;
this.input = null;
this.lock = null;
this.notifier = null;
} else if (p_cmd === 'start_nothreads') {
this.output = new RingBuffer(p_data[0], p_data[0].length, false);
} else if (p_cmd === 'chunk') {
this.output.write(p_data);
}
}
static array_has_data(arr) {
return arr.length && arr[0].length && arr[0][0].length;
}
process(inputs, outputs, parameters) {
if (!this.running) {
return false; // Stop processing.
}
if (this.output === null) {
return true; // Not ready yet, keep processing.
}
const process_input = GodotProcessor.array_has_data(inputs);
if (process_input) {
const input = inputs[0];
const chunk = input[0].length * input.length;
if (this.input_buffer.length !== chunk) {
this.input_buffer = new Float32Array(chunk);
}
if (!this.threads) {
GodotProcessor.write_input(this.input_buffer, input);
this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer });
} else if (this.input.space_left() >= chunk) {
GodotProcessor.write_input(this.input_buffer, input);
this.input.write(this.input_buffer);
} else {
// this.port.postMessage('Input buffer is full! Skipping input frame.'); // Uncomment this line to debug input buffer.
}
}
const process_output = GodotProcessor.array_has_data(outputs);
if (process_output) {
const output = outputs[0];
const chunk = output[0].length * output.length;
if (this.output_buffer.length !== chunk) {
this.output_buffer = new Float32Array(chunk);
}
if (this.output.data_left() >= chunk) {
this.output.read(this.output_buffer);
GodotProcessor.write_output(output, this.output_buffer);
if (!this.threads) {
this.port.postMessage({ 'cmd': 'read', 'data': chunk });
}
} else {
// this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); // Uncomment this line to debug output buffer.
}
}
this.process_notify();
return true;
}
static write_output(dest, source) {
const channels = dest.length;
for (let ch = 0; ch < channels; ch++) {
for (let sample = 0; sample < dest[ch].length; sample++) {
dest[ch][sample] = source[sample * channels + ch];
}
}
}
static write_input(dest, source) {
const channels = source.length;
for (let ch = 0; ch < channels; ch++) {
for (let sample = 0; sample < source[ch].length; sample++) {
dest[sample * channels + ch] = source[ch][sample];
}
}
}
}
registerProcessor('godot-processor', GodotProcessor);

@ -0,0 +1,199 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
<title>Starter Kit City Builder</title>
<style>
html, body, #canvas {
margin: 0;
padding: 0;
border: 0;
}
body {
color: white;
background-color: black;
overflow: hidden;
touch-action: none;
}
#canvas {
display: block;
}
#canvas:focus {
outline: none;
}
#status, #status-splash, #status-progress {
position: absolute;
left: 0;
right: 0;
}
#status, #status-splash {
top: 0;
bottom: 0;
}
#status {
background-color: #242424;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
visibility: hidden;
}
#status-splash {
max-height: 100%;
max-width: 100%;
margin: auto;
}
#status-progress, #status-notice {
display: none;
}
#status-progress {
bottom: 10%;
width: 50%;
margin: 0 auto;
}
#status-notice {
background-color: #5b3943;
border-radius: 0.5rem;
border: 1px solid #9b3943;
color: #e0e0e0;
font-family: 'Noto Sans', 'Droid Sans', Arial, sans-serif;
line-height: 1.3;
margin: 0 2rem;
overflow: hidden;
padding: 1rem;
text-align: center;
z-index: 1;
}
</style>
<link id="-gd-engine-icon" rel="icon" type="image/png" href="Starter Kit City Builder.icon.png" />
<link rel="apple-touch-icon" href="Starter Kit City Builder.apple-touch-icon.png"/>
</head>
<body>
<canvas id="canvas">
Your browser does not support the canvas tag.
</canvas>
<noscript>
Your browser does not support JavaScript.
</noscript>
<div id="status">
<img id="status-splash" src="Starter Kit City Builder.png" alt="">
<progress id="status-progress"></progress>
<div id="status-notice"></div>
</div>
<script src="Starter Kit City Builder.js"></script>
<script>
const GODOT_CONFIG = {"args":[],"canvasResizePolicy":2,"ensureCrossOriginIsolationHeaders":false,"executable":"Starter Kit City Builder","experimentalVK":false,"fileSizes":{"Starter Kit City Builder.pck":14570480,"Starter Kit City Builder.wasm":1620216},"focusCanvas":true,"gdextensionLibs":[]};
const GODOT_THREADS_ENABLED = false;
const engine = new Engine(GODOT_CONFIG);
(function () {
const statusOverlay = document.getElementById('status');
const statusProgress = document.getElementById('status-progress');
const statusNotice = document.getElementById('status-notice');
let initializing = true;
let statusMode = '';
function setStatusMode(mode) {
if (statusMode === mode || !initializing) {
return;
}
if (mode === 'hidden') {
statusOverlay.remove();
initializing = false;
return;
}
statusOverlay.style.visibility = 'visible';
statusProgress.style.display = mode === 'progress' ? 'block' : 'none';
statusNotice.style.display = mode === 'notice' ? 'block' : 'none';
statusMode = mode;
}
function setStatusNotice(text) {
while (statusNotice.lastChild) {
statusNotice.removeChild(statusNotice.lastChild);
}
const lines = text.split('\n');
lines.forEach((line) => {
statusNotice.appendChild(document.createTextNode(line));
statusNotice.appendChild(document.createElement('br'));
});
}
function displayFailureNotice(err) {
console.error(err);
if (err instanceof Error) {
setStatusNotice(err.message);
} else if (typeof err === 'string') {
setStatusNotice(err);
} else {
setStatusNotice('An unknown error occured');
}
setStatusMode('notice');
initializing = false;
}
const missing = Engine.getMissingFeatures({
threads: GODOT_THREADS_ENABLED,
});
if (missing.length !== 0) {
if (GODOT_CONFIG['serviceWorker'] && GODOT_CONFIG['ensureCrossOriginIsolationHeaders'] && 'serviceWorker' in navigator) {
// There's a chance that installing the service worker would fix the issue
Promise.race([
navigator.serviceWorker.getRegistration().then((registration) => {
if (registration != null) {
return Promise.reject(new Error('Service worker already exists.'));
}
return registration;
}).then(() => engine.installServiceWorker()),
// For some reason, `getRegistration()` can stall
new Promise((resolve) => {
setTimeout(() => resolve(), 2000);
}),
]).catch((err) => {
console.error('Error while registering service worker:', err);
}).then(() => {
window.location.reload();
});
} else {
// Display the message as usual
const missingMsg = 'Error\nThe following features required to run Godot projects on the Web are missing:\n';
displayFailureNotice(missingMsg + missing.join('\n'));
}
} else {
setStatusMode('progress');
engine.startGame({
'onProgress': function (current, total) {
if (current > 0 && total > 0) {
statusProgress.value = current;
statusProgress.max = total;
} else {
statusProgress.removeAttribute('value');
statusProgress.removeAttribute('max');
}
},
}).then(() => {
setStatusMode('hidden');
}, displayFailureNotice);
}
}());
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c4u6k4wxw15co"
path="res://.godot/imported/Starter Kit City Builder.icon.png-db6dbcb23970726fc8baf3f9f92171f6.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://builds/html/Starter Kit City Builder.icon.png"
dest_files=["res://.godot/imported/Starter Kit City Builder.icon.png-db6dbcb23970726fc8baf3f9f92171f6.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c1wm7isue5iys"
path="res://.godot/imported/Starter Kit City Builder.png-a4c352fa688cf84948ae1c3c4c544c9e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://builds/html/Starter Kit City Builder.png"
dest_files=["res://.godot/imported/Starter Kit City Builder.png-a4c352fa688cf84948ae1c3c4c544c9e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

@ -65,3 +65,4 @@ companion_dialog = {
"text": "Perfect placement! Now the power plant can distribute electricity to all the houses in your city."
}
}
unlocked_items = Array[String](["res://models/power_plant-wind.glb", "res://models/building-garage.glb"])

@ -1,4 +1,4 @@
[gd_resource type="Resource" script_class="MissionData" load_steps=5 format=3 uid="uid://x5h4xutbldq3"]
[gd_resource type="Resource" script_class="MissionData" load_steps=4 format=3 uid="uid://x5h4xutbldq3"]
[ext_resource type="Script" path="res://scripts/mission/mission_data.gd" id="1_nv6c6"]
[ext_resource type="Script" path="res://scripts/mission/mission_objective.gd" id="1_yfbrc"]
@ -12,28 +12,19 @@ description = "Build a road"
structure_index = -1
completed = false
[sub_resource type="Resource" id="Resource_7c02e"]
script = ExtResource("1_yfbrc")
type = 3
target_count = 1
current_count = 0
description = "Build a residential building"
structure_index = -1
completed = false
[resource]
script = ExtResource("1_nv6c6")
id = "1"
title = "Getting Started"
description = "Welcome to the city builder! Let's start by placing your first building and connecting it with a road."
objectives = Array[ExtResource("1_yfbrc")]([SubResource("Resource_ywws1"), SubResource("Resource_7c02e")])
title = "Building Roads"
description = "Welcome to the city builder! Let's start by placing your first road."
objectives = Array[ExtResource("1_yfbrc")]([SubResource("Resource_ywws1")])
rewards = {
"cash": 500
"cash": 250
}
next_mission_id = "2"
graph_path = ""
full_screen_path = ""
intro_text = "Welcome to Stem City! Let's learn how to build a city from the ground up."
intro_text = "Welcome to Stem City! Let's learn how to build a city from the ground up. Start by building a road."
question_text = ""
correct_answer = ""
feedback_text = ""
@ -56,7 +47,7 @@ companion_dialog = {
"mission_completed": {
"animation": "happy",
"duration": 6000,
"text": ["You've completed your first mission! Great job building your first road and residential building!"]
"text": ["You've completed your first mission! Great job building your first road!"]
},
"mission_started": {
"animation": "excited",
@ -67,10 +58,6 @@ companion_dialog = {
"animation": "happy",
"duration": 5000,
"text": ["You built a road! Now your citizens can move around the city.", "Nice work on that road! Roads help connect different parts of your city."]
},
"objective_completed_3": {
"animation": "excited",
"duration": 5000,
"text": ["Amazing! You built your first residential building where people can live.", "You just built a home for your city residents! The population is growing."]
}
}
unlocked_items = Array[String](["res://models/building-small-a.glb"])

@ -81,3 +81,4 @@ companion_dialog = {
"text": "This is a system of equations problem. We have two houses with different combinations of gaming consoles and TVs."
}
}
unlocked_items = Array[String](["res://models/power_plant-coal.glb", "res://models/power_plant-solar.glb"])

@ -0,0 +1,63 @@
[gd_resource type="Resource" script_class="MissionData" load_steps=4 format=3 uid="uid://bsafjxsrgxcqf"]
[ext_resource type="Script" path="res://scripts/mission/mission_objective.gd" id="1_dhx01"]
[ext_resource type="Script" path="res://scripts/mission/mission_data.gd" id="2_mum3p"]
[sub_resource type="Resource" id="Resource_c06be"]
script = ExtResource("1_dhx01")
type = 7
target_count = 1
current_count = 0
description = "Compare two construction companies and determine which one is more efficient for building 40 houses in a week."
structure_index = -1
completed = false
[resource]
script = ExtResource("2_mum3p")
id = "3a"
title = "Compare Construction Companies"
description = "As your city grows, you need to choose the most efficient construction company."
objectives = Array[ExtResource("1_dhx01")]([SubResource("Resource_c06be")])
rewards = {
"cash": 500
}
next_mission_id = "3"
graph_path = "res://images/mission_2.png"
full_screen_path = ""
intro_text = "Your city is rapidly growing, and you need to build houses to accommodate new residents! Two different construction companies offer to help. Study the company data below, find the unit rates (houses per worker), and determine which company would require fewer workers to build 40 houses in a week. Which company requires fewer workers to build 40 houses in a week? (A or B)"
question_text = "Which company requires fewer workers to build 40 houses in a week? (A or B)"
correct_answer = "A"
feedback_text = "Correct! Company A (City Builders Inc.) would require fewer workers to build 40 houses. Company A builds at a rate of 4 houses per worker per week, while Company B builds at a rate of 3 houses per worker per week. For 40 houses, Company A needs 10 workers while Company B needs about 13.33 workers."
incorrect_feedback = "Not quite right. Look carefully at the data for both companies. Compare their rates: Company A builds 4 houses per worker per week, while Company B builds 3 houses per worker per week. Calculate how many workers each would need for 40 houses."
company_data = ""
power_math_content = ""
num_of_user_inputs = 1
input_labels = Array[String]([])
companion_dialog = {
"correct_answer": {
"animation": "happy",
"duration": 5000,
"text": ["Perfect! Company A is more efficient because each worker builds more houses per week.", "Exactly right! You found the company that can build the houses with fewer workers."]
},
"incorrect_answer": {
"animation": "sad",
"duration": 5000,
"text": ["Let's think about this differently. Look at how many houses each worker can build per week and calculate from there.", "Not quite right. Check the rates carefully: how many houses can one worker build in a week for each company?"]
},
"mission_completed": {
"animation": "happy",
"duration": 6000,
"text": "Great job selecting the most efficient construction company! Your city will grow faster now."
},
"mission_started": {
"animation": "excited",
"duration": 6000,
"text": ["It's time to expand our city! We need to choose the most efficient construction company.", "We have two construction companies offering their services. Let's analyze which one is better!"]
},
"objective_completed_7": {
"animation": "happy",
"duration": 5000,
"text": ["You've solved the construction company problem! That's good mathematical thinking.", "You've made a wise decision based on the data. That's how city planners work!"]
}
}
unlocked_items = Array[String](["res://models/road-corner.glb", "res://models/building-small-b.glb"])

@ -3,32 +3,32 @@
[ext_resource type="Script" path="res://scripts/mission/mission_objective.gd" id="1_dhx01"]
[ext_resource type="Script" path="res://scripts/mission/mission_data.gd" id="2_mum3p"]
[sub_resource type="Resource" id="Resource_c06be"]
[sub_resource type="Resource" id="Resource_7c02e"]
script = ExtResource("1_dhx01")
type = 7
type = 3
target_count = 1
current_count = 0
description = "Compare two construction companies and determine which one is more efficient for building 40 houses in a week."
description = "Build a residential building"
structure_index = -1
completed = false
[resource]
script = ExtResource("2_mum3p")
id = "2"
title = "Compare Construction Companies"
description = ""
objectives = Array[ExtResource("1_dhx01")]([SubResource("Resource_c06be")])
title = "Building Homes"
description = "Now that we have a road, let's build a residential building for our citizens!"
objectives = Array[ExtResource("1_dhx01")]([SubResource("Resource_7c02e")])
rewards = {
"cash": 0
"cash": 250
}
next_mission_id = ""
graph_path = "res://images/mission_2.png"
next_mission_id = "3"
graph_path = ""
full_screen_path = ""
intro_text = "Your city is rapidly growing, and you need to build houses to accommodate new residents! Two different construction companies offer to help. Study the company data below, find the unit rates (houses per worker), and determine which company would require fewer workers to build 40 houses in a week. Which company requires fewer workers to build 40 houses in a week? (A or B)"
intro_text = "Great job on the road! Now let's build a residential building where our citizens can live."
question_text = ""
correct_answer = "A"
feedback_text = "Correct! Company A (City Builders Inc.) would require fewer workers to build 40 houses. Company A builds at a rate of 4 houses per worker per week, while Company B builds at a rate of 3 houses per worker per week. For 40 houses, Company A needs 10 workers while Company B needs about 13.33 workers."
incorrect_feedback = "Not quite right. Look carefully at the data for both companies. Compare their rates: Company A builds 4 houses per worker per week, while Company B builds 3 houses per worker per week. Calculate how many workers each would need for 40 houses."
correct_answer = ""
feedback_text = ""
incorrect_feedback = ""
company_data = ""
power_math_content = ""
num_of_user_inputs = 1
@ -37,26 +37,27 @@ companion_dialog = {
"correct_answer": {
"animation": "happy",
"duration": 5000,
"text": ["Perfect! Company A is more efficient because each worker builds more houses per week.", "Exactly right! You found the company that can build the houses with fewer workers."]
"text": ["Great job! That's the right answer!", "Perfect! You got it right!"]
},
"incorrect_answer": {
"animation": "sad",
"duration": 5000,
"text": ["Let's think about this differently. Look at how many houses each worker can build per week and calculate from there.", "Not quite right. Check the rates carefully: how many houses can one worker build in a week for each company?"]
"text": ["Hmm, that doesn't look right. Let's try again.", "Not quite right. Don't worry, you can try again!"]
},
"mission_completed": {
"animation": "happy",
"duration": 6000,
"text": "Great job selecting the most efficient construction company! Your city will grow faster now."
"text": ["You've completed your second mission! Great job building your first residential building!"]
},
"mission_started": {
"animation": "excited",
"duration": 6000,
"text": ["It's time to expand our city! We need to choose the most efficient construction company.", "We have two construction companies offering their services. Let's analyze which one is better!"]
"text": ["Now it's time to build homes for our citizens!", "Let's add a residential building to our city!"]
},
"objective_completed_7": {
"animation": "happy",
"objective_completed_3": {
"animation": "excited",
"duration": 5000,
"text": ["You've solved the construction company problem! That's good mathematical thinking.", "You've made a wise decision based on the data. That's how city planners work!"]
"text": ["Amazing! You built your first residential building where people can live.", "You just built a home for your city residents! The population is growing."]
}
}
unlocked_items = Array[String](["res://models/grass-trees.glb", "res://models/grass.glb"])

@ -65,3 +65,4 @@ companion_dialog = {
"text": "Welcome to the City Expansion Project! We need to build 40 residential buildings using our construction workers."
}
}
unlocked_items = Array[String](["res://models/building-small-c.glb", "res://models/road-intersection.glb", "res://models/grass-trees.glb"])

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

@ -0,0 +1,36 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bnn1b2yg61elu"
path.s3tc="res://.godot/imported/colormap.png-c1bc3c3aabeec406ff4b53328583776a.s3tc.ctex"
path.etc2="res://.godot/imported/colormap.png-c1bc3c3aabeec406ff4b53328583776a.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://models/Textures/colormap.png"
dest_files=["res://.godot/imported/colormap.png-c1bc3c3aabeec406ff4b53328583776a.s3tc.ctex", "res://.godot/imported/colormap.png-c1bc3c3aabeec406ff4b53328583776a.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0

@ -4,8 +4,9 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://cwmxak3hokb58"
path.s3tc="res://.godot/imported/building-small-a_colormap.png-5c3df3cd6e4a6c3a9b3ecf5882af7d0c.s3tc.ctex"
path.etc2="res://.godot/imported/building-small-a_colormap.png-5c3df3cd6e4a6c3a9b3ecf5882af7d0c.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
@ -15,7 +16,7 @@ generator_parameters={
[deps]
source_file="res://models/building-small-a_colormap.png"
dest_files=["res://.godot/imported/building-small-a_colormap.png-5c3df3cd6e4a6c3a9b3ecf5882af7d0c.s3tc.ctex"]
dest_files=["res://.godot/imported/building-small-a_colormap.png-5c3df3cd6e4a6c3a9b3ecf5882af7d0c.s3tc.ctex", "res://.godot/imported/building-small-a_colormap.png-5c3df3cd6e4a6c3a9b3ecf5882af7d0c.etc2.ctex"]
[params]

@ -4,8 +4,9 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://du3u60s01ni0e"
path.s3tc="res://.godot/imported/building-small-b_colormap.png-88fece0a14f60320dabbfa87ba219d29.s3tc.ctex"
path.etc2="res://.godot/imported/building-small-b_colormap.png-88fece0a14f60320dabbfa87ba219d29.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
@ -15,7 +16,7 @@ generator_parameters={
[deps]
source_file="res://models/building-small-b_colormap.png"
dest_files=["res://.godot/imported/building-small-b_colormap.png-88fece0a14f60320dabbfa87ba219d29.s3tc.ctex"]
dest_files=["res://.godot/imported/building-small-b_colormap.png-88fece0a14f60320dabbfa87ba219d29.s3tc.ctex", "res://.godot/imported/building-small-b_colormap.png-88fece0a14f60320dabbfa87ba219d29.etc2.ctex"]
[params]

@ -4,8 +4,9 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://cmnjw1rqw0t57"
path.s3tc="res://.godot/imported/building-small-c_colormap.png-c006c810bfe7f2b5ff9ffddb2a614e72.s3tc.ctex"
path.etc2="res://.godot/imported/building-small-c_colormap.png-c006c810bfe7f2b5ff9ffddb2a614e72.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
@ -15,7 +16,7 @@ generator_parameters={
[deps]
source_file="res://models/building-small-c_colormap.png"
dest_files=["res://.godot/imported/building-small-c_colormap.png-c006c810bfe7f2b5ff9ffddb2a614e72.s3tc.ctex"]
dest_files=["res://.godot/imported/building-small-c_colormap.png-c006c810bfe7f2b5ff9ffddb2a614e72.s3tc.ctex", "res://.godot/imported/building-small-c_colormap.png-c006c810bfe7f2b5ff9ffddb2a614e72.etc2.ctex"]
[params]

@ -4,15 +4,16 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://bnn1b2yg61elu"
path.s3tc="res://.godot/imported/colormap.png-6f96a945bc6d1c76c460db26dbd64298.s3tc.ctex"
path.etc2="res://.godot/imported/colormap.png-6f96a945bc6d1c76c460db26dbd64298.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://models/colormap.png"
dest_files=["res://.godot/imported/colormap.png-6f96a945bc6d1c76c460db26dbd64298.s3tc.ctex"]
dest_files=["res://.godot/imported/colormap.png-6f96a945bc6d1c76c460db26dbd64298.s3tc.ctex", "res://.godot/imported/colormap.png-6f96a945bc6d1c76c460db26dbd64298.etc2.ctex"]
[params]

@ -4,8 +4,9 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://b8l28de0bquvh"
path.s3tc="res://.godot/imported/grass-trees_colormap.png-dfa57c874305a3beb22f3746cd6c2447.s3tc.ctex"
path.etc2="res://.godot/imported/grass-trees_colormap.png-dfa57c874305a3beb22f3746cd6c2447.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
@ -15,7 +16,7 @@ generator_parameters={
[deps]
source_file="res://models/grass-trees_colormap.png"
dest_files=["res://.godot/imported/grass-trees_colormap.png-dfa57c874305a3beb22f3746cd6c2447.s3tc.ctex"]
dest_files=["res://.godot/imported/grass-trees_colormap.png-dfa57c874305a3beb22f3746cd6c2447.s3tc.ctex", "res://.godot/imported/grass-trees_colormap.png-dfa57c874305a3beb22f3746cd6c2447.etc2.ctex"]
[params]

@ -4,8 +4,9 @@ importer="texture"
type="CompressedTexture2D"
uid="uid://dwkl36x8dcge4"
path.s3tc="res://.godot/imported/grass_colormap.png-65dd6ce9d27477b7e7324a3ab6dee258.s3tc.ctex"
path.etc2="res://.godot/imported/grass_colormap.png-65dd6ce9d27477b7e7324a3ab6dee258.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc"],
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
generator_parameters={
@ -15,7 +16,7 @@ generator_parameters={
[deps]
source_file="res://models/grass_colormap.png"
dest_files=["res://.godot/imported/grass_colormap.png-65dd6ce9d27477b7e7324a3ab6dee258.s3tc.ctex"]
dest_files=["res://.godot/imported/grass_colormap.png-65dd6ce9d27477b7e7324a3ab6dee258.s3tc.ctex", "res://.godot/imported/grass_colormap.png-65dd6ce9d27477b7e7324a3ab6dee258.etc2.ctex"]
[params]

@ -31,6 +31,13 @@ animation/trimming=false
animation/remove_immutable_tracks=true
animation/import_rest_as_RESET=false
import_script/path=""
_subresources={}
_subresources={
"materials": {
"colormap": {
"use_external/enabled": false,
"use_external/path": "res://mesges/road-mesh.res"
}
}
}
gltf/naming_version=0
gltf/embedded_image_handling=1

@ -0,0 +1 @@
uid://dsdxor1cwpqp

@ -1,7 +1,7 @@
[gd_scene load_steps=4 format=3 uid="uid://bqjnp7uypupog"]
[ext_resource type="FontFile" uid="uid://d0cxd77jybrcn" path="res://fonts/lilita_one_regular.ttf" id="1_tnlhn"]
[ext_resource type="Script" uid="uid://bhjbj3n3dak4g" path="res://scripts/controls_panel.gd" id="1_xyuqg"]
[ext_resource type="Script" path="res://scripts/controls_panel.gd" id="1_xyuqg"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_f2mso"]
bg_color = Color(0.145098, 0.172549, 0.231373, 0.941176)

@ -0,0 +1,92 @@
[gd_scene format=3 uid="uid://b81e2k3cevxnb"]
[node name="DirectUnlockedPanel" type="CanvasLayer"]
[node name="Control" type="Control" parent="."]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="ColorRect" type="ColorRect" parent="Control"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.501961)
[node name="PanelContainer" type="PanelContainer" parent="Control"]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -300.0
offset_top = -200.0
offset_right = 300.0
offset_bottom = 200.0
grow_horizontal = 2
grow_vertical = 2
[node name="MarginContainer" type="MarginContainer" parent="Control/PanelContainer"]
layout_mode = 2
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 20
[node name="VBoxContainer" type="VBoxContainer" parent="Control/PanelContainer/MarginContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="Control/PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 24
text = "New Items Unlocked!"
horizontal_alignment = 1
[node name="HSeparator" type="HSeparator" parent="Control/PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="ScrollContainer" type="ScrollContainer" parent="Control/PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="Control/PanelContainer/MarginContainer/VBoxContainer/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ItemRow" type="HBoxContainer" parent="Control/PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/VBoxContainer"]
layout_mode = 2
[node name="TextureRect" type="TextureRect" parent="Control/PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/VBoxContainer/ItemRow"]
custom_minimum_size = Vector2(100, 100)
layout_mode = 2
expand_mode = 1
stretch_mode = 5
[node name="VBoxContainer" type="VBoxContainer" parent="Control/PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/VBoxContainer/ItemRow"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="Control/PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/VBoxContainer/ItemRow/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.9, 0.9, 0.2, 1)
theme_override_font_sizes/font_size = 18
text = "Item Name"
[node name="Label2" type="Label" parent="Control/PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/VBoxContainer/ItemRow/VBoxContainer"]
layout_mode = 2
text = "Item Description"
autowrap_mode = 2
[node name="SimpleCloseButton" type="Button" parent="Control/PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 0, 0, 1)
theme_override_font_sizes/font_size = 20
text = "CLOSE"

@ -1,6 +1,6 @@
[gd_scene load_steps=10 format=3 uid="uid://cgk66f6rg03mj"]
[ext_resource type="Script" uid="uid://bp75qrvcc3at4" path="res://scripts/hud_manager.gd" id="1_6vdxc"]
[ext_resource type="Script" path="res://scripts/hud_manager.gd" id="1_6vdxc"]
[ext_resource type="Texture2D" uid="uid://hendpftbt4iw" path="res://sprites/population_icon.png" id="2_28oy1"]
[ext_resource type="Texture2D" uid="uid://jk3mremfu7lm" path="res://sprites/electricity_icon.png" id="3_2u5bk"]
[ext_resource type="FontFile" uid="uid://d0cxd77jybrcn" path="res://fonts/lilita_one_regular.ttf" id="4_qfmf5"]
@ -32,7 +32,6 @@ anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 0
script = ExtResource("1_6vdxc")
[node name="HBoxContainer" type="HBoxContainer" parent="."]
@ -129,7 +128,6 @@ theme_override_constants/separation = 8
custom_minimum_size = Vector2(40, 40)
layout_mode = 2
size_flags_vertical = 4
mouse_filter = 0
mouse_default_cursor_shape = 2
texture_normal = ExtResource("7_80m3c")
ignore_texture_size = true
@ -148,7 +146,6 @@ theme_override_constants/separation = 8
custom_minimum_size = Vector2(40, 40)
layout_mode = 2
size_flags_vertical = 4
mouse_filter = 0
mouse_default_cursor_shape = 2
texture_normal = ExtResource("6_i1y88")
ignore_texture_size = true

@ -0,0 +1,69 @@
[gd_scene load_steps=2 format=3 uid="uid://bb04k2hkfmls6"]
[ext_resource type="Script" path="res://scripts/mission/improved_unlocked_panel.gd" id="1_r3fws"]
[node name="ImprovedUnlockedPanel" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_r3fws")
[node name="ColorRect" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.501961)
[node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -300.0
offset_top = -200.0
offset_right = 300.0
offset_bottom = 200.0
grow_horizontal = 2
grow_vertical = 2
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"]
layout_mode = 2
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 20
[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"]
layout_mode = 2
[node name="TitleLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 24
text = "New Items Unlocked!"
horizontal_alignment = 1
[node name="HSeparator" type="HSeparator" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="ScrollContainer" type="ScrollContainer" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="ItemsContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="CloseButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Close"
[connection signal="pressed" from="PanelContainer/MarginContainer/VBoxContainer/CloseButton" to="." method="_on_close_button_pressed"]

@ -29,6 +29,7 @@ corner_radius_bottom_right = 15
corner_radius_bottom_left = 15
[node name="LearningPanel" type="ColorRect"]
process_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0

@ -1,4 +1,4 @@
[gd_scene load_steps=27 format=3 uid="uid://vgwrcfy1qawf"]
[gd_scene load_steps=22 format=3 uid="uid://vgwrcfy1qawf"]
[ext_resource type="Script" path="res://scripts/builder.gd" id="1_jybm7"]
[ext_resource type="Environment" uid="uid://jbptgqvstei3" path="res://scenes/main-environment.tres" id="1_yndf3"]
@ -6,16 +6,11 @@
[ext_resource type="Resource" uid="uid://dv14kkhb6umkv" path="res://structures/road-straight.tres" id="2_bwyku"]
[ext_resource type="Texture2D" uid="uid://cbk07cxgshg26" path="res://sprites/selector.png" id="4_wr1wv"]
[ext_resource type="Resource" uid="uid://cntgl86ianngh" path="res://structures/building-small-a.tres" id="5_v5o2m"]
[ext_resource type="Resource" uid="uid://ccb475jeg7ym5" path="res://structures/grass-trees.tres" id="6_fwsy4"]
[ext_resource type="Script" path="res://scripts/view.gd" id="8_yovpv"]
[ext_resource type="Script" path="res://scripts/mission/mission_manager.gd" id="10_oe3re"]
[ext_resource type="Script" path="res://scripts/mission/mission_data.gd" id="11_msovb"]
[ext_resource type="Resource" uid="uid://x5h4xutbldq3" path="res://mission/first_mission.tres" id="12_ms7i7"]
[ext_resource type="Resource" uid="uid://cjr36hqnmyn0x" path="res://mission/second_mission.tres" id="13_s13s0"]
[ext_resource type="Script" path="res://scripts/mission/mission_ui.gd" id="13_xvw5w"]
[ext_resource type="Resource" uid="uid://dykbopx8n3c3v" path="res://mission/third_mission.tres" id="14_bnke0"]
[ext_resource type="Script" path="res://scripts/mission/learning_panel.gd" id="14_q2ymb"]
[ext_resource type="Resource" uid="uid://bho4qh41asyk1" path="res://mission/fourth_mission.tres" id="15_plrw2"]
[ext_resource type="Resource" uid="uid://p3xwn2mp6bm6" path="res://mission/fifth_mission.tres" id="16_5fmk3"]
[ext_resource type="FontFile" uid="uid://d0cxd77jybrcn" path="res://fonts/lilita_one_regular.ttf" id="17_vlub6"]
[ext_resource type="PackedScene" uid="uid://b4gkfwf4i3ydl" path="res://scenes/character.tscn" id="18_8lrh8"]
[ext_resource type="PackedScene" uid="uid://cgk66f6rg03mj" path="res://scenes/hud.tscn" id="18_hud"]
@ -80,7 +75,7 @@ script = ExtResource("20_game_manager")
[node name="Builder" type="Node3D" parent="." node_paths=PackedStringArray("selector", "selector_container", "view_camera", "gridmap", "cash_display")]
script = ExtResource("1_jybm7")
structures = Array[ExtResource("2_54v6r")]([ExtResource("2_bwyku"), ExtResource("5_v5o2m")])
structures = Array[ExtResource("2_54v6r")]([ExtResource("2_bwyku"), ExtResource("5_v5o2m"), ExtResource("6_fwsy4")])
selector = NodePath("Selector")
selector_container = NodePath("Selector/Container")
view_camera = NodePath("../View/Camera")
@ -126,7 +121,6 @@ anchors_preset = 8
[node name="MissionManager" type="Node" parent="." node_paths=PackedStringArray("mission_ui", "builder")]
script = ExtResource("10_oe3re")
missions = Array[ExtResource("11_msovb")]([ExtResource("12_ms7i7"), ExtResource("13_s13s0"), ExtResource("14_bnke0"), ExtResource("15_plrw2"), ExtResource("16_5fmk3")])
mission_ui = NodePath("MissionPanel")
builder = NodePath("../Builder")
character_scene = ExtResource("18_8lrh8")

@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://cam5blhxixlnb"]
[ext_resource type="Script" uid="uid://cny88bdnjstg" path="res://scripts/mission/mission_ui.gd" id="1_wl28p"]
[ext_resource type="Script" path="res://scripts/mission/mission_ui.gd" id="1_wl28p"]
[node name="Control" type="Control"]
layout_mode = 3

@ -0,0 +1,95 @@
[gd_scene load_steps=4 format=3 uid="uid://cc3c30ioo11if"]
[ext_resource type="Script" path="res://scripts/mission/unlocked_items_panel.gd" id="1_j0tih"]
[ext_resource type="FontFile" uid="uid://d0cxd77jybrcn" path="res://fonts/lilita_one_regular.ttf" id="2_exmr5"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2q5cr"]
bg_color = Color(0.12549, 0.117647, 0.235294, 0.929412)
border_width_left = 4
border_width_top = 4
border_width_right = 4
border_width_bottom = 4
border_color = Color(0.796078, 0.721569, 0.133333, 1)
border_blend = true
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
shadow_color = Color(0, 0, 0, 0.45098)
shadow_size = 8
[node name="UnlockedItemsPanel" type="Control"]
process_mode = 3
z_index = 100
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_j0tih")
[node name="ColorRect" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.501961)
[node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -300.0
offset_top = -200.0
offset_right = 300.0
offset_bottom = 200.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_2q5cr")
[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer"]
layout_mode = 2
theme_override_constants/separation = 12
[node name="TitleLabel" type="Label" parent="PanelContainer/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 0.921569, 0.231373, 1)
theme_override_fonts/font = ExtResource("2_exmr5")
theme_override_font_sizes/font_size = 28
text = "New Items Unlocked!"
horizontal_alignment = 1
[node name="HSeparator" type="HSeparator" parent="PanelContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 8
[node name="ScrollContainer" type="ScrollContainer" parent="PanelContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="ItemsContainer" type="VBoxContainer" parent="PanelContainer/VBoxContainer/ScrollContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/separation = 16
[node name="MarginContainer" type="MarginContainer" parent="PanelContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/margin_top = 8
[node name="CloseButton" type="Button" parent="PanelContainer/VBoxContainer/MarginContainer"]
layout_mode = 2
theme_override_colors/font_hover_color = Color(1, 0.921569, 0.231373, 1)
theme_override_colors/font_pressed_color = Color(0.87451, 0.87451, 0.87451, 1)
theme_override_colors/font_color = Color(1, 1, 1, 1)
theme_override_fonts/font = ExtResource("2_exmr5")
theme_override_font_sizes/font_size = 20
text = "CONTINUE"
[connection signal="pressed" from="PanelContainer/VBoxContainer/MarginContainer/CloseButton" to="." method="_on_close_button_pressed"]

@ -0,0 +1,552 @@
extends Node
class_name AudioBridge
# This script provides a bridge between the Godot SoundManager
# and the React Audio Manager in platform-one
signal bridge_connected(is_connected)
var is_connected: bool = false
func _ready():
# Connect to the React sound manager if in web environment
if OS.has_feature("web"):
connect_to_sound_manager()
# Try to connect to the React sound manager
func connect_to_sound_manager() -> bool:
if not OS.has_feature("web"):
return false
print("AudioBridge: Attempting to connect to React Sound Manager")
# Check if the JavaScriptBridge is available
if not Engine.has_singleton("JavaScriptBridge"):
print("AudioBridge: JavaScriptBridge singleton not available")
return false
var js = Engine.get_singleton("JavaScriptBridge")
# Try to connect using direct function call first
var direct_connect_script = """
(function() {
if (window.connectSoundManager && typeof window.connectSoundManager === 'function') {
console.log('AudioBridge: Calling connectSoundManager directly');
return window.connectSoundManager();
}
return false;
})();
"""
var result = js.eval(direct_connect_script)
if result == true:
print("AudioBridge: Connected via direct call to connectSoundManager")
is_connected = true
bridge_connected.emit(true)
return true
# If direct call fails, try via ReactSoundBridge
var react_bridge_script = """
(function() {
if (window.ReactSoundBridge && typeof window.ReactSoundBridge.isAvailable === 'function') {
console.log('AudioBridge: Found ReactSoundBridge, checking availability');
if (window.ReactSoundBridge.isAvailable()) {
console.log('AudioBridge: ReactSoundBridge is available');
// Get initial state
window.ReactSoundBridge.getSoundState();
return true;
}
}
return false;
})();
"""
result = js.eval(react_bridge_script)
if result == true:
print("AudioBridge: Connected via ReactSoundBridge")
is_connected = true
bridge_connected.emit(true)
return true
# If both methods fail, try via postMessage
var post_message_script = """
(function() {
try {
console.log('AudioBridge: Attempting to connect via postMessage');
// Send a message to the parent window
if (window.parent) {
window.parent.postMessage({
type: 'sound_manager',
action: 'get_state',
source: 'godot-game',
timestamp: Date.now()
}, '*');
// Set up a listener if not already
if (!window._audioBridgeListener) {
window._audioBridgeListener = true;
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'sound_manager_state') {
console.log('AudioBridge: Received sound manager state');
if (typeof godot_audio_state_callback === 'function') {
godot_audio_state_callback(JSON.stringify(event.data));
}
}
});
}
return true;
}
return false;
} catch (e) {
console.error('AudioBridge: Error connecting via postMessage', e);
return false;
}
})();
"""
// Set up the callback
js.set_callback("godot_audio_state_callback", Callable(self, "_on_audio_state_received"))
result = js.eval(post_message_script)
if result == true:
print("AudioBridge: Connection attempted via postMessage")
# Note: We don't set is_connected here, we wait for the callback
return true
print("AudioBridge: All connection methods failed")
return false
# Play music through the React sound bridge
func play_music(sound_name: String) -> bool:
if not is_connected or not OS.has_feature("web"):
return false
if not Engine.has_singleton("JavaScriptBridge"):
return false
var js = Engine.get_singleton("JavaScriptBridge")
# Try direct function call first
var direct_call_script = """
(function() {
if (window.playMusic && typeof window.playMusic === 'function') {
return window.playMusic('%s');
} else if (window.ReactSoundBridge && typeof window.ReactSoundBridge.playMusic === 'function') {
return window.ReactSoundBridge.playMusic('%s');
}
return false;
})();
""" % [sound_name, sound_name]
var result = js.eval(direct_call_script)
if result == true:
return true
# If direct call fails, try postMessage
var post_message_script = """
(function() {
try {
if (window.parent) {
window.parent.postMessage({
type: 'sound_manager',
action: 'play_music',
soundName: '%s',
source: 'godot-game',
timestamp: Date.now()
}, '*');
return true;
}
return false;
} catch (e) {
console.error('AudioBridge: Error sending play_music via postMessage', e);
return false;
}
})();
""" % sound_name
result = js.eval(post_message_script)
return result == true
# Play sound effect through the React sound bridge
func play_sfx(sound_name: String) -> bool:
if not is_connected or not OS.has_feature("web"):
return false
if not Engine.has_singleton("JavaScriptBridge"):
return false
var js = Engine.get_singleton("JavaScriptBridge")
# Try direct function call first
var direct_call_script = """
(function() {
if (window.playSfx && typeof window.playSfx === 'function') {
return window.playSfx('%s');
} else if (window.ReactSoundBridge && typeof window.ReactSoundBridge.playSfx === 'function') {
return window.ReactSoundBridge.playSfx('%s');
}
return false;
})();
""" % [sound_name, sound_name]
var result = js.eval(direct_call_script)
if result == true:
return true
# If direct call fails, try postMessage
var post_message_script = """
(function() {
try {
if (window.parent) {
window.parent.postMessage({
type: 'sound_manager',
action: 'play_sfx',
soundName: '%s',
source: 'godot-game',
timestamp: Date.now()
}, '*');
return true;
}
return false;
} catch (e) {
console.error('AudioBridge: Error sending play_sfx via postMessage', e);
return false;
}
})();
""" % sound_name
result = js.eval(post_message_script)
return result == true
# Stop music through the React sound bridge
func stop_music() -> bool:
if not is_connected or not OS.has_feature("web"):
return false
if not Engine.has_singleton("JavaScriptBridge"):
return false
var js = Engine.get_singleton("JavaScriptBridge")
# Try direct function call first
var direct_call_script = """
(function() {
if (window.stopMusic && typeof window.stopMusic === 'function') {
return window.stopMusic();
} else if (window.ReactSoundBridge && typeof window.ReactSoundBridge.stopMusic === 'function') {
return window.ReactSoundBridge.stopMusic();
}
return false;
})();
"""
var result = js.eval(direct_call_script)
if result == true:
return true
# If direct call fails, try postMessage
var post_message_script = """
(function() {
try {
if (window.parent) {
window.parent.postMessage({
type: 'sound_manager',
action: 'stop_music',
source: 'godot-game',
timestamp: Date.now()
}, '*');
return true;
}
return false;
} catch (e) {
console.error('AudioBridge: Error sending stop_music via postMessage', e);
return false;
}
})();
"""
result = js.eval(post_message_script)
return result == true
# Set music volume through the React sound bridge
func set_music_volume(volume: float) -> bool:
if not is_connected or not OS.has_feature("web"):
return false
if not Engine.has_singleton("JavaScriptBridge"):
return false
var js = Engine.get_singleton("JavaScriptBridge")
# Try direct function call first
var direct_call_script = """
(function() {
if (window.setMusicVolume && typeof window.setMusicVolume === 'function') {
return window.setMusicVolume(%f);
} else if (window.ReactSoundBridge && typeof window.ReactSoundBridge.setMusicVolume === 'function') {
return window.ReactSoundBridge.setMusicVolume(%f);
}
return false;
})();
""" % [volume, volume]
var result = js.eval(direct_call_script)
if result == true:
return true
# If direct call fails, try postMessage
var post_message_script = """
(function() {
try {
if (window.parent) {
window.parent.postMessage({
type: 'sound_manager',
action: 'set_music_volume',
value: %f,
source: 'godot-game',
timestamp: Date.now()
}, '*');
return true;
}
return false;
} catch (e) {
console.error('AudioBridge: Error sending set_music_volume via postMessage', e);
return false;
}
})();
""" % volume
result = js.eval(post_message_script)
return result == true
# Set SFX volume through the React sound bridge
func set_sfx_volume(volume: float) -> bool:
if not is_connected or not OS.has_feature("web"):
return false
if not Engine.has_singleton("JavaScriptBridge"):
return false
var js = Engine.get_singleton("JavaScriptBridge")
# Try direct function call first
var direct_call_script = """
(function() {
if (window.setSfxVolume && typeof window.setSfxVolume === 'function') {
return window.setSfxVolume(%f);
} else if (window.ReactSoundBridge && typeof window.ReactSoundBridge.setSfxVolume === 'function') {
return window.ReactSoundBridge.setSfxVolume(%f);
}
return false;
})();
""" % [volume, volume]
var result = js.eval(direct_call_script)
if result == true:
return true
# If direct call fails, try postMessage
var post_message_script = """
(function() {
try {
if (window.parent) {
window.parent.postMessage({
type: 'sound_manager',
action: 'set_sfx_volume',
value: %f,
source: 'godot-game',
timestamp: Date.now()
}, '*');
return true;
}
return false;
} catch (e) {
console.error('AudioBridge: Error sending set_sfx_volume via postMessage', e);
return false;
}
})();
""" % volume
result = js.eval(post_message_script)
return result == true
# Toggle music mute through the React sound bridge
func toggle_music_mute() -> bool:
if not is_connected or not OS.has_feature("web"):
return false
if not Engine.has_singleton("JavaScriptBridge"):
return false
var js = Engine.get_singleton("JavaScriptBridge")
# Try direct function call first
var direct_call_script = """
(function() {
if (window.toggleMusicMute && typeof window.toggleMusicMute === 'function') {
return window.toggleMusicMute();
} else if (window.ReactSoundBridge && typeof window.ReactSoundBridge.toggleMusicMute === 'function') {
return window.ReactSoundBridge.toggleMusicMute();
}
return false;
})();
"""
var result = js.eval(direct_call_script)
if result == true:
return true
# If direct call fails, try postMessage
var post_message_script = """
(function() {
try {
if (window.parent) {
window.parent.postMessage({
type: 'sound_manager',
action: 'toggle_music_mute',
source: 'godot-game',
timestamp: Date.now()
}, '*');
return true;
}
return false;
} catch (e) {
console.error('AudioBridge: Error sending toggle_music_mute via postMessage', e);
return false;
}
})();
"""
result = js.eval(post_message_script)
return result == true
# Toggle SFX mute through the React sound bridge
func toggle_sfx_mute() -> bool:
if not is_connected or not OS.has_feature("web"):
return false
if not Engine.has_singleton("JavaScriptBridge"):
return false
var js = Engine.get_singleton("JavaScriptBridge")
# Try direct function call first
var direct_call_script = """
(function() {
if (window.toggleSfxMute && typeof window.toggleSfxMute === 'function') {
return window.toggleSfxMute();
} else if (window.ReactSoundBridge && typeof window.ReactSoundBridge.toggleSfxMute === 'function') {
return window.ReactSoundBridge.toggleSfxMute();
}
return false;
})();
"""
var result = js.eval(direct_call_script)
if result == true:
return true
# If direct call fails, try postMessage
var post_message_script = """
(function() {
try {
if (window.parent) {
window.parent.postMessage({
type: 'sound_manager',
action: 'toggle_sfx_mute',
source: 'godot-game',
timestamp: Date.now()
}, '*');
return true;
}
return false;
} catch (e) {
console.error('AudioBridge: Error sending toggle_sfx_mute via postMessage', e);
return false;
}
})();
"""
result = js.eval(post_message_script)
return result == true
# Request sound state from the React sound bridge
func get_sound_state() -> bool:
if not OS.has_feature("web"):
return false
if not Engine.has_singleton("JavaScriptBridge"):
return false
var js = Engine.get_singleton("JavaScriptBridge")
# Set up the callback if not already
js.set_callback("godot_audio_state_callback", Callable(self, "_on_audio_state_received"))
# Try direct function call first
var direct_call_script = """
(function() {
if (window.getSoundState && typeof window.getSoundState === 'function') {
var state = window.getSoundState();
if (typeof godot_audio_state_callback === 'function') {
godot_audio_state_callback(JSON.stringify(state));
}
return true;
} else if (window.ReactSoundBridge && typeof window.ReactSoundBridge.getSoundState === 'function') {
return window.ReactSoundBridge.getSoundState();
}
return false;
})();
"""
var result = js.eval(direct_call_script)
if result == true:
return true
# If direct call fails, try postMessage
var post_message_script = """
(function() {
try {
if (window.parent) {
window.parent.postMessage({
type: 'sound_manager',
action: 'get_state',
source: 'godot-game',
timestamp: Date.now()
}, '*');
return true;
}
return false;
} catch (e) {
console.error('AudioBridge: Error sending get_state via postMessage', e);
return false;
}
})();
"""
result = js.eval(post_message_script)
return result == true
# Called when sound state is received from React
func _on_audio_state_received(state_json: String):
print("AudioBridge: Received audio state: ", state_json)
# Parse JSON
var json = JSON.new()
var error = json.parse(state_json)
if error == OK:
var state = json.get_data()
# Mark the bridge as connected
if not is_connected:
is_connected = true
bridge_connected.emit(true)
# Emit the data for other components to use
emit_signal("sound_state_received", state)
else:
print("AudioBridge: Error parsing audio state JSON: ", error)

@ -19,6 +19,7 @@ var construction_manager: BuildingConstructionManager
@export var view_camera:Camera3D # Used for raycasting mouse
@export var gridmap:GridMap
@export var cash_display:Label # Reference to cash label in HUD
var hud_manager: Node
var plane:Plane # Used for raycasting mouse
var disabled: bool = false # Used to disable building functionality
@ -29,6 +30,7 @@ func _ready():
map = DataMap.new()
plane = Plane(Vector3.UP, Vector3.ZERO)
hud_manager = get_node_or_null("/root/Main/CanvasLayer/HUD")
# Create new MeshLibrary dynamically, can also be done in the editor
# See: https://docs.godotengine.org/en/stable/tutorials/3d/using_gridmaps.html
@ -72,6 +74,18 @@ func _ready():
gridmap.mesh_library = mesh_library
# Ensure we start with an unlocked structure
var found_unlocked = false
for i in range(structures.size()):
if "unlocked" in structures[i] and structures[i].unlocked:
index = i
found_unlocked = true
print("Starting with unlocked structure: " + structures[i].model.resource_path)
break
if not found_unlocked:
print("WARNING: No unlocked structures found at start!")
update_structure()
update_cash()
@ -200,6 +214,11 @@ func action_build(gridmap_position):
# Check if the mouse is over any UI elements before building
if is_mouse_over_ui():
return
# Check if the current structure is unlocked before allowing placement
if "unlocked" in structures[index] and not structures[index].unlocked:
print("Cannot build locked structure: " + structures[index].model.resource_path)
return
var previous_tile = gridmap.get_cell_item(gridmap_position)
@ -213,21 +232,8 @@ func action_build(gridmap_position):
var is_terrain = structures[index].type == Structure.StructureType.TERRAIN
# Check if we're in mission 3 (when we should use construction workers)
var use_worker_construction = false
var use_worker_construction = true
var mission_manager = get_node_or_null("/root/Main/MissionManager")
if mission_manager and mission_manager.current_mission:
var mission_id = mission_manager.current_mission.id
if mission_id == "3" or (mission_id == "1" and is_residential):
use_worker_construction = true
# Send placement_hint dialog if available and we're on power plant mission
if mission_id == "5" and is_power_plant and mission_manager.learning_companion_connected:
if mission_manager.current_mission.companion_dialog.has("placement_hint"):
var dialog_data = mission_manager.current_mission.companion_dialog["placement_hint"]
const JSBridge = preload("res://scripts/javascript_bridge.gd")
if JSBridge.has_interface():
JSBridge.get_interface().sendCompanionDialog("placement_hint", dialog_data)
# Sound effects are handled via game_manager.gd through the structure_placed signal
if is_road:
@ -433,15 +439,42 @@ func action_rotate():
func action_structure_toggle():
if Input.is_action_just_pressed("structure_next"):
index = wrap(index + 1, 0, structures.size())
# Find the next unlocked structure
var next_index = index
var tried_indices = []
while tried_indices.size() < structures.size():
next_index = wrap(next_index + 1, 0, structures.size())
if tried_indices.has(next_index):
break # We've already tried this index, avoid infinite loop
tried_indices.append(next_index)
# Check if this structure is unlocked
if "unlocked" in structures[next_index] and structures[next_index].unlocked:
index = next_index
break
if Input.is_action_just_pressed("structure_previous"):
index = wrap(index - 1, 0, structures.size())
# Find the previous unlocked structure
var prev_index = index
var tried_indices = []
while tried_indices.size() < structures.size():
prev_index = wrap(prev_index - 1, 0, structures.size())
if tried_indices.has(prev_index):
break # We've already tried this index, avoid infinite loop
tried_indices.append(prev_index)
# Check if this structure is unlocked
if "unlocked" in structures[prev_index] and structures[prev_index].unlocked:
index = prev_index
break
update_structure()
# Update the structure visual in the 'cursor'
func update_structure():
# Clear previous structure preview in selector
for n in selector_container.get_children():
@ -617,10 +650,9 @@ func _remove_resident_for_building(position: Vector3):
# Update the HUD population count
var hud = get_node_or_null("/root/Main/CanvasLayer/HUD")
if hud:
hud.total_population = max(0, hud.total_population - 1)
hud.update_hud()
hud.population_updated.emit(hud.total_population)
_on_update_population(-1)
func _on_update_population(count: int):
hud_manager.population_updated.emit(count)
# Function to update mission objectives when residential building is demolished
func _update_mission_objective_on_demolish():
# Get reference to mission manager

@ -26,4 +26,4 @@ func _on_close_button_pressed():
closed.emit()
# Consume the event to prevent click-through
get_viewport().set_input_as_handled()
get_viewport().set_input_as_handled()

@ -115,4 +115,4 @@ func count_residential_buildings():
residential_count += 1
found_positions.append(pos)
return residential_count
return residential_count

@ -11,6 +11,7 @@ var total_kW_production: float = 0.0
# References
var builder
var building_construction_manager
var population_label: Label
var electricity_label: Label
var electricity_indicator: ColorRect
@ -22,6 +23,7 @@ var sound_panel: PanelContainer
func _ready():
# Connect to signals from the builder
builder = get_node_or_null("/root/Main/Builder")
population_updated.connect(update_population_count)
if builder:
builder.structure_placed.connect(_on_structure_placed)
builder.structure_removed.connect(_on_structure_removed)
@ -72,11 +74,7 @@ func _on_structure_placed(structure_index, position):
if mission_manager and mission_manager.current_mission:
var mission_id = mission_manager.current_mission.id
using_construction = (mission_id == "3" or mission_id == "1")
# Only increment population for residential buildings when not using construction workers
if (!is_residential or !using_construction):
total_population += structure.population_count
# Always update electricity usage/production
total_kW_usage += structure.kW_usage
total_kW_production += structure.kW_production
@ -85,7 +83,6 @@ func _on_structure_placed(structure_index, position):
update_hud()
# Emit signals
population_updated.emit(total_population)
electricity_updated.emit(total_kW_usage, total_kW_production)
# Called when a structure is removed
@ -118,9 +115,21 @@ func _on_structure_removed(structure_index, position):
update_hud()
# Emit signals
population_updated.emit(total_population)
electricity_updated.emit(total_kW_usage, total_kW_production)
# Update Population
func update_population_count(added_population: int):
if(added_population < 0):
total_population -= added_population
else:
total_population += added_population
population_label.text = str(total_population)
# # Emit signal
# increased_population.emit(added_population)
# Updates the HUD elements
func update_hud():
# Update population label
@ -192,4 +201,4 @@ func _on_help_button_pressed():
get_viewport().set_input_as_handled()
if controls_panel:
controls_panel.show_panel()
controls_panel.show_panel()

@ -1,6 +1,7 @@
extends Node
class_name BuildingConstructionManager
# Signals
signal construction_completed(position)
signal worker_construction_started
signal worker_construction_ended
@ -9,6 +10,7 @@ const CONSTRUCTION_TIME = 10.0 # seconds to build a building
# References to necessary scenes and resources
var worker_scene: PackedScene
var hud_manager: Node
var nav_region: NavigationRegion3D
var builder: Node3D
var building_plot_scene: PackedScene
@ -20,6 +22,7 @@ var construction_sites = {} # position (Vector3) -> construction data (dict)
func _ready():
# Load the worker character scene - add more fallbacks to ensure we get a valid model
worker_scene = load("res://people/character-male-a.glb")
hud_manager = get_node_or_null("/root/Main/CanvasLayer/HUD")
if not worker_scene:
worker_scene = load("res://people/character-female-a.glb")
if not worker_scene:
@ -141,11 +144,23 @@ func _spawn_worker_for_construction(target_position: Vector3):
var road_position = _find_nearest_road(target_position)
if road_position == Vector3.ZERO:
print("ERROR: No road found for worker spawn at target position: ", target_position)
# Force completion immediately without worker since we can't spawn one
var timer = get_tree().create_timer(0.5)
timer.timeout.connect(func(): _complete_construction(target_position))
return
print("DEBUG: Spawning worker at road position: ", road_position, " for target: ", target_position)
# Create the worker
var worker = _create_worker(road_position, target_position)
if worker == null:
print("ERROR: Failed to create worker for construction at: ", target_position)
# Force completion immediately without worker
var timer = get_tree().create_timer(0.5)
timer.timeout.connect(func(): _complete_construction(target_position))
return
# Store in the construction site data
construction_sites[target_position]["worker"] = worker
@ -244,7 +259,12 @@ func _on_worker_construction_started():
func _on_worker_construction_ended():
# Forward the signal for mission managers/other systems that need it
worker_construction_ended.emit()
func _on_update_population(count: int):
hud_manager.population_updated.emit(count)
# Complete construction at a position
func _complete_construction(position: Vector3):
if not position in construction_sites:
@ -320,9 +340,10 @@ func _complete_construction(position: Vector3):
var structure = builder.structures[site["structure_index"]]
if structure.type == Structure.StructureType.RESIDENTIAL_BUILDING and structure.population_count > 0:
hud.total_population += structure.population_count
hud.update_hud()
hud.population_updated.emit(hud.total_population)
_on_update_population(structure.population_count)
# hud.total_population += structure.population_count
# hud.update_hud()
# hud.population_updated.emit(hud.total_population)
# Emit completion signal
construction_completed.emit(position)
@ -412,7 +433,9 @@ func _place_final_building(position: Vector3, structure_index: int):
if JSBridge.has_interface() and mission.companion_dialog.has("placement_success"):
var dialog_data = mission.companion_dialog["placement_success"]
JSBridge.get_interface().sendCompanionDialog("placement_success", dialog_data)
# Make a model semi-transparent with outline effect
func _make_model_transparent(model: Node3D, alpha: float):
# Find all mesh instances
@ -678,4 +701,4 @@ func _find_all_mesh_instances(node: Node, result: Array):
result.append(node)
for child in node.get_children():
_find_all_mesh_instances(child, result)
_find_all_mesh_instances(child, result)

@ -0,0 +1,91 @@
extends Node
# This is a helper script to be attached to mission_manager to handle the direct unlocked panel
# Simplified function to show the unlocked panel without using signals
func show_direct_unlocked_panel(structures, callback_node, callback_method):
print("Showing direct unlocked panel")
# Load the scene
var scene = load("res://scenes/direct_unlocked_panel.tscn")
if not scene:
push_error("Failed to load direct_unlocked_panel.tscn")
return
# Instantiate
var instance = scene.instantiate()
get_tree().root.add_child(instance)
# Get the main nodes
var panel = instance.get_node("Control")
var close_button = instance.get_node("Control/PanelContainer/MarginContainer/VBoxContainer/SimpleCloseButton")
var items_container = instance.get_node("Control/PanelContainer/MarginContainer/VBoxContainer/ScrollContainer/VBoxContainer")
# Connect close button directly
close_button.pressed.connect(func():
print("Direct panel close button pressed")
instance.queue_free()
get_tree().paused = false
callback_node.call(callback_method)
)
# Clear sample content
for child in items_container.get_children():
child.queue_free()
# Add each structure
for structure in structures:
# Create item row
var row = HBoxContainer.new()
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
items_container.add_child(row)
# Add texture
var tex_rect = TextureRect.new()
tex_rect.custom_minimum_size = Vector2(100, 100)
tex_rect.expand_mode = TextureRect.EXPAND_KEEP_ASPECT
tex_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
row.add_child(tex_rect)
# Add info container
var info = VBoxContainer.new()
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(info)
# Add name
var name_label = Label.new()
name_label.add_theme_color_override("font_color", Color(0.9, 0.9, 0.2))
name_label.add_theme_font_size_override("font_size", 18)
name_label.text = get_structure_name(structure)
info.add_child(name_label)
# Add description
var desc_label = Label.new()
desc_label.text = structure.description if structure.has_method("description") else "No description"
desc_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
info.add_child(desc_label)
# Add separator
var separator = HSeparator.new()
items_container.add_child(separator)
# Pause game
get_tree().paused = true
# Get structure name
func get_structure_name(structure):
if structure.has_method("model") and structure.model:
var path = structure.model.resource_path
var filename = path.get_file().get_basename()
# Convert kebab-case to Title Case
var words = filename.split("-")
var title_case = ""
for word in words:
if word.length() > 0:
title_case += word[0].to_upper() + word.substr(1) + " "
return title_case.strip_edges()
return "Unknown Structure"

@ -0,0 +1,62 @@
extends Node
# Simple function to handle structure unlocking for missions
static func process_structures(structures, mission_id):
for structure in structures:
# Default is locked
structure.unlocked = false
# Handle basic structures (always available)
if basic_structure(structure):
structure.unlocked = true
# Handle mission-specific structures
if mission_id == "1":
# Mission 1: Unlock residential buildings
if residential_building(structure):
structure.unlocked = true
elif mission_id == "2" or mission_id == "3":
# Missions 2-3: Unlock curved roads and decoration
if curved_road(structure) or decoration(structure):
structure.unlocked = true
elif mission_id == "4" or mission_id == "5":
# Missions 4-5: Unlock power plants
if power_plant(structure):
structure.unlocked = true
# Simple helper functions to categorize structures
static func basic_structure(structure):
var path = structure.model.resource_path
return path.contains("road-straight") or path.contains("pavement")
static func residential_building(structure):
return structure.model.resource_path.contains("building-small-a")
static func curved_road(structure):
return structure.model.resource_path.contains("road-corner")
static func decoration(structure):
return structure.model.resource_path.contains("grass-trees")
static func power_plant(structure):
return structure.model.resource_path.contains("power_plant")
# Ensure builder has an unlocked structure selected
static func select_unlocked(builder):
var found = false
# Look for an unlocked structure
for i in range(builder.structures.size()):
if builder.structures[i].unlocked:
builder.index = i
builder.update_structure()
found = true
break
# If nothing is unlocked, unlock the first structure
if not found and builder.structures.size() > 0:
builder.structures[0].unlocked = true
builder.index = 0
builder.update_structure()

@ -1,6 +1,6 @@
extends Control
signal completed
signal completed(mission)
signal panel_opened
signal panel_closed
@ -375,8 +375,8 @@ func _on_modal_complete_mission(modal):
hide_fullscreen_panel()
# Emit completed signal
completed.emit()
# Emit completed signal with mission parameter
completed.emit(mission)
func hide_fullscreen_panel():
visible = false

@ -0,0 +1,66 @@
extends Node
# This is a helper function to handle structure unlocking
# We're putting it in a separate file to avoid syntax issues in mission_manager.gd
# Unlocks basic structures and locks special ones
func process_structures(structures_array):
# Start with all unlocked
for structure in structures_array:
# Basic structures are always unlocked
if basic_structure(structure):
structure.unlocked = true
# Special structures start locked unless mission-specific
elif special_structure(structure):
structure.unlocked = false
else:
# Default to unlocked for other structures
structure.unlocked = true
# Check if this is a mission-dependent structure type that should be unlocked
func unlock_by_mission(structures_array, mission_id):
if mission_id == "1":
# Only basic structures in mission 1
return
if mission_id == "2" or mission_id == "3":
# Unlock decorations and curved roads
for structure in structures_array:
if structure.model.resource_path.contains("road-corner") or structure.model.resource_path.contains("grass-trees-tall"):
structure.unlocked = true
if mission_id == "4" or mission_id == "5":
# Unlock power plants
for structure in structures_array:
if structure.model.resource_path.contains("power_plant"):
structure.unlocked = true
# Check if a structure is one of the basic types (always available)
func basic_structure(structure):
return (structure.model.resource_path.contains("road-straight") or
structure.model.resource_path.contains("building-small-a") or
structure.model.resource_path.contains("pavement") or
structure.model.resource_path.contains("grass.glb"))
# Check if a structure is a special type (requires unlocking)
func special_structure(structure):
return (structure.model.resource_path.contains("road-corner") or
structure.model.resource_path.contains("grass-trees-tall") or
structure.model.resource_path.contains("power_plant"))
# Ensure an unlocked structure is selected
func select_unlocked_structure(builder):
# Find the first unlocked structure
var found_unlocked = false
for i in range(builder.structures.size()):
if builder.structures[i].unlocked:
builder.index = i
builder.update_structure()
found_unlocked = true
break
# If no structures are unlocked, unlock a basic one
if not found_unlocked and builder.structures.size() > 0:
builder.structures[0].unlocked = true
builder.index = 0
builder.update_structure()

@ -1,6 +1,6 @@
extends Control
signal completed
signal completed(mission)
signal panel_opened
signal panel_closed
@ -18,6 +18,12 @@ func _ready():
# Hide panel initially
visible = false
# Ensure we're set to process regardless of pause state
process_mode = Node.PROCESS_MODE_ALWAYS
# Make sure we grab input focus
mouse_filter = Control.MOUSE_FILTER_STOP
# Wait for the scene to be ready
await get_tree().process_frame
@ -47,6 +53,10 @@ func show_learning_panel(mission_data: MissionData):
mission = mission_data
# Make panel fully visible and ensure process mode is set to handle input while paused
visible = true
process_mode = Node.PROCESS_MODE_ALWAYS
# First, reset the panel to a clean state
_reset_panel()
@ -63,19 +73,27 @@ func show_learning_panel(mission_data: MissionData):
# Set up user input fields based on mission data
if mission.num_of_user_inputs > 1:
_setup_multiple_user_inputs()
# Set focus to the first input field after a short delay
get_tree().create_timer(0.2).timeout.connect(func():
if user_inputs.size() > 0 and user_inputs[0]:
user_inputs[0].grab_focus()
print("Set focus to the first input field in multi-input")
)
else:
# Traditional single input
if user_input:
user_input.placeholder_text = mission.question_text if not mission.question_text.is_empty() else "Enter your answer"
# Set focus to the single input field after a short delay
get_tree().create_timer(0.2).timeout.connect(func():
user_input.grab_focus()
print("Set focus to the single input field")
)
# Hide the HUD when learning panel is shown
var hud = get_node_or_null("/root/Main/CanvasLayer/HUD")
if hud:
hud.visible = false
# Make the panel visible
visible = true
# Make sure we're on top
if get_parent():
get_parent().move_child(self, get_parent().get_child_count() - 1)
@ -628,8 +646,8 @@ func _on_complete_mission():
# Hide the panel
hide_learning_panel()
# Emit signal
completed.emit()
# Emit signal with mission as argument
completed.emit(mission)
func _on_hint_button_pressed():
print("Hint button pressed")

@ -19,3 +19,4 @@ class_name MissionData
@export var num_of_user_inputs: int = 1 # Number of user input fields to display
@export var input_labels: Array[String] = [] # Labels for each input field
@export var companion_dialog: Dictionary = {} # Map of event keys to dialog entries for the learning companion
@export var unlocked_items: Array[String] = [] # Array of structure resource paths that get unlocked after mission completion

File diff suppressed because it is too large Load Diff

@ -10,7 +10,9 @@ enum ObjectiveType {
BUILD_INDUSTRIAL,
REACH_CASH_AMOUNT,
LEARNING,
CUSTOM
CUSTOM,
MEET_CHARACTER,
ECONOMY
}
@export var type: ObjectiveType

@ -103,3 +103,18 @@ func show_temporary_message(message: String, duration: float = 2.0, color: Color
func _on_temp_message_timer_timeout():
if temp_message_label:
temp_message_label.visible = false
# Handle updating the UI with multiple missions
func update_missions(missions_dictionary: Dictionary):
# If no missions, hide the panel
if missions_dictionary.size() == 0:
visible = false
return
# For now, just take the first mission in the dictionary
# In a multi-mission UI, we might display them differently
var mission_id = missions_dictionary.keys()[0]
var mission = missions_dictionary[mission_id]
# Use the existing function to update the display
update_mission_display(mission)

@ -0,0 +1,197 @@
extends Control
# Signal emitted when the panel is closed
signal closed
# Reference to the builder node to access all structures
var builder
func _ready():
# Make sure this control stays on top and blocks input
mouse_filter = Control.MOUSE_FILTER_STOP
# Initially hide the panel - will be shown when called
visible = false
# Find the builder reference to access structure data
builder = get_node_or_null("/root/Main/Builder")
print("UnlockedItemsPanel ready")
func setup(unlocked_structures):
print("Setting up panel with " + str(unlocked_structures.size()) + " structures")
var items_container = $PanelContainer/VBoxContainer/ScrollContainer/ItemsContainer
if not items_container:
push_error("Items container not found!")
return
# Clear any previous items
for child in items_container.get_children():
child.queue_free()
# Debug info about structures
for i in range(unlocked_structures.size()):
if unlocked_structures[i].model:
print("Structure " + str(i) + ": " + unlocked_structures[i].model.resource_path)
if "title" in unlocked_structures[i]:
print(" Title: " + unlocked_structures[i].title)
if "description" in unlocked_structures[i]:
print(" Description: " + unlocked_structures[i].description)
if "thumbnail" in unlocked_structures[i]:
print(" Thumbnail: " + unlocked_structures[i].thumbnail)
else:
print("Structure " + str(i) + ": No model")
# Add each unlocked structure
for structure in unlocked_structures:
# Skip structures without models
if not structure.model:
print("Skipping structure without model")
continue
# Create the row container
var row = HBoxContainer.new()
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
items_container.add_child(row)
# Add structure thumbnail
var icon = TextureRect.new()
icon.custom_minimum_size = Vector2(64, 64)
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH
# Try to get thumbnail from structure thumbnail field
if "thumbnail" in structure and structure.thumbnail and structure.thumbnail != "Thumbnail":
# Check if the thumbnail path is valid
if ResourceLoader.exists(structure.thumbnail):
print("Loading thumbnail from path: " + structure.thumbnail)
icon.texture = load(structure.thumbnail)
else:
print("Thumbnail path invalid: " + structure.thumbnail)
# Fall back to getting thumbnail from model
var model_thumbnail = get_structure_thumbnail(structure)
if model_thumbnail:
icon.texture = model_thumbnail
else:
# Fall back to getting thumbnail from model
var model_thumbnail = get_structure_thumbnail(structure)
if model_thumbnail:
icon.texture = model_thumbnail
# Apply type-based colors for empty thumbnails
if not icon.texture:
if structure.type == Structure.StructureType.ROAD:
# Use a road icon (fallback)
icon.modulate = Color(0.7, 0.7, 0.7)
elif structure.type == Structure.StructureType.RESIDENTIAL_BUILDING:
# Use building color (fallback)
icon.modulate = Color(0.2, 0.6, 0.9)
elif structure.type == Structure.StructureType.POWER_PLANT:
# Use power plant color (fallback)
icon.modulate = Color(0.8, 0.3, 0.3)
row.add_child(icon)
# Info container for name and description
var info = VBoxContainer.new()
info.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(info)
# Structure name - use title field if available
var name_label = Label.new()
if "title" in structure and structure.title:
name_label.text = structure.title
else:
name_label.text = get_structure_name(structure)
name_label.add_theme_font_size_override("font_size", 18)
name_label.add_theme_color_override("font_color", Color(0.9, 0.9, 0.2))
info.add_child(name_label)
# Structure description
var desc = Label.new()
if "description" in structure and structure.description and structure.description != "Description":
desc.text = structure.description
else:
desc.text = "A new structure for your city!"
desc.autowrap_mode = TextServer.AUTOWRAP_WORD
info.add_child(desc)
# Add separator if not the last item
if structure != unlocked_structures[-1]:
var sep = HSeparator.new()
items_container.add_child(sep)
func show_panel():
print("show_panel() called")
visible = true
# Ensure the game is paused when showing this panel
# This prevents learning panels from appearing on top of this one
get_tree().paused = true
# Set higher z-index to ensure it's on top
z_index = 100
# Notify mission manager if available
var mission_manager = get_node_or_null("/root/MissionManager")
if mission_manager and mission_manager.has_method("_on_unlocked_panel_shown"):
mission_manager._on_unlocked_panel_shown()
func hide_panel():
print("hide_panel() called")
visible = false
# We'll let the mission manager handle unpausing in its closed signal handler
# This ensures proper handling of the mission queue
# get_tree().paused = false
func _on_close_button_pressed():
print("Close button pressed!")
hide_panel()
closed.emit()
get_viewport().set_input_as_handled()
# New function to show all unlocked structures
func show_all_unlocked_structures():
if not builder:
builder = get_node_or_null("/root/Main/Builder")
if not builder:
push_error("Cannot find Builder node to get structures")
return
# Create an array to hold all unlocked structures
var all_unlocked = []
# Get all unlocked structures from the builder
for structure in builder.structures:
if "unlocked" in structure and structure.unlocked:
all_unlocked.append(structure)
print("Found " + str(all_unlocked.size()) + " unlocked structures")
# Call the regular setup function with all unlocked structures
setup(all_unlocked)
show_panel()
# Try to get a thumbnail for the structure
func get_structure_thumbnail(structure):
var structure_path = structure.model.resource_path
# Check if we can find a colormap texture for this model
var colormap_path = structure_path.get_basename() + "_colormap.png"
if ResourceLoader.exists(colormap_path):
return load(colormap_path)
# Default case - no icon found
return null
func get_structure_name(structure):
var file_name = structure.model.resource_path.get_file().get_basename()
var names = file_name.split("-")
var title = ""
for part in names:
if part.length() > 0:
title += part[0].to_upper() + part.substr(1) + " "
return title.strip_edges()

@ -13,6 +13,9 @@ enum StructureType {
}
@export_subgroup("Gameplay")
@export var title: String = ""
@export_subgroup("Model")
@export var model:PackedScene # Model of the structure
@ -29,3 +32,13 @@ enum StructureType {
@export_subgroup("Visual")
@export var selector_scale:float = 1.0 # Scale factor for the selector when this structure is selected
@export_subgroup("Game Progression")
@export var unlocked:bool = false # Whether this structure is available to the player
@export_subgroup("Game Progression")
@export var description: String = "Description" # Whether this structure is available to the player
@export_subgroup("Game Progression")
@export var thumbnail: String = "Thumbnail" # Whether this structure is available to the player

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://da2i0hcjhukce"
path="res://.godot/imported/checkbox.png-56c7f74df246924c046c9d85864db5f4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://sprites/checkbox.png"
dest_files=["res://.godot/imported/checkbox.png-56c7f74df246924c046c9d85864db5f4.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://om3jgm014lds"
path="res://.godot/imported/checkbox_outline.png-405c555cfeeccf96424a8a975eb5d83e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://sprites/checkbox_outline.png"
dest_files=["res://.godot/imported/checkbox_outline.png-405c555cfeeccf96424a8a975eb5d83e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://3b76xyttegyj"
path="res://.godot/imported/grass.png-24ac03dd3c22df56f9dd063178e4c133.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://sprites/residential/grass.png"
dest_files=["res://.godot/imported/grass.png-24ac03dd3c22df56f9dd063178e4c133.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://0ghyp8mkxgkq"
path="res://.godot/imported/residential-building-red.png-5577575bda87e15a7be34320a8e58f3e.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://sprites/residential/residential-building-red.png"
dest_files=["res://.godot/imported/residential-building-red.png-5577575bda87e15a7be34320a8e58f3e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cdcq7nj3ib6hc"
path="res://.godot/imported/trees.png-121171095056bf555d97c70e7420e8e1.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://sprites/residential/trees.png"
dest_files=["res://.godot/imported/trees.png-121171095056bf555d97c70e7420e8e1.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cfhhatnsmydxg"
path="res://.godot/imported/road-corner.png-d81fc439b542f9c192ba02b016c22ff3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://sprites/roads/road-corner.png"
dest_files=["res://.godot/imported/road-corner.png-d81fc439b542f9c192ba02b016c22ff3.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

@ -1,7 +1,7 @@
[gd_resource type="Resource" script_class="Structure" load_steps=3 format=3 uid="uid://bqb6g3t0tebno"]
[ext_resource type="PackedScene" uid="uid://d0nnrx2y4px2v" path="res://models/building-garage.glb" id="1_gyclk"]
[ext_resource type="Script" uid="uid://smbpvh2nwds4" path="res://scripts/structure.gd" id="2_jrinw"]
[ext_resource type="Script" path="res://scripts/structure.gd" id="2_jrinw"]
[resource]
script = ExtResource("2_jrinw")

@ -1,10 +1,11 @@
[gd_resource type="Resource" script_class="Structure" load_steps=3 format=3 uid="uid://cntgl86ianngh"]
[ext_resource type="PackedScene" uid="uid://cnycdi6t5tj01" path="res://models/building-small-a.glb" id="1_v5apy"]
[ext_resource type="Script" uid="uid://smbpvh2nwds4" path="res://scripts/structure.gd" id="2_q3i1h"]
[ext_resource type="Script" path="res://scripts/structure.gd" id="2_q3i1h"]
[resource]
script = ExtResource("2_q3i1h")
title = "Residential Building"
model = ExtResource("1_v5apy")
type = 1
price = 50
@ -12,3 +13,6 @@ population_count = 1
kW_usage = 1.0
kW_production = 0.0
selector_scale = 2.8
unlocked = false
description = "A small residential building that can house 1 person. Perfect for getting your city started!"
thumbnail = "res://sprites/residential/residential-building-red.png"

@ -1,10 +1,11 @@
[gd_resource type="Resource" script_class="Structure" load_steps=3 format=3 uid="uid://ccb475jeg7ym5"]
[ext_resource type="PackedScene" uid="uid://b1711sieed2u6" path="res://models/grass-trees.glb" id="1_lcgc1"]
[ext_resource type="Script" uid="uid://smbpvh2nwds4" path="res://scripts/structure.gd" id="2_pnpij"]
[ext_resource type="Script" path="res://scripts/structure.gd" id="2_pnpij"]
[resource]
script = ExtResource("2_pnpij")
title = ""
model = ExtResource("1_lcgc1")
type = 6
price = 25
@ -12,3 +13,6 @@ population_count = 0
kW_usage = 0.0
kW_production = 0.0
selector_scale = 2.8
unlocked = false
description = "Some cool trees and grass!"
thumbnail = "res://sprites/residential/trees.png"

@ -12,3 +12,6 @@ population_count = 0
kW_usage = 0.0
kW_production = 0.0
selector_scale = 2.8
unlocked = false
description = "Some lush green grass!"
thumbnail = "res://sprites/residential/grass.png"

@ -1,10 +1,11 @@
[gd_resource type="Resource" script_class="Structure" load_steps=3 format=3 uid="uid://d2jplegnkl6u2"]
[ext_resource type="PackedScene" uid="uid://c4ccm2qr5wa58" path="res://models/road-corner.glb" id="1_r8n8k"]
[ext_resource type="Script" uid="uid://smbpvh2nwds4" path="res://scripts/structure.gd" id="3_oloyn"]
[ext_resource type="Script" path="res://scripts/structure.gd" id="3_oloyn"]
[resource]
script = ExtResource("3_oloyn")
title = "Road Corner"
model = ExtResource("1_r8n8k")
type = 0
price = 25
@ -12,3 +13,6 @@ population_count = 0
kW_usage = 0.0
kW_production = 0.0
selector_scale = 2.8
unlocked = false
description = "Curve your roads to make them more efficient!"
thumbnail = "res://sprites/roads/road-corner.png"

@ -1,10 +1,11 @@
[gd_resource type="Resource" script_class="Structure" load_steps=3 format=3 uid="uid://dv14kkhb6umkv"]
[ext_resource type="Script" uid="uid://smbpvh2nwds4" path="res://scripts/structure.gd" id="1_5fmmh"]
[ext_resource type="Script" path="res://scripts/structure.gd" id="1_5fmmh"]
[ext_resource type="PackedScene" uid="uid://b4tgtg0j2dgh8" path="res://models/road-straight.glb" id="1_ump1f"]
[resource]
script = ExtResource("1_5fmmh")
title = "Road"
model = ExtResource("1_ump1f")
type = 0
price = 25
@ -12,3 +13,6 @@ population_count = 0
kW_usage = 0.0
kW_production = 0.0
selector_scale = 2.8
unlocked = true
description = "A basic road for connecting parts of your city."
thumbnail = "res://models/road-straight.glb"