Merge pull request #2 from STEMuli-Tx/bug/demolish-count

Bug/demolish count
pull/15/head
mrwadepro 2025-04-08 21:27:13 +07:00 committed by GitHub
commit 012ad62491
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
64 changed files with 2989 additions and 415 deletions

@ -1,10 +1,10 @@
<component name="libraryTable">
<library name="GdSdk Master" type="GdScript">
<properties path="$USER_HOME$/Library/Caches/JetBrains/Rider2024.3/projects/stem-city.46f8129/sdk/GdSdk Master" version="Master" date="2024-06-01T15:14:16.000+02:00" />
<properties path="$USER_HOME$/.cache/JetBrains/Rider2024.3/projects/stem-city.408706df/sdk/GdSdk Master" version="Master" date="2024-06-01T15:14:16.000+02:00" />
<CLASSES />
<JAVADOC />
<SOURCES>
<root url="file://$USER_HOME$/Library/Caches/JetBrains/Rider2024.3/projects/stem-city.46f8129/sdk/GdSdk Master" />
<root url="file://$USER_HOME$/.cache/JetBrains/Rider2024.3/projects/stem-city.408706df/sdk/GdSdk Master" />
</SOURCES>
</library>
</component>

@ -1,6 +1,7 @@
<p align="center"><img src="icon.png"/></p>
# Starter Kit City Builder
# Stem City
This package includes a basic template for a 3D city builder in Godot 4.3 (stable). Includes features like;
@ -10,6 +11,13 @@ This package includes a basic template for a 3D city builder in Godot 4.3 (stabl
- Saving/loading
- Sprites and 3D Models _(CC0 licensed)_
### Standards Covered
- 8.EE
- 8.EE.A - Work with radicals and integer exponents.
- 8.EE.B - Understand the connections between proportional relationships, lines, and linear equations.
### Screenshot
<p align="center"><img src="screenshots/screenshot.png"/></p>
@ -33,6 +41,11 @@ This package includes a basic template for a 3D city builder in Godot 4.3 (stabl
ATTRIBUTE
"Power Plant" (https://skfb.ly/6vZoR) by Romain PERRONE is licensed under Creative Commons Attribution (http://creativecommons.org/licenses/by/4.0/).
building placing.wav by strange_dragoon -- https://freesound.org/s/271141/ -- License: Attribution 3.0
building construct p2.wav by strange_dragoon -- https://freesound.org/s/271135/ -- License: Attribution 3.0
Assets included in this package (2D sprites, 3D models and sound effects) are [CC0 licensed](https://creativecommons.org/publicdomain/zero/1.0/)
MIT License

Binary file not shown.

@ -31,3 +31,5 @@ feedback_text = ""
incorrect_feedback = ""
company_data = ""
power_math_content = ""
num_of_user_inputs = 1
input_labels = Array[String]([])

@ -40,3 +40,5 @@ feedback_text = ""
incorrect_feedback = ""
company_data = ""
power_math_content = ""
num_of_user_inputs = 1
input_labels = Array[String]([])

@ -15,24 +15,32 @@ completed = false
[resource]
script = ExtResource("2_mum3p")
id = "4"
title = "Power Math Challenge"
description = "Your city of 40 houses needs a reliable electricity supply. Calculate the total power demand using the formula and determine how many power plants you need to build. This will require using mathematical skills with exponents and radicals."
title = "Residential Energy Usage"
description = ""
objectives = Array[ExtResource("1_dhx01")]([SubResource("Resource_c06be")])
rewards = {
"cash": 0
}
next_mission_id = "5"
graph_path = ""
full_screen_path = "res://images/city_expansion.png"
intro_text = "Your city now has 40 houses and requires reliable electricity. You'll need to build the right number of power plants to meet your city's needs."
question_text = "How many power plants are needed to power 40 houses? (Enter a number)"
correct_answer = "1"
feedback_text = "Correct! Your calculations are accurate. Total power demand is 2×√40 + 40^0.8 = 12.64 + 19.14 = 31.78 kilowatts. Since each plant generates 40 kilowatts, 1 power plant is sufficient. The plant will power all houses within a 31.6 grid unit radius (5×√40 = 31.6)."
incorrect_feedback = "Not quite right. Remember to follow these steps:
1. Calculate power needed: 2×√40 + 40^0.8
2. For √40, use 6.32 (or calculate precisely)
3. For 40^0.8, use about 19.14
4. Add both parts: 12.64 + 19.14 = 31.78 kilowatts
5. Divide by plant output (40 kW) and round up to a whole number"
full_screen_path = ""
intro_text = "A neighborhood is calculating energy usage from gaming consoles and TVs.
At House A, they ran 2 gaming consoles and 3 TVs and used a total of 660 kilowatt-hours (kWh) of electricity.
At House B, they ran 4 gaming consoles and 1 TV and used 760 kWh of electricity."
question_text = ""
correct_answer = "150,70"
feedback_text = "Correct! You've accurately calculated that gaming consoles use 150 kWh and TVs use 70 kWh. Using a system of equations: 2x + 3y = 660 and 4x + y = 760, you can solve by substitution to find x = 150 and y = 70."
incorrect_feedback = "Not quite right. Try setting up a system of equations:
- House A: 2 gaming consoles + 3 TVs = 660 kWh
- House B: 4 gaming consoles + 1 TV = 760 kWh
Let x be the kWh for gaming consoles and y be the kWh for TVs.
So we have: 2x + 3y = 660 and 4x + y = 760
Solve for y in the second equation: y = 760 - 4x
Substitute into the first equation: 2x + 3(760 - 4x) = 660
Simplify: 2x + 2280 - 12x = 660
Solve: -10x = -1620
Therefore: x = 150 and y = 70"
company_data = ""
power_math_content = ""
num_of_user_inputs = 2
input_labels = Array[String](["Gaming Consoles", "TVs"])

@ -16,7 +16,7 @@ completed = false
script = ExtResource("2_mum3p")
id = "2"
title = "Compare Construction Companies"
description = "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."
description = ""
objectives = Array[ExtResource("1_dhx01")]([SubResource("Resource_c06be")])
rewards = {
"cash": 0
@ -24,25 +24,12 @@ rewards = {
next_mission_id = ""
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."
question_text = "Which company requires fewer workers to build 40 houses in a week? (A or B)"
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 = ""
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 = "[b][color=#60c2a8]Company A: City Builders Inc.[/color][/b]
• 2 workers build 6 houses per week
• 4 workers build 12 houses per week
• 6 workers build 18 houses per week
• 10 workers build 30 houses per week
[b][color=#e06666]Company B: Urban Growth Solutions[/color][/b]
• 3 workers build 9 houses per week
• 6 workers build 18 houses per week
• 9 workers build 27 houses per week
• 12 workers build 36 houses per week
If you need 40 houses in a week, which company would require fewer workers?
Enter A or B below.
Hint: Find the pattern for each company, then calculate how many workers would be needed for 40 houses."
company_data = ""
power_math_content = ""
num_of_user_inputs = 1
input_labels = Array[String]([])

@ -31,3 +31,5 @@ feedback_text = ""
incorrect_feedback = ""
company_data = ""
power_math_content = ""
num_of_user_inputs = 1
input_labels = Array[String]([])

@ -4,15 +4,16 @@ 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"],
"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"]
dest_files=["res://.godot/imported/colormap.png-c1bc3c3aabeec406ff4b53328583776a.s3tc.ctex", "res://.godot/imported/colormap.png-c1bc3c3aabeec406ff4b53328583776a.etc2.ctex"]
[params]

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

@ -18,6 +18,14 @@ boot_splash/bg_color=Color(0.92549, 0.92549, 0.960784, 1)
boot_splash/image="res://splash-screen.png"
config/icon="res://icon.png"
[audio]
general/default_playback_type.web=0
[autoload]
SoundManager="*res://scripts/sound_manager.gd"
[display]
window/size/viewport_width=1920
@ -73,7 +81,7 @@ rotate={
}
demolish={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194312,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194308,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
camera_rotate={
@ -109,6 +117,7 @@ camera_center={
[rendering]
textures/vram_compression/import_etc2_astc=true
anti_aliasing/quality/msaa_3d=2
debug/shapes/navigation/enable_edge_lines=false
debug/shapes/navigation/enable_edge_lines.web=false

@ -0,0 +1,81 @@
[gd_scene load_steps=3 format=3 uid="uid://ib2t48fgsw62"]
[ext_resource type="FontFile" uid="uid://d0cxd77jybrcn" path="res://fonts/lilita_one_regular.ttf" id="1_ncwjb"]
[ext_resource type="Script" path="res://scripts/attribution_screen.gd" id="1_qwu4y"]
[node name="AttributionScreen" type="CanvasLayer"]
script = ExtResource("1_qwu4y")
[node name="ColorRect" type="ColorRect" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.145098, 0.172549, 0.231373, 1)
[node name="MarginContainer" type="MarginContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 100
theme_override_constants/margin_top = 100
theme_override_constants/margin_right = 100
theme_override_constants/margin_bottom = 100
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 30
[node name="TitleLabel" type="Label" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_fonts/font = ExtResource("1_ncwjb")
theme_override_font_sizes/font_size = 48
text = "Attributions"
horizontal_alignment = 1
[node name="HSeparator" type="HSeparator" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="AttributionsContainer" type="VBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/separation = 20
[node name="PowerPlantLabel" type="Label" parent="MarginContainer/VBoxContainer/AttributionsContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "3D Models - \"Power Plant\" (https://skfb.ly/6vZoR) by Romain PERRONE is licensed under Creative Commons Attribution (http://creativecommons.org/licenses/by/4.0/)"
autowrap_mode = 3
[node name="Kenny" type="Label" parent="MarginContainer/VBoxContainer/AttributionsContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "3D Models - Provided by Kenny.nl (Thanks Kenny)"
autowrap_mode = 3
[node name="SoundLabel1" type="Label" parent="MarginContainer/VBoxContainer/AttributionsContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "SoundFX - building placing.wav by strange_dragoon -- https://freesound.org/s/271141/ -- License: Attribution 3.0"
autowrap_mode = 3
[node name="SoundLabel2" type="Label" parent="MarginContainer/VBoxContainer/AttributionsContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "SoundFX - building construct p2.wav by strange_dragoon -- https://freesound.org/s/271135/ -- License: Attribution 3.0"
autowrap_mode = 3
[node name="SongLabel" type="Label" parent="MarginContainer/VBoxContainer/AttributionsContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "Music - Provided by PaoloArgento - The Best Jazz Club In New Orleans"
autowrap_mode = 3
[node name="SoundLabel3" type="Label" parent="MarginContainer/VBoxContainer/AttributionsContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "building construct p2.wav by strange_dragoon -- https://freesound.org/s/271135/ -- License: Attribution 3.0"
autowrap_mode = 3

@ -18,3 +18,5 @@ shape = SubResource("CapsuleShape3D_8rf01")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0530187, 0, 0.0432749)
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
[editable path="character-female-d2"]

@ -1,4 +1,4 @@
[gd_scene load_steps=9 format=3 uid="uid://cgk66f6rg03mj"]
[gd_scene load_steps=11 format=3 uid="uid://cgk66f6rg03mj"]
[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"]
@ -6,6 +6,7 @@
[ext_resource type="FontFile" uid="uid://d0cxd77jybrcn" path="res://fonts/lilita_one_regular.ttf" id="4_qfmf5"]
[ext_resource type="Texture2D" uid="uid://bng0d1sou7c8a" path="res://sprites/coin.png" id="5_vd1bj"]
[ext_resource type="Texture2D" uid="uid://cgpwknjrqnuae" path="res://sprites/help_icon.png" id="6_i1y88"]
[ext_resource type="Texture2D" uid="uid://bwp2j1v3vnqbf" path="res://sprites/unmuted.png" id="7_80m3c"]
[sub_resource type="LabelSettings" id="LabelSettings_q176i"]
font = ExtResource("4_qfmf5")
@ -119,6 +120,25 @@ color = Color(1, 0, 0, 1)
layout_mode = 2
theme_override_constants/separation = 10
[node name="SoundItem" type="HBoxContainer" parent="HBoxContainer"]
layout_mode = 2
mouse_filter = 0
theme_override_constants/separation = 8
[node name="SoundButton" type="TextureButton" parent="HBoxContainer/SoundItem"]
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
stretch_mode = 5
[node name="Separator4" type="VSeparator" parent="HBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="HelpItem" type="HBoxContainer" parent="HBoxContainer"]
layout_mode = 2
mouse_filter = 0
@ -188,4 +208,5 @@ autowrap_mode = 3
[connection signal="mouse_exited" from="HBoxContainer/PopulationItem/PopulationIcon" to="." method="_on_population_icon_mouse_exited"]
[connection signal="mouse_entered" from="HBoxContainer/ElectricityItem/ElectricityIcon" to="." method="_on_electricity_icon_mouse_entered"]
[connection signal="mouse_exited" from="HBoxContainer/ElectricityItem/ElectricityIcon" to="." method="_on_electricity_icon_mouse_exited"]
[connection signal="pressed" from="HBoxContainer/HelpItem/HelpButton" to="." method="_on_help_button_pressed"]
[connection signal="pressed" from="HBoxContainer/SoundItem/SoundButton" to="." method="_on_sound_button_pressed"]
[connection signal="pressed" from="HBoxContainer/HelpItem/HelpButton" to="." method="_on_help_button_pressed"]

@ -151,13 +151,6 @@ layout_mode = 2
theme_override_constants/separation = 5
alignment = 1
[node name="InputLabel" type="Label" parent="PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/UserInputContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(0.376471, 0.760784, 0.658824, 1)
theme_override_font_sizes/font_size = 32
text = "Enter your answer:"
horizontal_alignment = 1
[node name="UserInput" type="LineEdit" parent="PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/UserInputContainer"]
custom_minimum_size = Vector2(600, 80)
layout_mode = 2

@ -1,4 +1,4 @@
[gd_scene load_steps=26 format=3 uid="uid://vgwrcfy1qawf"]
[gd_scene load_steps=27 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"]
@ -21,6 +21,7 @@
[ext_resource type="PackedScene" uid="uid://cgk66f6rg03mj" path="res://scenes/hud.tscn" id="18_hud"]
[ext_resource type="PackedScene" uid="uid://bqjnp7uypupog" path="res://scenes/controls_panel.tscn" id="19_controls"]
[ext_resource type="Script" path="res://scripts/game_manager.gd" id="20_game_manager"]
[ext_resource type="PackedScene" path="res://scenes/sound_panel.tscn" id="21_sound_panel"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mission"]
bg_color = Color(0.145098, 0.172549, 0.231373, 0.941176)
@ -120,6 +121,9 @@ shadow_opacity = 0.75
[node name="ControlsPanel" parent="CanvasLayer" instance=ExtResource("19_controls")]
[node name="SoundPanel" parent="CanvasLayer" instance=ExtResource("21_sound_panel")]
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")])

@ -0,0 +1,162 @@
[gd_scene load_steps=7 format=3 uid="uid://b4s46k58ddpyc"]
[ext_resource type="Script" path="res://scripts/sound_panel.gd" id="1_c6ykp"]
[ext_resource type="FontFile" uid="uid://d0cxd77jybrcn" path="res://fonts/lilita_one_regular.ttf" id="2_kpgjp"]
[ext_resource type="Texture2D" uid="uid://bwp2j1v3vnqbf" path="res://sprites/unmuted.png" id="3_c3hj5"]
[ext_resource type="Texture2D" uid="uid://dkajgv48qw6hv" path="res://sprites/muted.png" id="4_jdlh2"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_u5w8t"]
bg_color = Color(0.145098, 0.172549, 0.231373, 0.941176)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.356863, 0.670588, 0.768627, 1)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
shadow_size = 5
shadow_offset = Vector2(2, 2)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wg3ha"]
bg_color = Color(0.356863, 0.670588, 0.768627, 1)
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4
[node name="SoundPanel" type="PanelContainer"]
process_mode = 3 # Process even when the game is paused
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -200.0
offset_top = -175.0
offset_right = 200.0
offset_bottom = 175.0
grow_horizontal = 2
grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_u5w8t")
script = ExtResource("1_c6ykp")
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 2
theme_override_constants/margin_left = 25
theme_override_constants/margin_top = 25
theme_override_constants/margin_right = 25
theme_override_constants/margin_bottom = 25
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 20
[node name="TitleLabel" type="Label" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_fonts/font = ExtResource("2_kpgjp")
theme_override_font_sizes/font_size = 32
text = "Sound Settings"
horizontal_alignment = 1
[node name="HSeparator" type="HSeparator" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_styles/separator = SubResource("StyleBoxFlat_wg3ha")
[node name="MusicSection" type="VBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="MusicTitle" type="Label" parent="MarginContainer/VBoxContainer/MusicSection"]
layout_mode = 2
theme_override_font_sizes/font_size = 22
text = "Music Volume"
[node name="MusicControls" type="HBoxContainer" parent="MarginContainer/VBoxContainer/MusicSection"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="MusicMuteButton" type="TextureButton" parent="MarginContainer/VBoxContainer/MusicSection/MusicControls"]
custom_minimum_size = Vector2(36, 36)
layout_mode = 2
size_flags_vertical = 4
toggle_mode = true
texture_normal = ExtResource("3_c3hj5")
texture_pressed = ExtResource("4_jdlh2")
ignore_texture_size = true
stretch_mode = 5
[node name="MusicSlider" type="HSlider" parent="MarginContainer/VBoxContainer/MusicSection/MusicControls"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
max_value = 1.0
step = 0.01
value = 0.8
[node name="MusicValueLabel" type="Label" parent="MarginContainer/VBoxContainer/MusicSection/MusicControls"]
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "80%"
[node name="HSeparator2" type="HSeparator" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="SFXSection" type="VBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="SFXTitle" type="Label" parent="MarginContainer/VBoxContainer/SFXSection"]
layout_mode = 2
theme_override_font_sizes/font_size = 22
text = "Sound Effects Volume"
[node name="SFXControls" type="HBoxContainer" parent="MarginContainer/VBoxContainer/SFXSection"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="SFXMuteButton" type="TextureButton" parent="MarginContainer/VBoxContainer/SFXSection/SFXControls"]
custom_minimum_size = Vector2(36, 36)
layout_mode = 2
size_flags_vertical = 4
toggle_mode = true
texture_normal = ExtResource("3_c3hj5")
texture_pressed = ExtResource("4_jdlh2")
ignore_texture_size = true
stretch_mode = 5
[node name="SFXSlider" type="HSlider" parent="MarginContainer/VBoxContainer/SFXSection/SFXControls"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 4
max_value = 1.0
step = 0.01
value = 0.8
[node name="SFXValueLabel" type="Label" parent="MarginContainer/VBoxContainer/SFXSection/SFXControls"]
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "80%"
[node name="HSeparator3" type="HSeparator" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 20
[node name="CloseButtonContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="CloseButton" type="Button" parent="MarginContainer/VBoxContainer/CloseButtonContainer"]
custom_minimum_size = Vector2(150, 50)
layout_mode = 2
theme_override_fonts/font = ExtResource("2_kpgjp")
theme_override_font_sizes/font_size = 20
text = "Close"
[connection signal="toggled" from="MarginContainer/VBoxContainer/MusicSection/MusicControls/MusicMuteButton" to="." method="_on_music_mute_button_toggled"]
[connection signal="value_changed" from="MarginContainer/VBoxContainer/MusicSection/MusicControls/MusicSlider" to="." method="_on_music_slider_value_changed"]
[connection signal="toggled" from="MarginContainer/VBoxContainer/SFXSection/SFXControls/SFXMuteButton" to="." method="_on_sfx_mute_button_toggled"]
[connection signal="value_changed" from="MarginContainer/VBoxContainer/SFXSection/SFXControls/SFXSlider" to="." method="_on_sfx_slider_value_changed"]
[connection signal="pressed" from="MarginContainer/VBoxContainer/CloseButtonContainer/CloseButton" to="." method="_on_close_button_pressed"]

@ -0,0 +1,23 @@
extends CanvasLayer
# Time in seconds to display the attribution screen
const DISPLAY_TIME: float = 3.0
const MAIN_SCENE_PATH: String = "res://scenes/main.tscn"
func _ready():
# Set up the timer to automatically transition to the main scene
var timer = Timer.new()
add_child(timer)
timer.wait_time = DISPLAY_TIME
timer.one_shot = true
timer.timeout.connect(_on_timer_timeout)
timer.start()
func _on_timer_timeout():
# Fade out the attribution screen
var tween = create_tween()
tween.tween_property(self, "modulate", Color(1, 1, 1, 0), 0.5)
tween.tween_callback(change_scene)
func change_scene():
get_tree().change_scene_to_file(MAIN_SCENE_PATH)

@ -10,6 +10,9 @@ var nav_region: NavigationRegion3D # Single navigation region for all roads
# Construction manager for building residential buildings with workers
var construction_manager: BuildingConstructionManager
# Create construction manager in _ready function
# Structure selection sound effect is now handled in game_manager.gd
@export var selector:Node3D # The 'cursor'
@export var selector_container:Node3D # Node that holds a preview of the structure
@ -37,6 +40,7 @@ func _ready():
# Setup construction manager
construction_manager = BuildingConstructionManager.new()
construction_manager.name = "BuildingConstructionManager" # Set a proper node name
add_child(construction_manager)
# Connect to the construction completion signal
@ -46,6 +50,8 @@ func _ready():
construction_manager.builder = self
construction_manager.nav_region = nav_region
# Sound effects now handled in game_manager.gd
for structure in structures:
var id = mesh_library.get_last_unused_item_id()
@ -104,31 +110,24 @@ func is_mouse_over_ui() -> bool:
# Get mouse position
var mouse_pos = get_viewport().get_mouse_position()
# Add diagnostic output
print("Mouse position: ", mouse_pos)
# Let's try an extremely simple approach - just check coordinates
# most HUDs are at top of screen
if mouse_pos.y < 100:
# Mouse is likely in the HUD area at top of screen
print("Mouse in top area (likely HUD)")
return true
# Get HUD dimensions for debug
var hud = get_node_or_null("/root/Main/CanvasLayer/HUD")
if hud:
var hud_rect = hud.get_global_rect()
print("HUD rect: ", hud_rect)
# Get HBoxContainer dimensions - this is the actual content area
var hbox = hud.get_node_or_null("HBoxContainer")
if hbox:
var hbox_rect = hbox.get_global_rect()
print("HUD HBoxContainer rect: ", hbox_rect)
# Simple approach - just check if within actual HUD content area
if hbox_rect.has_point(mouse_pos):
print("Mouse over HUD content area")
return true
# Skip the complex recursion for now since it's not working
@ -137,30 +136,23 @@ func is_mouse_over_ui() -> bool:
var mission_panel = get_node_or_null("/root/Main/MissionManager/MissionPanel")
if mission_panel and mission_panel.visible:
var panel_rect = mission_panel.get_global_rect()
print("Mission panel rect: ", panel_rect)
if panel_rect.has_point(mouse_pos):
print("Mouse over mission panel")
return true
# Check learning panel too
var learning_panel = get_node_or_null("/root/Main/MissionManager/LearningPanel")
if learning_panel and learning_panel.visible:
var panel_rect = learning_panel.get_global_rect()
print("Learning panel rect: ", panel_rect)
if panel_rect.has_point(mouse_pos):
print("Mouse over learning panel")
return true
# Check controls panel
var controls_panel = get_node_or_null("/root/Main/CanvasLayer/ControlsPanel")
if controls_panel and controls_panel.visible:
var panel_rect = controls_panel.get_global_rect()
print("Controls panel rect: ", panel_rect)
if panel_rect.has_point(mouse_pos):
print("Mouse over controls panel")
return true
print("Mouse not over any UI element")
return false
# Retrieve the mesh from a PackedScene, used for dynamically creating a MeshLibrary
@ -228,6 +220,8 @@ func action_build(gridmap_position):
if mission_id == "3" or (mission_id == "1" and is_residential):
use_worker_construction = true
# Sound effects are handled via game_manager.gd through the structure_placed signal
if is_road:
# For roads, we'll need to track in our data without using the GridMap
# But for now, we won't add it to the GridMap visually, just add to NavRegion3D
@ -265,6 +259,9 @@ func action_build(gridmap_position):
# Don't place the building immediately - it will be placed when construction completes
# We leave gridmap empty for now
# For mission 3, don't update objectives immediately - wait for construction to finish
# See _update_mission_objective_on_completion in building_construction_manager.gd
else:
# For non-road structures or not in mission 3, add to the gridmap as usual
gridmap.set_cell_item(gridmap_position, index, gridmap.get_orthogonal_index_from_basis(selector.basis))
@ -293,6 +290,9 @@ func setup_navigation_region():
nav_mesh.agent_radius = 0.25
add_child(nav_region)
# Sound effects are now handled in game_manager.gd
# Rebake navigation mesh to update the navigation data
@ -303,8 +303,7 @@ func rebake_navigation_mesh():
# Bake the navigation mesh for the entire map
nav_region.bake_navigation_mesh()
print("Navigation mesh rebaked")
# Demolish (remove) a structure
signal structure_removed(structure_index, position)
@ -340,9 +339,17 @@ func action_demolish(gridmap_position):
var current_item = gridmap.get_cell_item(gridmap_position)
var is_building = current_item >= 0
# Check for building model in the scene as a direct child of builder
var building_model_name = "Building_" + str(int(gridmap_position.x)) + "_" + str(int(gridmap_position.z))
var has_building_model = has_node(building_model_name)
# Store structure index before removal for signaling
var structure_index = -1
# Clean up any construction site at this position before demolishing
if construction_manager and is_building:
construction_manager.handle_demolition(gridmap_position)
# Remove the appropriate item
if is_road:
# Find the road structure index
@ -384,11 +391,22 @@ func action_demolish(gridmap_position):
structure_index = current_item
# Remove the building from the gridmap
gridmap.set_cell_item(gridmap_position, -1)
# Also remove any direct building model in the scene
_remove_building_model(gridmap_position)
# Check if this was a residential building to remove a resident model
if structures[structure_index].type == Structure.StructureType.RESIDENTIAL_BUILDING:
_remove_resident_for_building(gridmap_position)
# Emit signal that structure was removed
if structure_index >= 0:
structure_removed.emit(structure_index, gridmap_position)
# For mission 3, update mission objective when a residential building is demolished
if structures[structure_index].type == Structure.StructureType.RESIDENTIAL_BUILDING:
_update_mission_objective_on_demolish()
# This function is no longer needed since we're using a single NavRegion3D
# Keeping it for compatibility, but it doesn't do anything now
func remove_navigation_region(position: Vector3):
@ -425,11 +443,15 @@ func update_structure():
var _model = structures[index].model.instantiate()
selector_container.add_child(_model)
# Get reference to the selector sprite
var selector_sprite = selector.get_node("Sprite")
# Apply appropriate scaling based on structure type
if structures[index].model.resource_path.contains("power_plant"):
# Scale power plant model to be much smaller (0.5x)
_model.scale = Vector3(0.5, 0.5, 0.5)
_model.position.y += 0.0 # No need for Y adjustment with scaling
# Center the power plant model within the selector
_model.position = Vector3(-3.0, 0.0, 3.0) # Reset position
elif (structures[index].type == Structure.StructureType.RESIDENTIAL_BUILDING
or structures[index].type == Structure.StructureType.ROAD
or structures[index].type == Structure.StructureType.TERRAIN
@ -441,6 +463,12 @@ func update_structure():
# Standard positioning for other structures
_model.position.y += 0.25
# Get the selector scale from the structure resource
var scale_factor = structures[index].selector_scale
selector_sprite.scale = Vector3(scale_factor, scale_factor, scale_factor)
# Sound effects are now handled in game_manager.gd
func update_cash():
cash_display.text = "$" + str(map.cash)
@ -455,7 +483,6 @@ func _add_road_to_navregion(position: Vector3, structure_index: int):
# Check if a road with this name already exists
if nav_region.has_node(road_name):
return
# Instantiate the road model - get the actual model based on road type
@ -501,7 +528,6 @@ func _add_power_plant(position: Vector3, structure_index: int):
# Check if a power plant with this name already exists
if has_node(power_plant_name):
return
# Instantiate the power plant model
@ -520,9 +546,14 @@ func _add_power_plant(position: Vector3, structure_index: int):
# Apply rotation from the selector to preserve the rotation the player chose
transform.basis = transform.basis * selector.basis
# Set position
# Set position with offset to center the model at the grid position
transform.origin = position
# Apply position offset to center the model (matching the preview)
# These offsets need to be transformed based on the current rotation
var offset = selector.basis * Vector3(0.25, 0, -0.25)
transform.origin += offset
# Apply the complete transform in one go
power_plant_model.transform = transform
@ -539,7 +570,60 @@ func _remove_power_plant(position: Vector3):
power_plant.queue_free()
else:
print("No power plant found at position ", position)
# No power plant found
pass
# Function to remove a resident model when a residential building is demolished
func _remove_resident_for_building(position: Vector3):
# First, check if we have a nav region reference
if not nav_region and has_node("NavRegion3D"):
nav_region = get_node("NavRegion3D")
if nav_region:
# Look for resident with matching position in the name
var resident_name = "Resident_" + str(int(position.x)) + "_" + str(int(position.z))
# First try to find by exact name
var found = false
for child in nav_region.get_children():
if child.name.begins_with(resident_name):
child.queue_free()
found = true
# 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)
break
# If not found by exact name, find any resident (as a fallback)
if not found:
var residents = get_tree().get_nodes_in_group("characters")
if residents.size() > 0:
# Just remove the first resident we find
residents[0].queue_free()
# 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)
# Function to update mission objectives when residential building is demolished
func _update_mission_objective_on_demolish():
# Get reference to mission manager
var mission_manager = get_node_or_null("/root/Main/MissionManager")
if mission_manager and mission_manager.current_mission:
# Check if we're in mission 3 (build 40 residential buildings)
if mission_manager.current_mission.id != "3":
# For other missions, use the normal method
var mission_id = mission_manager.current_mission.id
mission_manager.update_objective_progress(mission_id, MissionObjective.ObjectiveType.BUILD_RESIDENTIAL, -1)
# Function to remove terrain (grass or trees)
func _remove_terrain(position: Vector3):
@ -551,9 +635,71 @@ func _remove_terrain(position: Vector3):
# Get the terrain and remove it
var terrain = get_node(terrain_name)
terrain.queue_free()
print("Removed terrain at position ", position)
else:
print("No terrain found at position ", position)
# No terrain found
pass
# Function to remove building model from scene
func _remove_building_model(position: Vector3):
# Try multiple possible naming patterns
var building_patterns = [
"Building_" + str(int(position.x)) + "_" + str(int(position.z)),
"building-small-a_" + str(int(position.x)) + "_" + str(int(position.z)),
"building-small-b_" + str(int(position.x)) + "_" + str(int(position.z)),
"building-small-c_" + str(int(position.x)) + "_" + str(int(position.z)),
"building-small-d_" + str(int(position.x)) + "_" + str(int(position.z)),
"building-garage_" + str(int(position.x)) + "_" + str(int(position.z))
]
# Check if we can find the building model with any of the pattern names
var found = false
for pattern in building_patterns:
if has_node(pattern):
# Get the building and remove it
var building = get_node(pattern)
building.queue_free()
found = true
break
# If not found as direct child, try to find by position in navigation region
if !found and nav_region:
for child in nav_region.get_children():
# Skip non-building nodes
if !child.name.begins_with("Building") and !child.name.begins_with("building"):
continue
# Check if this building is at our position (with some tolerance)
var pos_diff = (child.global_transform.origin - position).abs()
if pos_diff.x < 0.5 and pos_diff.z < 0.5:
child.queue_free()
found = true
break
# If still not found, search the entire scene
if !found:
var main = get_node_or_null("/root/Main")
if main:
for child in main.get_children():
# Skip non-building nodes
if !child.name.begins_with("Building") and !child.name.begins_with("building"):
continue
# Check if this building is at our position (with some tolerance)
var pos_diff = (child.global_transform.origin - position).abs()
if pos_diff.x < 0.5 and pos_diff.z < 0.5:
child.queue_free()
found = true
break
# If STILL not found, try one last approach - scan for gridmap children
if !found and gridmap:
for child in gridmap.get_children():
# Check if this is any model at our position (with some tolerance)
var pos_diff = (child.global_transform.origin - position).abs()
if pos_diff.x < 0.5 and pos_diff.z < 0.5:
child.queue_free()
found = true
break
# Function to remove a road model from the navigation region
func _remove_road_from_navregion(position: Vector3):
@ -569,9 +715,9 @@ func _remove_road_from_navregion(position: Vector3):
# Get the road and remove it
var road = nav_region.get_node(road_name)
road.queue_free()
print("Removed road at position ", position, " from NavRegion3D")
else:
print("No road found at position ", position, " in NavRegion3D")
# No road found
pass
# Function to add all existing roads to the navigation region
func _add_existing_roads_to_navregion():
@ -585,7 +731,6 @@ func _add_existing_roads_to_navregion():
child.queue_free()
# Find all road cells in the gridmap
print("Finding and adding all existing roads to NavRegion3D...")
var added_count = 0
# We need to convert any existing roads in the GridMap to our new system
@ -600,8 +745,6 @@ func _add_existing_roads_to_navregion():
gridmap.set_cell_item(cell, -1)
added_count += 1
print("Added ", added_count, " existing roads to NavRegion3D")
# Function to move all character NPCs to be children of the navigation region
func _move_characters_to_navregion():
# Make sure we have a navigation region
@ -635,7 +778,6 @@ func _add_terrain(position: Vector3, structure_index: int):
# Check if terrain with this name already exists
if has_node(terrain_name):
return
# Instantiate the terrain model
@ -659,8 +801,6 @@ func _add_terrain(position: Vector3, structure_index: int):
# Apply the complete transform in one go
terrain_model.transform = transform
print("Added terrain at position ", position, " as direct child of builder")
# Callback for when construction is completed
func _on_construction_completed(position: Vector3):
@ -684,7 +824,6 @@ func _on_construction_completed(position: Vector3):
# Add the completed residential building to the gridmap with the correct rotation
gridmap.set_cell_item(position, residential_index, rotation_index)
print("Construction completed: added building to gridmap at ", position, " with rotation index ", rotation_index)
# Check if we need to spawn a character for mission 1
var mission_manager = get_node_or_null("/root/Main/MissionManager")
@ -695,7 +834,6 @@ func _on_construction_completed(position: Vector3):
# Now check if we need to manually handle mission 1 character spawning
if mission_manager.current_mission and mission_manager.current_mission.id == "1" and not mission_manager.character_spawned:
print("This is the first residential building in mission 1, spawning character")
mission_manager.character_spawned = true
mission_manager._spawn_character_on_road(position)
@ -704,7 +842,8 @@ func _on_construction_completed(position: Vector3):
# We don't emit the signal anymore to prevent double-counting
pass
else:
print("ERROR: No residential building structure found!")
# No residential building structure found
pass
# Make sure all characters (including newly spawned residents) are children of NavRegion3D
_move_characters_to_navregion()
@ -712,14 +851,15 @@ func _on_construction_completed(position: Vector3):
# Make sure the navigation mesh is updated
rebake_navigation_mesh()
# Note that mission objective updates are now handled in the construction manager
# to ensure they only occur after construction is complete
# Saving/load
func action_save():
if Input.is_action_just_pressed("save"):
print("Saving map...")
map.structures.clear()
for cell in gridmap.get_used_cells():
@ -735,8 +875,6 @@ func action_save():
func action_load():
if Input.is_action_just_pressed("load"):
print("Loading map...")
gridmap.clear()
map = ResourceLoader.load("user://map.res")

@ -0,0 +1,118 @@
extends Node
# This script provides an accurate counter for mission 3's residential buildings
# Instead of polling, it listens for signals when buildings are constructed or demolished
func _ready():
# Wait a moment for the game to initialize
await get_tree().create_timer(1.0).timeout
# Get builder reference
var builder = get_node_or_null("/root/Main/Builder")
if builder:
# Connect to structure placed and removed signals
builder.structure_placed.connect(_on_structure_placed)
builder.structure_removed.connect(_on_structure_removed)
builder.construction_manager.construction_completed.connect(_on_construction_completed)
# Do initial count on mission start
update_mission_3_count()
# Called when a structure is placed
func _on_structure_placed(structure_index, position):
# We don't need immediate action - residential counts are updated after construction
pass
# Called when a structure is removed
func _on_structure_removed(structure_index, position):
var builder = get_node_or_null("/root/Main/Builder")
if builder and structure_index >= 0 and structure_index < builder.structures.size():
if builder.structures[structure_index].type == 1: # Residential building
# Wait one frame to make sure the GridMap is updated
await get_tree().process_frame
# Update the count
update_mission_3_count()
# Called when construction is completed
func _on_construction_completed(position):
update_mission_3_count()
# Updates the mission 3 objective count based on actual residential buildings
func update_mission_3_count():
# Find the mission manager
var mission_manager = get_node_or_null("/root/Main/MissionManager")
if not mission_manager:
return
# Check if we're in mission 3
if mission_manager.current_mission and mission_manager.current_mission.id == "3":
# Count the actual number of residential buildings
var count = count_residential_buildings()
# Get the current objective count
var current_count = 0
for objective in mission_manager.current_mission.objectives:
if objective.type == 3: # BUILD_RESIDENTIAL type
current_count = objective.current_count
break
# Only update if the counts don't match
if current_count != count:
# Reset the objective count to match the actual number
mission_manager.reset_objective_count(3, count) # 3 is the BUILD_RESIDENTIAL type
func count_residential_buildings():
# Find the builder
var builder = get_node_or_null("/root/Main/Builder")
if not builder:
return 0
# Find the gridmap
var gridmap = builder.gridmap
if not gridmap:
return 0
# Count residential buildings in the gridmap
var residential_count = 0
var found_positions = []
# First count buildings in the gridmap
for cell in gridmap.get_used_cells():
var structure_index = gridmap.get_cell_item(cell)
if structure_index >= 0 and structure_index < builder.structures.size():
if builder.structures[structure_index].type == 1: # 1 is RESIDENTIAL_BUILDING type
residential_count += 1
found_positions.append(Vector2(cell.x, cell.z))
# Also count completed buildings that might not be in the gridmap
if builder.has_node("NavRegion3D"):
var nav_region = builder.get_node("NavRegion3D")
for child in nav_region.get_children():
if child.name.begins_with("Building_"):
var parts = child.name.split("_")
if parts.size() >= 3:
var x = int(parts[1])
var z = int(parts[2])
var pos = Vector2(x, z)
# Only count if we haven't already counted this position
if not pos in found_positions:
residential_count += 1
found_positions.append(pos)
# Also count any buildings under construction
if builder.construction_manager:
for position in builder.construction_manager.construction_sites:
var site = builder.construction_manager.construction_sites[position]
if site.structure_index >= 0 and site.structure_index < builder.structures.size():
if builder.structures[site.structure_index].type == 1 and site.completed: # Only count completed residential buildings
# Check if there's actually a building at this position in the GridMap
var cell_item = builder.gridmap.get_cell_item(position)
if cell_item >= 0: # Only count if there's still a building in the GridMap
var pos = Vector2(position.x, position.z)
if not pos in found_positions:
residential_count += 1
found_positions.append(pos)
return residential_count

@ -2,13 +2,22 @@ extends Node
# This script handles overall game management tasks
var music_player: AudioStreamPlayer
var building_sfx: AudioStreamPlayer
var construction_sfx: AudioStreamPlayer
func _ready():
# Reference to the controls panel and HUD
# Register SoundManager in the main loop for JavaScript bridge to find
Engine.get_main_loop().set_meta("sound_manager", get_node_or_null("/root/SoundManager"))
# Reference to the controls panel, sound panel, and HUD
var controls_panel = $CanvasLayer/ControlsPanel
var sound_panel = $CanvasLayer/SoundPanel
var hud = $CanvasLayer/HUD
# Set up the HUD's reference to the controls panel
# Set up the HUD's reference to the panels
hud.controls_panel = controls_panel
hud.sound_panel = sound_panel
# Auto-show controls at start
if controls_panel:
@ -16,7 +25,282 @@ func _ready():
# Connect the closed signal to handle when player closes the controls
controls_panel.closed.connect(_on_controls_panel_closed)
# Check for audio initialization status (important for web)
var sound_manager = get_node_or_null("/root/SoundManager")
var can_initialize_audio = true
if OS.has_feature("web") and sound_manager:
can_initialize_audio = sound_manager.audio_initialized
if not can_initialize_audio:
# For web, wait for the audio_ready signal before initializing audio
sound_manager.audio_ready.connect(_initialize_game_audio)
# Set up audio if allowed (immediate for desktop, after interaction for web)
if can_initialize_audio:
_initialize_game_audio()
# Find the builder and connect to it
var builder = get_node_or_null("/root/Main/Builder")
if builder:
builder.structure_placed.connect(_on_structure_placed)
# Connect to construction signals via deferred call to make sure everything is ready
call_deferred("_setup_construction_signals")
# Make sure sound buses are properly configured
call_deferred("_setup_sound_buses")
# Initialize all game audio - called immediately on desktop, after user interaction on web
func _initialize_game_audio():
# Set up all audio systems
setup_background_music()
setup_building_sfx()
setup_construction_sfx()
# This function is called when the controls panel is closed
func _on_controls_panel_closed():
print("Controls panel closed by player")
# This is the perfect place to initialize audio for web builds
# since we know the user has interacted with the game
if OS.has_feature("web"):
# Force initialize the sound manager (will have no effect if already initialized)
var sound_manager = get_node_or_null("/root/SoundManager")
if sound_manager and not sound_manager.audio_initialized:
sound_manager._initialize_web_audio()
# Make sure our music is playing
if music_player and music_player.stream and not music_player.playing:
music_player.play()
# Function to set up the sound buses
func _setup_sound_buses():
# Wait a moment to ensure SoundManager is ready
await get_tree().process_frame
# Get reference to SoundManager singleton
var sound_manager = get_node_or_null("/root/SoundManager")
if !sound_manager:
return
# Move audio players to the appropriate buses
if music_player:
music_player.bus = "Music"
if building_sfx:
building_sfx.bus = "SFX"
if construction_sfx:
construction_sfx.bus = "SFX"
# Setup background music player
func setup_background_music():
music_player = AudioStreamPlayer.new()
add_child(music_player)
# Set this to make the music player ignore the game tree's pause state
music_player.process_mode = Node.PROCESS_MODE_ALWAYS
# Use a direct file path for the music file to avoid any loading issues
var music_path = "res://sounds/jazz_new_orleans.mp3"
# Try both direct preload and load for maximum compatibility
var music = null
# Try preload first - this ensures MP3 is pre-decoded
music = preload("res://sounds/jazz_new_orleans.mp3")
# If preload failed, try regular load
if !music:
music = load(music_path)
# Continue setup if we have the music file
if music:
# Set looping on the AudioStreamMP3 itself
if music is AudioStreamMP3:
music.loop = true
music_player.stream = music
music_player.volume_db = 0 # Full volume for better web playback
music_player.bus = "Music" # Use the Music bus
# Direct check of music bus
var music_bus_idx = AudioServer.get_bus_index("Music")
if music_bus_idx >= 0:
# Force bus volume
AudioServer.set_bus_volume_db(music_bus_idx, 0)
AudioServer.set_bus_mute(music_bus_idx, false)
# Check if we can play audio immediately (desktop) or need to wait (web)
var can_play_now = true
if OS.has_feature("web"):
var sound_manager = get_node_or_null("/root/SoundManager")
if sound_manager:
can_play_now = sound_manager.audio_initialized
# Force SoundManager settings
sound_manager.music_volume = 1.0
sound_manager.music_muted = false
sound_manager._apply_music_volume()
# If not initialized, connect to the ready signal
if not can_play_now:
sound_manager.audio_ready.connect(_start_background_music)
# Play immediately if allowed
if can_play_now:
_start_background_music()
else:
# Try a fallback sound as music
var fallback_sound = load("res://sounds/building_placing.wav")
if fallback_sound:
music_player.stream = fallback_sound
music_player.volume_db = 0
music_player.bus = "Music"
# Check if we can play immediately
var can_play_now = true
if OS.has_feature("web"):
var sound_manager = get_node_or_null("/root/SoundManager")
if sound_manager:
can_play_now = sound_manager.audio_initialized
if can_play_now:
music_player.play()
# Start background music playing (called directly or via signal)
func _start_background_music():
if music_player and music_player.stream and not music_player.playing:
# For web builds, use a simple approach to starting audio
if OS.has_feature("web"):
# Make sure we start from the beginning
music_player.stop()
music_player.seek(0.0)
# Set reasonable volume
music_player.volume_db = -10 # Normal volume for web
music_player.bus = "Music"
# Make sure buses are unmuted
AudioServer.set_bus_mute(0, false) # Master
# Music bus
var music_bus_idx = AudioServer.get_bus_index("Music")
if music_bus_idx >= 0:
AudioServer.set_bus_mute(music_bus_idx, false)
# Play the music
music_player.play()
# Simple JavaScript to ensure audio context is running
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
js.eval("""
(function() {
try {
if (window._godotAudioContext && window._godotAudioContext.state === 'suspended') {
console.log('GameManager: Resuming audio context');
window._godotAudioContext.resume();
}
} catch(e) {
console.error('GameManager: Error in audio context check:', e);
}
})()
""")
else:
# Standard approach for desktop builds
music_player.play()
# This retry audio function has been removed in favor of the simpler approach
# This helper has been removed in favor of a simpler approach
# Setup building sound effects
func setup_building_sfx():
building_sfx = AudioStreamPlayer.new()
add_child(building_sfx)
# Set this to make the sound effects player ignore the game tree's pause state
building_sfx.process_mode = Node.PROCESS_MODE_ALWAYS
var sfx = load("res://sounds/building_placing.wav")
if sfx:
building_sfx.stream = sfx
building_sfx.volume_db = -5
building_sfx.bus = "SFX" # Use the SFX bus
# Setup construction sound effects
# Note: Now mainly used for backward compatibility
# Individual workers handle their own construction sounds
func setup_construction_sfx():
construction_sfx = AudioStreamPlayer.new()
add_child(construction_sfx)
# Set this to make the sound effects player ignore the game tree's pause state
construction_sfx.process_mode = Node.PROCESS_MODE_ALWAYS
var sfx = load("res://sounds/construction.wav")
if sfx:
construction_sfx.stream = sfx
construction_sfx.volume_db = -8 # Reduced volume since workers have their own sounds
construction_sfx.bus = "SFX" # Use the SFX bus
# Play the building sound effect when a structure is placed
func _on_structure_placed(structure_index, position):
# Check web audio initialized status if needed
var can_play_audio = true
if OS.has_feature("web"):
var sound_manager = get_node_or_null("/root/SoundManager")
if sound_manager:
can_play_audio = sound_manager.audio_initialized
# Only play if audio is initialized (always true on desktop, depends on user interaction for web)
if can_play_audio and building_sfx and building_sfx.stream:
if building_sfx.playing:
building_sfx.stop()
building_sfx.play()
# Variables for construction sound looping
var construction_active = false
var construction_sound_timer = null
# These functions remain for backward compatibility with mission logic
# but they don't actually play sounds anymore since workers handle their own sounds
# Compatibility function for mission triggers
func play_construction_sound():
# We don't play any sounds from here anymore - workers handle their own sounds
# but we need to keep this function for backward compatibility
pass
# Compatibility function for mission triggers
func _loop_construction_sound():
# This function exists only for backward compatibility
pass
# Compatibility function for mission triggers
func stop_construction_sound():
# We don't stop any sounds from here anymore - workers handle their own sounds
# but we need to keep this function for backward compatibility
pass
# Removed duplicate _retry_music_play function that was here
# Setup construction signals properly
func _setup_construction_signals():
var builder = get_node_or_null("/root/Main/Builder")
if builder and builder.has_method("get") and builder.get("construction_manager"):
var construction_manager = builder.construction_manager
if construction_manager:
# Disconnect any existing connections first to avoid duplicates
if construction_manager.worker_construction_started.is_connected(play_construction_sound):
construction_manager.worker_construction_started.disconnect(play_construction_sound)
if construction_manager.worker_construction_ended.is_connected(stop_construction_sound):
construction_manager.worker_construction_ended.disconnect(stop_construction_sound)
# Connect signals
construction_manager.worker_construction_started.connect(play_construction_sound)
construction_manager.worker_construction_ended.connect(stop_construction_sound)

@ -17,6 +17,7 @@ var electricity_indicator: ColorRect
var population_tooltip: Control
var electricity_tooltip: Control
var controls_panel: PanelContainer
var sound_panel: PanelContainer
func _ready():
# Connect to signals from the builder
@ -64,9 +65,6 @@ func _on_structure_placed(structure_index, position):
var structure = builder.structures[structure_index]
# Debug info
print("Structure placed: " + str(structure.type) + " with population: " + str(structure.population_count))
# Only update population for non-residential buildings or if we're NOT in the construction mission
var is_residential = structure.type == Structure.StructureType.RESIDENTIAL_BUILDING
var mission_manager = get_node_or_null("/root/Main/MissionManager")
@ -82,7 +80,6 @@ func _on_structure_placed(structure_index, position):
# Always update electricity usage/production
total_kW_usage += structure.kW_usage
total_kW_production += structure.kW_production
print("Energy updated - Usage: " + str(total_kW_usage) + " kW, Production: " + str(total_kW_production) + " kW")
# Update HUD
update_hud()
@ -98,13 +95,24 @@ func _on_structure_removed(structure_index, position):
var structure = builder.structures[structure_index]
# Update population
total_population = max(0, total_population - structure.population_count)
# Update population (but only for non-residential buildings in mission 3)
# For residential buildings in mission 3, we handle population separately in builder._remove_resident_for_building
var skip_population_update = false
var mission_manager = get_node_or_null("/root/Main/MissionManager")
if mission_manager and mission_manager.current_mission:
if mission_manager.current_mission.id == "3" and structure.type == Structure.StructureType.RESIDENTIAL_BUILDING:
# Only update population for one resident, since we're removing them one by one
# We don't do total reset based on structure.population_count
skip_population_update = true
# We decrement by 1 in builder._remove_resident_for_building instead
if !skip_population_update:
total_population = max(0, total_population - structure.population_count)
# Update electricity
total_kW_usage = max(0, total_kW_usage - structure.kW_usage)
total_kW_production = max(0, total_kW_production - structure.kW_production)
print("Energy updated after removal - Usage: " + str(total_kW_usage) + " kW, Production: " + str(total_kW_production) + " kW")
# Update HUD
update_hud()
@ -152,7 +160,6 @@ func update_hud():
# Update the color of the indicator rectangle
if electricity_indicator:
electricity_indicator.color = indicator_color
print("Electricity indicator updated - Color: " + str(indicator_color))
# Tooltip handling
func _on_population_icon_mouse_entered():
@ -171,10 +178,18 @@ func _on_electricity_icon_mouse_exited():
if electricity_tooltip:
electricity_tooltip.visible = false
# Called when the sound button is pressed
func _on_sound_button_pressed():
# Consume the event to prevent click-through to the world
get_viewport().set_input_as_handled()
if sound_panel:
sound_panel.show_panel()
# Called when the help button is pressed
func _on_help_button_pressed():
# Consume the event to prevent click-through to the world
get_viewport().set_input_as_handled()
if controls_panel:
controls_panel.show_panel()
controls_panel.show_panel()

@ -0,0 +1,506 @@
extends Node
class_name JSBridge
# This script provides a bridge to JavaScript functionality
# while gracefully handling platforms that don't support it
# Check if JavaScript is available
static func has_interface() -> bool:
# Check if running in a web environment
# Use OS.has_feature("web") for consistency with sound_manager.gd
if OS.has_feature("web"):
print("Running in web environment, JavaScript should be available")
# Double-check by evaluating a simple script
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
var test_result = js.eval("!!window && typeof window !== 'undefined'")
print("JavaScript test result: " + str(test_result))
return test_result != null
else:
print("JavaScriptBridge singleton not available, running in editor or non-web platform")
else:
print("Not running in web environment")
return false
# Get the JavaScript interface
static func get_interface():
if has_interface():
return JavaScriptGlobal
return null
# JavaScriptGlobal is a mock class that provides fallback implementations
# for platforms that don't support JavaScript
class JavaScriptGlobal:
# Check if a JavaScript function exists
static func has_function(function_name: String) -> bool:
if not OS.has_feature("web"):
return false
print("Checking if function exists: " + function_name)
var script = "typeof %s === 'function'" % function_name
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
var result = js.eval(script)
# If result is null, the JavaScript eval failed
if result == null:
print("JavaScript eval failed when checking for function: " + function_name)
return false
print("Function check result for " + function_name + ": " + str(result))
return result
else:
print("JavaScriptBridge singleton not available")
return false
# Evaluate JavaScript code
static func eval(script: String):
if not OS.has_feature("web"):
return null
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
return js.eval(script)
else:
print("JavaScriptBridge singleton not available")
return null
# Call a JavaScript function with arguments
static func call_js_function(function_name: String, args = []):
if not OS.has_feature("web"):
return null
var formatted_args = []
for arg in args:
if arg is String:
formatted_args.append("\"%s\"" % arg.replace("\"", "\\\""))
elif arg is Dictionary or arg is Array:
formatted_args.append(JSON.stringify(arg))
else:
formatted_args.append(str(arg))
var script = "%s(%s)" % [function_name, ",".join(formatted_args)]
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
return js.eval(script)
else:
print("JavaScriptBridge singleton not available")
return null
# Connect to the learning companion - legacy method with postMessage fallback
static func connectLearningCompanion(success_callback = null, error_callback = null):
print("Attempting to connect to learning companion")
if not OS.has_feature("web"):
print("Skipping learning companion connection on non-web platform")
if error_callback != null and error_callback.is_valid():
error_callback.call()
return
# Always use postMessage approach regardless of function availability
connectLearningCompanionViaPostMessage(success_callback, error_callback)
# Connect to the learning companion using only postMessage
static func connectLearningCompanionViaPostMessage(success_callback = null, error_callback = null):
print("Connecting to learning companion via postMessage")
if not OS.has_feature("web"):
print("Skipping learning companion connection on non-web platform")
if error_callback != null and error_callback.is_valid():
error_callback.call()
return
# Use postMessage approach exclusively - note: no return statements allowed in the script
var script = """
(function() {
try {
// Send a message directly to the parent window
if (window.parent) {
console.log('Sending connection message to parent window');
window.parent.postMessage({
type: 'stemCity_connect',
source: 'godot-game',
timestamp: Date.now()
}, '*');
// Set up a global event listener for responses if not already set up
if (!window._stemCityListenerInitialized) {
window._stemCityListenerInitialized = true;
window.addEventListener('message', function(event) {
console.log('Game received message:', event.data);
if (event.data && event.data.type === 'stemCity_connect_ack') {
console.log('Received connection acknowledgment from parent');
}
});
}
// Don't use return statements here - they're not allowed in top-level eval
var result = true;
} else {
console.log('No parent window found');
var result = false;
}
} catch (e) {
console.error('Error connecting via postMessage:', e);
var result = false;
}
})();
"""
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
js.eval(script)
# Always consider this a success - we'll use the force connection timer as backup
print("Sent connection message via postMessage")
# Try to ensure audio is initialized as well since we now have user interaction
JavaScriptGlobal.ensure_audio_initialized()
if success_callback != null and success_callback.is_valid():
success_callback.call()
else:
print("JavaScriptBridge singleton not available")
if error_callback != null and error_callback.is_valid():
error_callback.call()
# The following methods call the JavaScript functions for game events using postMessage
static func onGameStarted():
if not OS.has_feature("web"):
return
print("Sending game started event via postMessage")
var script = """
(function() {
try {
if (window.parent) {
console.log('Sending gameStarted message to parent window');
window.parent.postMessage({
type: 'stemCity_gameStarted',
source: 'godot-game',
timestamp: Date.now()
}, '*');
} else {
console.log('No parent window found for gameStarted event');
}
} catch (e) {
console.error('Error sending gameStarted via postMessage:', e);
}
})();
"""
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
js.eval(script)
else:
print("JavaScriptBridge singleton not available")
static func onMissionStarted(mission_data: Dictionary):
if not OS.has_feature("web"):
return
print("Sending mission started event for mission: " + str(mission_data.get("id", "unknown")))
var mission_json = JSON.stringify(mission_data)
var script = """
(function() {
try {
if (window.parent) {
console.log('Sending missionStarted message to parent window');
window.parent.postMessage({
type: 'stemCity_missionStarted',
data: %s,
source: 'godot-game',
timestamp: Date.now()
}, '*');
} else {
console.log('No parent window found for missionStarted event');
}
} catch (e) {
console.error('Error sending missionStarted via postMessage:', e);
}
})();
""" % mission_json
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
js.eval(script)
else:
print("JavaScriptBridge singleton not available")
static func onMissionCompleted(mission_data: Dictionary):
if not OS.has_feature("web"):
return
print("Sending mission completed event for mission: " + str(mission_data.get("id", "unknown")))
var mission_json = JSON.stringify(mission_data)
var script = """
(function() {
try {
if (window.parent) {
console.log('Sending missionCompleted message to parent window');
window.parent.postMessage({
type: 'stemCity_missionCompleted',
data: %s,
source: 'godot-game',
timestamp: Date.now()
}, '*');
} else {
console.log('No parent window found for missionCompleted event');
}
} catch (e) {
console.error('Error sending missionCompleted via postMessage:', e);
}
})();
""" % mission_json
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
js.eval(script)
else:
print("JavaScriptBridge singleton not available")
static func onAllMissionsCompleted():
if not OS.has_feature("web"):
return
print("Sending all missions completed event")
var script = """
(function() {
try {
if (window.parent) {
console.log('Sending allMissionsCompleted message to parent window');
window.parent.postMessage({
type: 'stemCity_allMissionsCompleted',
source: 'godot-game',
timestamp: Date.now()
}, '*');
} else {
console.log('No parent window found for allMissionsCompleted event');
}
} catch (e) {
console.error('Error sending allMissionsCompleted via postMessage:', e);
}
})();
"""
# Use Engine.get_singleton for consistency with sound_manager.gd
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
js.eval(script)
else:
print("JavaScriptBridge singleton not available")
# Handle audio actions via JavaScript
static func handle_audio_action(action: String, sound_name: String = "", volume: float = -1.0):
if not OS.has_feature("web"):
return false
print("Handling audio action via JavaScript bridge: " + action)
var action_data = {
"action": action,
"sound": sound_name,
}
if volume >= 0.0:
action_data["volume"] = volume
var action_json = JSON.stringify(action_data)
var script = """
(function() {
try {
if (window.parent) {
console.log('Sending audio action to parent window:', %s);
window.parent.postMessage({
type: 'stemCity_audio',
data: %s,
source: 'godot-game',
timestamp: Date.now()
}, '*');
return true;
} else {
console.log('No parent window found for audio action');
return false;
}
} catch (e) {
console.error('Error sending audio action via postMessage:', e);
return false;
}
})();
""" % [action_json, action_json]
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
return js.eval(script)
else:
print("JavaScriptBridge singleton not available")
return false
# Helper method to ensure the sound manager's audio is initialized
# Call this method after user interaction to ensure audio works
static func ensure_audio_initialized():
if not OS.has_feature("web"):
return true # Audio always works on non-web platforms
print("Ensuring audio is initialized via JavaScript bridge")
# Setup audio message listener if it's not already set up
setup_audio_message_listener()
# Try to initialize audio through the sound manager if it exists
var sound_manager = _get_sound_manager()
if sound_manager and sound_manager.has_method("init_web_audio_from_js"):
print("Found SoundManager, calling init_web_audio_from_js")
sound_manager.init_web_audio_from_js()
# Follow up with direct JavaScript audio context unlocking for extra reliability
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
_run_audio_unlock_script(js)
return true
else:
# Fallback: directly try to unlock web audio using JavaScript
if Engine.has_singleton("JavaScriptBridge"):
var js = Engine.get_singleton("JavaScriptBridge")
var result = _run_audio_unlock_script(js)
return result
else:
print("JavaScriptBridge singleton not available for audio initialization")
return false
# Helper method to run the audio unlocking script with maximum compatibility
static func _run_audio_unlock_script(js_interface):
var script = """
(function() {
var result = false;
try {
// Simple approach to unlock audio
console.log('Running simplified audio unlock');
// Create audio context if needed
if (!window._godotAudioContext) {
window._godotAudioContext = new (window.AudioContext || window.webkitAudioContext)();
}
var audioCtx = window._godotAudioContext;
console.log('Audio context state:', audioCtx.state);
// Resume it (for Chrome/Safari)
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
// Play a short, quiet beep
var oscillator = audioCtx.createOscillator();
var gainNode = audioCtx.createGain();
gainNode.gain.value = 0.01; // Very quiet
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.start(0);
oscillator.stop(0.1);
// Add event listeners for future interactions
['click', 'touchstart', 'touchend'].forEach(function(event) {
document.addEventListener(event, function() {
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}, {once: false});
});
result = audioCtx.state === 'running';
} catch (e) {
console.error("JavaScript bridge: Audio unlock error:", e);
result = false;
}
return result;
})()
"""
var result = js_interface.eval(script)
print("JavaScript audio initialization result:", result)
return result
# Setup audio message listener from JavaScript
static func setup_audio_message_listener():
if not OS.has_feature("web"):
return false
print("Setting up audio message listener via JavaScript bridge")
if not Engine.has_singleton("JavaScriptBridge"):
print("JavaScriptBridge singleton not available")
return false
var js = Engine.get_singleton("JavaScriptBridge")
# Register the callback function
js.set_callback("godot_audio_callback", Callable(_get_sound_manager(), "process_js_audio_state"))
# Set up a listener for audio state messages
var script = """
(function() {
// Set up message listener if not already done
if (!window.godot_audio_listener_initialized) {
window.addEventListener('message', function(event) {
if (event.data && event.data.type === 'stemCity_audio_state') {
console.log('Godot received audio state:', event.data);
// Call our Godot callback with the state data
if (typeof godot_audio_callback === 'function') {
console.log('Sending audio state to Godot');
godot_audio_callback(event.data.data);
} else {
console.warn('godot_audio_callback is not available');
}
}
});
console.log('Audio message listener initialized');
window.godot_audio_listener_initialized = true;
// Request initial audio state from parent
window.parent.postMessage({
type: 'stemCity_audio',
data: { action: 'GET_STATE' },
source: 'godot-game',
timestamp: Date.now()
}, '*');
return true;
}
return false;
})();
"""
var result = js.eval(script)
print("Audio message listener setup result: ", result)
return result
# Helper to get the sound manager instance
static func _get_sound_manager():
var sound_manager = null
# Try to find using meta
if Engine.get_main_loop().has_meta("sound_manager"):
sound_manager = Engine.get_main_loop().get_meta("sound_manager")
else:
# Try to find in scene tree
var scene_tree = Engine.get_main_loop() as SceneTree
if scene_tree:
sound_manager = scene_tree.root.get_node_or_null("/root/SoundManager")
return sound_manager

@ -2,6 +2,8 @@ extends Node
class_name BuildingConstructionManager
signal construction_completed(position)
signal worker_construction_started
signal worker_construction_ended
const CONSTRUCTION_TIME = 10.0 # seconds to build a building
@ -23,34 +25,26 @@ func _ready():
if not worker_scene:
worker_scene = load("res://people/character-female-d.glb")
if not worker_scene:
print("WARNING: Could not load any character models for workers")
# Create an empty PackedScene as a last resort
worker_scene = PackedScene.new()
else:
print("Successfully loaded worker model: ", worker_scene.resource_path)
# Load the building plot scene (placeholder during construction)
building_plot_scene = load("res://models/building-small-a.glb")
if not building_plot_scene:
print("WARNING: Could not load building plot scene")
# Create an empty PackedScene as a last resort
building_plot_scene = PackedScene.new()
# Load the final building scene
final_building_scene = load("res://models/building-small-a.glb")
if not final_building_scene:
print("WARNING: Could not load final building scene")
# Create an empty PackedScene as a last resort
final_building_scene = PackedScene.new()
# Call this method to start construction at a position
func start_construction(position: Vector3, structure_index: int, rotation_basis = null):
if position in construction_sites:
print("Construction already in progress at ", position)
return
print("Starting new construction at ", position)
# Get the current selector rotation if available
var rotation_index = 0
if builder and builder.selector:
@ -60,7 +54,6 @@ func start_construction(position: Vector3, structure_index: int, rotation_basis
if builder.gridmap:
rotation_index = builder.gridmap.get_orthogonal_index_from_basis(rotation_basis)
print("Using rotation index: ", rotation_index)
# Create a construction site entry
construction_sites[position] = {
@ -127,7 +120,6 @@ func _spawn_worker_for_construction(target_position: Vector3):
var road_position = _find_nearest_road(target_position)
if road_position == Vector3.ZERO:
print("No roads found to spawn worker!")
return
# Create the worker
@ -169,13 +161,13 @@ func _create_worker(spawn_position: Vector3, target_position: Vector3):
# Load the worker script
var worker_script = load("res://scripts/mission/construction_worker.gd")
if not worker_script:
print("ERROR: Could not load construction worker script!")
return null
# Create the worker node with the script
var worker_node = Node3D.new()
worker_node.set_script(worker_script)
worker_node.name = "Worker_" + str(int(target_position.x)) + "_" + str(int(target_position.z))
# Give each worker a unique name to avoid conflicts with their sound effects
worker_node.name = "Worker_" + str(int(target_position.x)) + "_" + str(int(target_position.z)) + "_" + str(randi())
# Add the worker to the scene
if nav_region:
@ -183,6 +175,10 @@ func _create_worker(spawn_position: Vector3, target_position: Vector3):
else:
builder.add_child(worker_node)
# Connect signals for construction sounds
worker_node.construction_started.connect(_on_worker_construction_started)
worker_node.construction_ended.connect(_on_worker_construction_ended)
# Position the worker
worker_node.global_transform.origin = Vector3(spawn_position.x, 0.1, spawn_position.z)
@ -207,8 +203,27 @@ func _create_worker(spawn_position: Vector3, target_position: Vector3):
# Initialize the worker script
worker_node.initialize(model, anim_player, navigation_agent, target_position)
# Introduce a tiny random delay before the worker reaches the construction site
# This helps stagger the sound effects and make them more natural
if worker_node.has_method("set_movement_speed"):
# Randomize movement speed slightly to stagger arrivals
worker_node.set_movement_speed(randf_range(2.3, 2.7))
return worker_node
# Signal handlers for worker construction sounds
# Now needed ONLY for mission-triggering logic, not for sound
func _on_worker_construction_started():
# Forward the signal for mission managers/other systems that need it
# Workers now handle their own sounds independently
worker_construction_started.emit()
# Signal handler for construction ended signals
# Now needed ONLY for mission-triggering logic, not for sound
func _on_worker_construction_ended():
# Forward the signal for mission managers/other systems that need it
worker_construction_ended.emit()
# Complete construction at a position
func _complete_construction(position: Vector3):
if not position in construction_sites:
@ -236,6 +251,9 @@ func _complete_construction(position: Vector3):
# Place the final building
_place_final_building(position, site["structure_index"])
# Update mission objective now that construction is complete
_update_mission_objective_on_completion(site["structure_index"])
# Check if we should spawn a resident
var mission_manager = builder.get_node_or_null("/root/Main/MissionManager")
var should_spawn_resident = true
@ -244,7 +262,6 @@ func _complete_construction(position: Vector3):
# (let the mission manager handle that case to avoid double spawning)
if mission_manager and mission_manager.current_mission and mission_manager.current_mission.id == "1" and !mission_manager.character_spawned:
should_spawn_resident = false
print("Skip spawning resident for first building in mission 1")
# Spawn a resident from the new building (except for first building in mission 1)
if should_spawn_resident:
@ -256,54 +273,85 @@ func _complete_construction(position: Vector3):
# If not found, try to find it by group (we added the HUD to "hud" group)
if not hud:
print("Trying to find HUD by group...")
var hud_nodes = get_tree().get_nodes_in_group("hud")
if hud_nodes.size() > 0:
hud = hud_nodes[0]
print("Found HUD via group: " + hud.name)
# If not found, try other common paths
if not hud:
print("Trying alternative paths for HUD...")
var scene_root = get_tree().get_root()
for child in scene_root.get_children():
if child.name == "Main":
if child.has_node("CanvasLayer/HUD"):
hud = child.get_node("CanvasLayer/HUD")
print("Found HUD at Main/CanvasLayer/HUD")
break
# Last resort - try to find using builder's cash_display
if not hud and builder and builder.cash_display:
print("Trying to find HUD via cash_display...")
var parent = builder.cash_display.get_parent()
while parent and parent.get_parent():
if "HUD" in parent.name:
hud = parent
print("Found HUD via cash_display parent: " + parent.name)
break
parent = parent.get_parent()
print("HUD node found: " + str(hud != null))
if hud and site["structure_index"] >= 0 and site["structure_index"] < builder.structures.size():
var structure = builder.structures[site["structure_index"]]
print("Structure type: " + str(structure.type) + ", Is residential: " + str(structure.type == Structure.StructureType.RESIDENTIAL_BUILDING))
print("Population count: " + str(structure.population_count))
if structure.type == Structure.StructureType.RESIDENTIAL_BUILDING and structure.population_count > 0:
print("Adding population to HUD")
hud.total_population += structure.population_count
hud.update_hud()
hud.population_updated.emit(hud.total_population)
print("Added " + str(structure.population_count) + " population after construction completed")
else:
print("Building completed but has no population: " + str(structure.type))
# Emit completion signal
construction_completed.emit(position)
# Function to handle building demolition at a position
func handle_demolition(position: Vector3):
# Check if this position has a construction site entry
if position in construction_sites:
# Clean up any resources
var site = construction_sites[position]
# Clean up plot if it exists
if site["plot"] != null:
site["plot"].queue_free()
# Clean up worker if it exists
if site["worker"] != null:
site["worker"].queue_free()
# Remove the entry from the dictionary
construction_sites.erase(position)
# Function to update mission objective when construction is complete
func _update_mission_objective_on_completion(structure_index: int):
# Get reference to mission manager
var mission_manager = builder.get_node_or_null("/root/Main/MissionManager")
print("Construction completed at ", position)
if mission_manager and mission_manager.current_mission:
# Check if this is a residential building
if structure_index >= 0 and structure_index < builder.structures.size():
var structure = builder.structures[structure_index]
if structure.type == Structure.StructureType.RESIDENTIAL_BUILDING:
# For mission 3, we'll rely on the fix_mission.gd script to maintain an accurate count
# This prevents double counting, as we just need to make sure buildings are counted
# based on their actual presence in the scene
if mission_manager.current_mission.id == "3":
# Instead of directly updating, we'll wait for the fix script to apply the proper count
pass
# Special handling for mission 1
elif mission_manager.current_mission.id == "1":
# For mission 1, we need to make sure the objectives are updated
mission_manager.update_objective_progress(
mission_manager.current_mission.id,
MissionObjective.ObjectiveType.BUILD_RESIDENTIAL
)
# Trigger an immediate progress check
mission_manager.check_mission_progress(mission_manager.current_mission.id)
# Place the final building at the construction site
func _place_final_building(position: Vector3, structure_index: int):
@ -320,7 +368,6 @@ func _place_final_building(position: Vector3, structure_index: int):
var site = construction_sites[position]
if "rotation_basis" in site and site["rotation_basis"]:
building.basis = site["rotation_basis"]
print("Applied saved rotation to building")
building.scale = Vector3(3.0, 3.0, 3.0) # Scale to match other buildings
@ -365,15 +412,11 @@ func _make_model_transparent(model: Node3D, alpha: float):
# Spawn a resident from a newly constructed building
func _spawn_resident_from_building(position: Vector3):
print("Spawning resident from building at ", position)
# Make sure we have a valid nav_region reference
if not nav_region and builder and builder.nav_region:
nav_region = builder.nav_region
print("Updated nav_region reference from builder")
if not nav_region:
print("ERROR: No navigation region available for spawning resident!")
return
# Find a road to spawn near
@ -384,7 +427,6 @@ func _spawn_resident_from_building(position: Vector3):
# Use the pre-made character pathing scene (the same one that works in mission 1)
var character_scene = load("res://scenes/character_pathing.tscn")
if not character_scene:
print("ERROR: Could not load character_pathing.tscn scene!")
return
var resident = character_scene.instantiate()
@ -433,14 +475,11 @@ func _spawn_resident_from_building(position: Vector3):
target_position = position + Vector3(randf_range(-5, 5), 0, randf_range(-5, 5))
resident.set_movement_target(target_position)
print("Initial movement target set for resident")
if resident.has_method("_start_initial_movement"):
# Call deferred to ensure the navigation system is ready
resident.call_deferred("_start_initial_movement")
)
print("NavigationNPC resident spawned successfully as child of NavRegion3D")
# Find a random road to use as a target
func _find_random_road() -> Vector3:
@ -493,8 +532,6 @@ func initialize(resident_model: Node3D, anim_player: AnimationPlayer, navigation
# Start patrolling after a short delay
wait_timer = 2.0 # Wait 2 seconds before starting
print("Resident initialized at ", global_position)
func _physics_process(delta: float):
if is_moving:
@ -506,8 +543,6 @@ func _physics_process(delta: float):
# Play idle animation
if animation_player and animation_player.has_animation("idle"):
animation_player.play("idle")
print("Resident reached destination, waiting...")
else:
# Continue moving
move_along_path(delta)
@ -547,8 +582,6 @@ func set_movement_target(target: Vector3):
# Play walking animation
if animation_player and animation_player.has_animation("walk"):
animation_player.play("walk")
print("Resident moving to ", target)
func find_new_destination():
# Find a road to walk to
@ -563,7 +596,6 @@ func find_new_destination():
else:
# If no road found, try again later
wait_timer = 0.0
print("No road found for resident to walk to")
# Find a random road to walk to
func _find_random_road() -> Vector3:
@ -582,7 +614,6 @@ func _find_random_road() -> Vector3:
roads.append(road_pos)
else:
# If we can't find roads from our parent, try going back home
print("Resident couldn't find parent navigation region")
return home_position
# Pick a random road
@ -592,15 +623,12 @@ func _find_random_road() -> Vector3:
# Fallback to home position if no roads found
return home_position
"""
# Create the file with the script content
var file = FileAccess.open("res://scripts/mission/resident_character.gd", FileAccess.WRITE)
if file:
file.store_string(script_content)
file.close()
print("Created resident character script")
else:
print("Failed to create resident character script file")
# Helper to find all MeshInstance3D nodes
func _find_all_mesh_instances(node: Node, result: Array):
@ -608,4 +636,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)

@ -9,6 +9,17 @@ var construction_position: Vector3
var is_moving: bool = false
var is_construction_active: bool = false
var construction_finished: bool = false
var movement_speed: float = 2.5 # Default walking speed
# Sound effect properties
var construction_sound: AudioStreamPlayer # Use regular AudioStreamPlayer instead of 3D
var loop_timer: Timer
var my_sound_id: int = 0 # Unique ID for this worker's sound
var sound_initialized: bool = false
# Signals
signal construction_started
signal construction_ended
# Initialize the worker
func initialize(worker_model: Node3D, anim_player: AnimationPlayer, navigation_agent: NavigationAgent3D, target_pos: Vector3):
@ -18,8 +29,65 @@ func initialize(worker_model: Node3D, anim_player: AnimationPlayer, navigation_a
construction_position = target_pos
is_moving = true
# Generate a unique ID for this worker
my_sound_id = randi()
print("DEBUG: Worker created with ID: ", my_sound_id)
# Set up sound effects (call after being added to the scene tree)
call_deferred("setup_sound")
# Start moving after a frame
call_deferred("set_movement_target", target_pos)
# Set up sound for this worker
func setup_sound():
print("DEBUG: Setting up sound for worker " + str(my_sound_id))
# Create a regular AudioStreamPlayer (not 3D) for better reliability
construction_sound = AudioStreamPlayer.new()
construction_sound.name = "ConstructionSound_" + str(my_sound_id)
add_child(construction_sound)
print("DEBUG: Created AudioStreamPlayer for worker " + str(my_sound_id))
# Create a timer for looping the sound
loop_timer = Timer.new()
loop_timer.name = "SoundLoopTimer_" + str(my_sound_id)
add_child(loop_timer)
# Load the sound effect
var sound_resource = load("res://sounds/construction.wav")
if sound_resource:
print("DEBUG: Sound file loaded successfully")
# Directly use the sound resource
construction_sound.stream = sound_resource
# Configure sound settings
construction_sound.volume_db = -5.0 # Volume level
construction_sound.bus = "SFX" # Use the SFX bus
sound_initialized = true
print("DEBUG: Worker " + str(my_sound_id) + " sound setup completed")
else:
push_error("Could not load construction sound effect!")
print("ERROR: Could not load construction sound effect!")
# Configure timer with slight random variation
loop_timer.wait_time = randf_range(1.85, 2.05) # Random loop time
loop_timer.one_shot = false
loop_timer.autostart = false
# Connect the timer to the loop function
loop_timer.timeout.connect(loop_construction_sound)
print("DEBUG: Timer set up and connected for worker " + str(my_sound_id))
# Check if we need to connect to the audio_ready signal (for web)
if OS.has_feature("web"):
var sound_manager = get_node_or_null("/root/SoundManager")
if sound_manager and not sound_manager.audio_initialized:
# Connect to the audio_ready signal so we can start playing when audio is ready
sound_manager.audio_ready.connect(check_and_play_sound)
print("DEBUG: Worker " + str(my_sound_id) + " connected to audio_ready signal")
func _physics_process(delta: float):
if construction_finished:
@ -38,24 +106,15 @@ func _physics_process(delta: float):
move_along_path(delta)
elif is_construction_active:
# Make sure we keep the construction animation looping
_ensure_construction_animation_playing()
ensure_animation_playing()
# Make sure the construction animation keeps playing
func _ensure_construction_animation_playing():
# Every 0.5 seconds check if the animation is still playing (determined by a timer logic)
if not animation_player:
return
func ensure_animation_playing():
# Check if animation isn't playing or is on the wrong animation
if not animation_player.is_playing() or (
animation_player.current_animation != "pick-up" and
animation_player.current_animation != "pick_up" and
animation_player.current_animation != "pickup"):
# Try to restart the animation
if animation_player and not animation_player.is_playing():
# Try different animation names (pick-up, pick_up, pickup)
if animation_player.has_animation("pick-up"):
animation_player.play("pick-up")
print("Restarted pick-up animation")
elif animation_player.has_animation("pick_up"):
animation_player.play("pick_up")
elif animation_player.has_animation("pickup"):
@ -66,9 +125,8 @@ func move_along_path(delta: float):
var next_position = nav_agent.get_next_path_position()
var direction = (next_position - global_position).normalized()
# Set velocity directly
var speed = 2.5 # walking speed
global_position += direction * speed * delta
# Set velocity directly using the instance's movement_speed
global_position += direction * movement_speed * delta
# Make character face the direction of movement
if direction.length() > 0.01:
@ -82,6 +140,10 @@ func move_along_path(delta: float):
if animation_player and animation_player.has_animation("walk"):
if not animation_player.is_playing() or animation_player.current_animation != "walk":
animation_player.play("walk")
# Set movement speed - can be used to vary worker speeds slightly
func set_movement_speed(speed: float):
movement_speed = speed
func set_movement_target(target: Vector3):
if nav_agent:
@ -93,89 +155,120 @@ func set_movement_target(target: Vector3):
animation_player.play("walk")
func start_construction():
print("DEBUG: Worker " + str(my_sound_id) + " starting construction")
is_construction_active = true
# Print all available animations for debugging
if animation_player:
print("Available animations for worker: ")
var anim_list = animation_player.get_animation_list()
for anim_name in anim_list:
print("- " + anim_name)
# Check if we can play sound (for web platform)
var can_play_sound = true
if OS.has_feature("web"):
var sound_manager = get_node_or_null("/root/SoundManager")
if sound_manager:
can_play_sound = sound_manager.audio_initialized
# Start playing construction sound if possible
if can_play_sound and sound_initialized and construction_sound and construction_sound.stream:
play_sound()
elif OS.has_feature("web"):
print("DEBUG: Worker " + str(my_sound_id) + " waiting for audio initialization")
# Try to force the animation to loop using different approaches
# Emit signal for compatibility with existing system
construction_started.emit()
# Start construction animation
if animation_player:
# First try with the actual animation name "pick-up"
if animation_player.has_animation("pick-up"):
print("Found animation: pick-up")
# Try to get the animation resource
var animation = animation_player.get_animation("pick-up")
if animation:
# Set the loop mode if possible
animation.loop_mode = 1 # LOOP_LINEAR
print("Set loop mode for pick-up animation")
# Set speed scale to make it look more natural
animation_player.speed_scale = 1.0
# Play it on repeat
animation_player.play("pick-up")
# Force looping by continuously queuing the same animation
animation_player.queue("pick-up")
animation_player.queue("pick-up")
animation_player.queue("pick-up")
print("Started pick-up animation loop")
# Schedule to check on animation status in 1 second
var timer = get_tree().create_timer(1.0)
timer.timeout.connect(_check_animation_status)
# Try alternative spellings if needed
elif animation_player.has_animation("pick_up"):
animation_player.play("pick_up")
animation_player.queue("pick_up")
print("Started pick_up animation loop")
elif animation_player.has_animation("pickup"):
animation_player.play("pickup")
animation_player.queue("pickup")
print("Started pickup animation loop")
elif animation_player.has_animation("idle"):
animation_player.play("idle")
print("No pickup animation, using idle")
else:
print("No suitable animations found. Available animations: ", animation_player.get_animation_list())
# Helper to check animation status
func _check_animation_status():
if animation_player and is_construction_active:
print("Animation status check - current animation: ", animation_player.current_animation)
print("Is playing: ", animation_player.is_playing())
# Helper to play construction sound
func play_sound():
if is_construction_active and construction_sound and construction_sound.stream:
# Set random pitch for variety
construction_sound.pitch_scale = randf_range(0.9, 1.1)
# Force animation to continue if needed
if not animation_player.is_playing() or animation_player.current_animation != "pick-up":
if animation_player.has_animation("pick-up"):
animation_player.play("pick-up")
print("Restarted pick-up animation from timer callback")
# Play the sound
construction_sound.play()
print("DEBUG: Playing sound for worker " + str(my_sound_id))
# Start the loop timer
loop_timer.start()
# Called when audio becomes available in web builds
func check_and_play_sound():
print("DEBUG: Audio now ready for worker " + str(my_sound_id))
if is_construction_active and not construction_finished:
play_sound()
# Loop the construction sound independently
func loop_construction_sound():
if is_construction_active and construction_sound and construction_sound.stream:
# Stop the sound if it's still playing (to prevent overlap)
if construction_sound.playing:
construction_sound.stop()
# Slight random pitch variation on each loop
construction_sound.pitch_scale = randf_range(0.9, 1.1)
# Play the sound again
construction_sound.play()
print("DEBUG: Looping sound for worker " + str(my_sound_id))
else:
print("DEBUG: Cannot loop sound - either worker not active or sound not set up")
func finish_construction():
print("DEBUG: Worker " + str(my_sound_id) + " finishing construction")
is_construction_active = false
construction_finished = true
# Stop the construction sound
if construction_sound and construction_sound.playing:
construction_sound.stop()
print("DEBUG: Stopped sound for worker " + str(my_sound_id))
# Stop the sound loop timer
if loop_timer and loop_timer.is_inside_tree():
loop_timer.stop()
print("DEBUG: Stopped timer for worker " + str(my_sound_id))
# Emit signal for compatibility
construction_ended.emit()
# Find a road to walk back to
var road_position = _find_random_road()
var road_position = find_random_road()
if road_position != Vector3.ZERO:
set_movement_target(road_position)
is_moving = true
# Start removal timer
var timer = get_tree().create_timer(5.0)
timer.timeout.connect(_remove_worker)
var removal_timer = get_tree().create_timer(5.0)
removal_timer.timeout.connect(remove_worker)
func _remove_worker():
func remove_worker():
# Make sure sounds are stopped and cleaned up
if construction_sound:
if construction_sound.playing:
construction_sound.stop()
construction_sound.queue_free()
construction_sound = null
print("DEBUG: Cleaned up sound for worker " + str(my_sound_id))
if loop_timer:
if loop_timer.is_inside_tree() and loop_timer.time_left > 0:
loop_timer.stop()
loop_timer.queue_free()
loop_timer = null
print("DEBUG: Cleaned up timer for worker " + str(my_sound_id))
print("DEBUG: Worker " + str(my_sound_id) + " removed from game")
queue_free()
# Find a random road to walk back to
func _find_random_road() -> Vector3:
func find_random_road() -> Vector3:
var roads = []
var parent = get_parent()
@ -199,4 +292,4 @@ func _find_random_road() -> Vector3:
return roads[randi() % roads.size()]
# Fallback to current position if no roads found
return global_position
return global_position

@ -4,8 +4,10 @@ signal completed
signal panel_opened
signal panel_closed
# Only store user_input and submit_button variables for signal connections
var user_input
# Store variables for signal connections
var user_input # For single input (backward compatibility)
var user_inputs = [] # Array for multiple inputs
var input_labels = [] # Array for input labels
var submit_button
var mission: MissionData
@ -26,6 +28,10 @@ func _ready():
user_input = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/UserInputContainer/UserInput")
submit_button = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/SubmitButtonContainer/SubmitButton")
# Clear the user inputs array
user_inputs = []
input_labels = []
# Connect button signals if the button exists
if submit_button != null:
if not submit_button.is_connected("pressed", Callable(self, "_on_submit_button_pressed")):
@ -54,9 +60,13 @@ func show_learning_panel(mission_data: MissionData):
# Default answer based on mission type
correct_answer = "1" if not mission.power_math_content.is_empty() else "A"
# Set up user input placeholder
if user_input:
user_input.placeholder_text = mission.question_text if not mission.question_text.is_empty() else "Enter your answer"
# Set up user input fields based on mission data
if mission.num_of_user_inputs > 1:
_setup_multiple_user_inputs()
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"
# Hide the HUD when learning panel is shown
var hud = get_node_or_null("/root/Main/CanvasLayer/HUD")
@ -103,23 +113,122 @@ func _disable_background_interaction():
print("Background interaction disabled")
# Function to create multiple user input fields
func _setup_multiple_user_inputs():
# Clear any existing user inputs
user_inputs = []
input_labels = []
# Get the container where inputs should be added
var user_input_container = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/UserInputContainer")
if not user_input_container:
push_error("User input container not found")
return
# If there's an existing single input, hide it
if user_input and user_input.get_parent() == user_input_container:
user_input.visible = false
# Create a centering container for better alignment
var center_container = CenterContainer.new()
center_container.name = "InputCenterContainer"
center_container.size_flags_horizontal = Control.SIZE_FILL
user_input_container.add_child(center_container)
# Add margin around the grid
var margin_container = MarginContainer.new()
margin_container.name = "InputMarginContainer"
margin_container.add_theme_constant_override("margin_top", 10)
margin_container.add_theme_constant_override("margin_bottom", 10)
center_container.add_child(margin_container)
# Create a grid container for inputs
var grid = GridContainer.new()
grid.name = "MultiInputGrid"
grid.columns = 2 # Label and input in each row
grid.size_flags_horizontal = Control.SIZE_FILL
grid.add_theme_constant_override("h_separation", 15) # Add horizontal spacing between columns
grid.add_theme_constant_override("v_separation", 10) # Add vertical spacing between rows
margin_container.add_child(grid)
# Create each input field
for i in range(mission.num_of_user_inputs):
# Create label
var label = Label.new()
label.name = "InputLabel" + str(i)
label.text = mission.input_labels[i] if i < mission.input_labels.size() else "Input " + str(i+1) + ":"
label.size_flags_horizontal = Control.SIZE_EXPAND
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT # Right-align text
# Set larger font size
var font_size = 26
label.add_theme_font_size_override("font_size", font_size)
# Add right margin for better spacing
var style = StyleBoxEmpty.new()
style.content_margin_right = 10 # Add 10 pixels of right margin
label.add_theme_stylebox_override("normal", style)
grid.add_child(label)
input_labels.append(label)
# Create input field
var input_field = LineEdit.new()
input_field.name = "UserInput" + str(i)
input_field.size_flags_horizontal = Control.SIZE_EXPAND_FILL
input_field.placeholder_text = "Enter value"
input_field.custom_minimum_size.x = 150 # Increase minimum width
input_field.custom_minimum_size.y = 40 # Set height for the input field
# Style the input field
input_field.alignment = HORIZONTAL_ALIGNMENT_LEFT # Left-align text inside the field
input_field.add_theme_font_size_override("font_size", 26) # Match label font size
# Connect text submitted signal
input_field.text_submitted.connect(_on_user_input_text_submitted)
grid.add_child(input_field)
user_inputs.append(input_field)
# Add spacing after the grid
var spacer = Control.new()
spacer.name = "InputSpacer"
spacer.custom_minimum_size.y = 20
user_input_container.add_child(spacer)
# Reset the panel to a clean state
func _reset_panel():
# Reset answer state
is_answer_correct = false
# Clear text inputs
# Clear single input if it exists
if user_input:
user_input.text = ""
# Clear multiple inputs if they exist
for input in user_inputs:
if input:
input.text = ""
# Hide feedback label
var feedback_label = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/UserInputContainer/FeedbackLabel")
if feedback_label:
feedback_label.visible = false
# Clean up any TopMargin that might have been added
# Clean up any added UI elements
var user_input_container = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/UserInputContainer")
if user_input_container:
# Clean up the input center container and all its children
var input_center_container = user_input_container.get_node_or_null("InputCenterContainer")
if input_center_container:
input_center_container.queue_free()
# Clean up input spacer if it exists
var input_spacer = user_input_container.get_node_or_null("InputSpacer")
if input_spacer:
input_spacer.queue_free()
# Clean up any TopMargin that might have been added
var top_margin = user_input_container.get_node_or_null("TopMargin")
if top_margin:
top_margin.queue_free()
@ -127,6 +236,14 @@ func _reset_panel():
# Reset custom sizing
user_input_container.custom_minimum_size.y = 0
user_input_container.size_flags_vertical = Control.SIZE_FILL
# Show the default input field if it exists
if user_input and user_input.get_parent() == user_input_container:
user_input.visible = true
# Clear the user inputs arrays
user_inputs = []
input_labels = []
# Reset submit button
if submit_button:
@ -185,7 +302,7 @@ func _setup_mission_specific_content():
func _clear_existing_content():
# Find the main containers
var graph_center_container = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/GraphContainer/GraphCenterContainer")
var company_data_container = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/GraphContainer/CompanyDataContainer")
var question_container = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/GraphContainer/CompanyDataContainer")
# Clear power math content from the graph container
if graph_center_container:
@ -193,12 +310,12 @@ func _clear_existing_content():
if power_math_label:
power_math_label.queue_free()
# Reset company data container
if company_data_container:
company_data_container.visible = false
var company_data_label = company_data_container.get_node_or_null("CompanyDataLabel")
if company_data_label:
company_data_label.text = ""
# Reset the container that will hold our question text (previously used for company data)
if question_container:
question_container.visible = false
var text_label = question_container.get_node_or_null("CompanyDataLabel")
if text_label:
text_label.text = ""
# Set up construction company mission content
func _setup_construction_mission():
@ -251,85 +368,26 @@ func _setup_construction_mission():
graph_image.visible = false
print("Failed to load graph image")
# 2. Set company data
# 2. Set question text instead of company data
var company_data_container = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/GraphContainer/CompanyDataContainer")
if company_data_container:
company_data_container.visible = true
# Check if we need to convert the company data to a horizontal layout
# Get the label where we'll display the question text
var company_data_label = company_data_container.get_node_or_null("CompanyDataLabel")
if company_data_label:
if mission.company_data.is_empty():
company_data_label.text = "[center][color=#e06666][font_size=18]No company data available.[/font_size][/color][/center]"
else:
# Split the original data to create a horizontal layout
var data_text = mission.company_data
# Check if we need to reformat to save vertical space
if graph_image and graph_image.visible and graph_image.custom_minimum_size.y > 400:
print("Graph is large, reformatting company data to horizontal layout")
# Parse and reformat the company data to be more compact
# This assumes the data has a typical format with company names and bullet points
var lines = data_text.split("\n")
var company_a_name = ""
var company_a_data = []
var company_b_name = ""
var company_b_data = []
var current_company = -1 # 0 for A, 1 for B
# Parse the data by line
for line in lines:
line = line.strip_edges()
if line == "" or line.length() == 0:
continue
if "[color=#60c2a8]" in line or "Company A:" in line:
# Found Company A header
company_a_name = line
current_company = 0
elif "[color=#e06666]" in line or "Company B:" in line:
# Found Company B header
company_b_name = line
current_company = 1
elif line.begins_with("") or line.begins_with("-") or line.begins_with("*"):
# This is a data point
if current_company == 0:
company_a_data.append(line)
elif current_company == 1:
company_b_data.append(line)
elif "Enter A or B" in line or "If you need" in line:
# This is the question part - add to both
company_a_data.append(line)
company_b_data.append("")
elif "Hint:" in line:
# This is the hint - add to both
company_a_data.append(line)
company_b_data.append("")
# Create a horizontal layout with two columns
var formatted_text = "[center]\n"
# Add Company A
formatted_text += "[color=#ce5371][b]" + (company_a_name.replace("[b]", "").replace("[/b]", "").replace("[color=#60c2a8]", "").replace("[/color]", "")) + "[/b][/color]\n"
for point in company_a_data:
formatted_text += point + "\n"
formatted_text += "\n"
# Add Company B
formatted_text += "[color=#3182c0][b]" + (company_b_name.replace("[b]", "").replace("[/b]", "").replace("[color=#e06666]", "").replace("[/color]", "")) + "[/b][/color]\n"
for point in company_b_data:
formatted_text += point + "\n"
formatted_text += "[/center]"
# Set the formatted text
company_data_label.text = formatted_text
company_data_label.custom_minimum_size.y = 140 # Reduce height for horizontal layout
else:
# Use original data format
company_data_label.text = data_text
# Create a formatted question text
var formatted_text = "[center]\n"
# Add the question text in a centered, clear format
if not mission.question_text.is_empty():
formatted_text += "[color=#dddddd][font_size=26]" + mission.question_text + "[/font_size][/color]"
formatted_text += "\n[/center]"
# Set the formatted text
company_data_label.text = formatted_text
company_data_label.custom_minimum_size.y = 80 # Reduce height for just question text
# Set up power math mission content
@ -449,12 +507,25 @@ func _check_answer():
push_error("Mission is null in _check_answer")
return
# Make sure we have a user input field
if not user_input:
push_error("Cannot check answer: user_input is null")
var user_answer = ""
# Handle multiple inputs if present
if mission.num_of_user_inputs > 1 and not user_inputs.is_empty():
var answers = []
for input in user_inputs:
if input:
answers.append(input.text.strip_edges())
user_answer = ",".join(answers)
# Fall back to single input
elif user_input:
user_answer = user_input.text.strip_edges()
else:
push_error("Cannot check answer: no user input fields available")
return
var user_answer = user_input.text.strip_edges().to_upper() # Convert to uppercase for case-insensitive comparison
# Convert to uppercase for case-insensitive comparison when appropriate
if not "," in correct_answer: # Don't uppercase comma-separated values
user_answer = user_answer.to_upper()
# Get the feedback label
var feedback_label = get_node_or_null("PanelContainer/MarginContainer/ScrollContainer/VBoxContainer/MainContent/UserInputContainer/FeedbackLabel")
@ -506,4 +577,4 @@ func _on_complete_mission():
hide_learning_panel()
# Emit signal
completed.emit()
completed.emit()

@ -16,3 +16,5 @@ class_name MissionData
@export var incorrect_feedback: String = "" # Feedback text shown when answer is incorrect
@export var company_data: String = "" # Company data for mission 2
@export var power_math_content: String = "" # Power math content for mission 4
@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

@ -1,10 +1,16 @@
extends Node
class_name MissionManager
# Add the JavaScript bridge for HTML5 export
# This ensures JavaScript is available and degrades gracefully on other platforms
const JSBridge = preload("res://scripts/javascript_bridge.gd")
signal mission_started(mission: MissionData)
signal mission_completed(mission: MissionData)
signal objective_completed(objective: MissionObjective)
signal objective_progress(objective: MissionObjective, new_count: int)
signal game_started()
signal all_missions_completed()
@export var missions: Array[MissionData] = []
@export var mission_ui: Control
@ -15,6 +21,7 @@ var current_mission: MissionData
var active_missions: Dictionary = {} # mission_id: MissionData
var character_spawned: bool = false
var learning_companion_connected: bool = false
# Mission skip variables
var skip_key_presses: int = 0
@ -36,7 +43,6 @@ func _ready():
var old_panel = get_node_or_null("LearningPanel")
if old_panel:
old_panel.queue_free()
print("Removed old learning panel from mission manager")
# Load the learning panel scene fresh each time
var learning_panel_scene = load("res://scenes/learning_panel.tscn")
@ -44,7 +50,6 @@ func _ready():
learning_panel = learning_panel_scene.instantiate()
learning_panel.name = "LearningPanelFromScene"
add_child(learning_panel)
print("Loaded fresh learning panel from scene file")
else:
print("ERROR: Could not load learning_panel.tscn scene")
@ -54,7 +59,6 @@ func _ready():
fullscreen_learning_panel = fullscreen_panel_scene.instantiate()
fullscreen_learning_panel.name = "FullscreenLearningPanel"
add_child(fullscreen_learning_panel)
print("Loaded fullscreen learning panel from scene file")
else:
print("ERROR: Could not load fullscreen_learning_panel.tscn scene")
@ -77,6 +81,29 @@ func _ready():
else:
print("WARNING: Fullscreen learning panel not found!")
# For web builds, try to proactively initialize audio on load
if OS.has_feature("web"):
# Try to find sound manager and init audio
var sound_manager = get_node_or_null("/root/SoundManager")
if sound_manager and not sound_manager.audio_initialized:
# Connect to user input to detect interaction
get_viewport().gui_focus_changed.connect(_on_gui_focus_for_audio)
get_tree().get_root().connect("gui_input", _on_gui_input_for_audio)
# Set up communication with the learning companion
_setup_learning_companion_communication()
# Create a simple timer to force a learning companion connection in 3 seconds
# This is a fallback in case the normal connection doesn't work
var connection_timer = Timer.new()
connection_timer.wait_time = 3.0
connection_timer.one_shot = true
connection_timer.autostart = true
connection_timer.name = "ConnectionTimer"
add_child(connection_timer)
connection_timer.timeout.connect(_force_learning_companion_connection)
print("Created timer to force learning companion connection in 3 seconds")
# Load third mission if not already in the list
var third_mission = load("res://mission/third_mission.tres")
if third_mission:
@ -111,10 +138,106 @@ func _ready():
if mission.id == "3":
mission.next_mission_id = "4"
# Emit game_started signal before starting the first mission
game_started.emit()
# Start the first mission if available
if missions.size() > 0:
start_mission(missions[0])
# Web-specific audio initialization helper methods
func _on_gui_focus_for_audio(_control=null):
if OS.has_feature("web"):
_try_init_audio_on_interaction()
func _on_gui_input_for_audio(_event=null):
if OS.has_feature("web"):
_try_init_audio_on_interaction()
# Used to handle input for possible audio initialization in web
func _input(event):
# Process mission skipping
if event is InputEventKey and event.pressed and not event.is_echo():
if event.keycode == SKIP_KEY: # Use the configured skip key
var current_time = Time.get_ticks_msec() / 1000.0
# Reset counter if too much time has passed since last press
if current_time - last_skip_press_time > skip_key_timeout:
skip_key_presses = 0
# Update time and increment counter
last_skip_press_time = current_time
skip_key_presses += 1
# Show progress toward skipping
if mission_ui and mission_ui.has_method("show_temporary_message"):
if skip_key_presses < skip_key_required:
mission_ui.show_temporary_message("Mission skip: " + str(skip_key_presses) + "/" + str(skip_key_required) + " presses", 0.75, Color(1.0, 0.7, 0.2))
else:
mission_ui.show_temporary_message("Mission skipped!", 2.0, Color(0.2, 0.8, 0.2))
# Check if we've reached the required number of presses
if skip_key_presses >= skip_key_required:
skip_key_presses = 0
_skip_current_mission()
# For web builds, use any input to initialize audio
if OS.has_feature("web"):
if event is InputEventMouseButton or event is InputEventKey:
if event.pressed:
_try_init_audio_on_interaction()
# Helper to try initializing audio on user interaction
func _try_init_audio_on_interaction():
# Find the sound manager
var sound_manager = get_node_or_null("/root/SoundManager")
if sound_manager and not sound_manager.audio_initialized:
print("User interaction detected in MissionManager, attempting audio init")
sound_manager._initialize_web_audio()
# Also use JavaScript bridge to help with audio
if JSBridge.has_interface():
JSBridge.get_interface().ensure_audio_initialized()
# Try to kick-start music if game manager exists
var game_manager = get_node("/root/GameManager")
if game_manager and game_manager.has_method("_start_background_music"):
game_manager._start_background_music()
# Function to set up communication with the learning companion
func _setup_learning_companion_communication():
# First, check if JavaScript is available
if JSBridge.has_interface():
print("Setting up learning companion communication via postMessage")
# Try to initialize audio first since we now have user interaction
JSBridge.get_interface().ensure_audio_initialized()
# Connect directly using the simpler postMessage approach
JSBridge.get_interface().connectLearningCompanionViaPostMessage(
# Success callback
func():
learning_companion_connected = true
print("Successfully connected to learning companion")
# Connect signals to JavaScript callbacks
game_started.connect(_on_game_started_for_companion)
mission_started.connect(_on_mission_started_for_companion)
mission_completed.connect(_on_mission_completed_for_companion)
all_missions_completed.connect(_on_all_missions_completed_for_companion)
print("Learning companion event handlers connected")
# Try to initialize audio again to ensure it works
JSBridge.get_interface().ensure_audio_initialized(),
func():
learning_companion_connected = false
print("Failed to connect to learning companion via postMessage")
)
else:
print("JavaScript interface for learning companion not available")
func start_mission(mission: MissionData):
# Check that the mission data is valid
if mission == null:
@ -124,6 +247,25 @@ func start_mission(mission: MissionData):
current_mission = mission
active_missions[mission.id] = mission
# Send mission started event to the learning companion
_on_mission_started_for_companion(mission)
# Fix for mission 3 to ensure accurate count
if mission.id == "3":
# Reset the residential building count to 0 to avoid any double counting
for objective in mission.objectives:
if objective.type == MissionObjective.ObjectiveType.BUILD_RESIDENTIAL:
objective.current_count = 0
objective.completed = false
# Load and run the fix script to count actual buildings
var FixMissionScript = load("res://scripts/fix_mission.gd")
if FixMissionScript:
var fix_node = Node.new()
fix_node.set_script(FixMissionScript)
fix_node.name = "FixMissionHelper"
add_child(fix_node)
# Add decorative structures and curved roads
# Use more robust checking - fallback to ID for backward compatibility
var is_construction_or_expansion = (mission.id == "2" or mission.id == "3")
@ -146,21 +288,18 @@ func start_mission(mission: MissionData):
if not has_road_corner:
var road_corner = load("res://structures/road-corner.tres")
if road_corner:
print("Adding road-corner structure for mission 3")
builder.structures.append(road_corner)
# Add the grass-trees-tall if missing
if not has_grass_trees_tall:
var grass_trees_tall = load("res://structures/grass-trees-tall.tres")
if grass_trees_tall:
print("Adding grass-trees-tall structure for mission 3")
builder.structures.append(grass_trees_tall)
# Add the grass if missing
if not has_grass:
var grass = load("res://structures/grass.tres")
if grass:
print("Adding grass structure for mission 3")
builder.structures.append(grass)
# Special handling for power plant mission: add power plant
@ -179,7 +318,6 @@ func start_mission(mission: MissionData):
if not has_power_plant:
var power_plant = load("res://structures/power-plant.tres")
if power_plant:
print("Adding power plant structure for mission 4")
builder.structures.append(power_plant)
# Update the mesh library to include the new structures
@ -208,8 +346,6 @@ func start_mission(mission: MissionData):
mesh_library.set_item_mesh_transform(id, transform)
print("Updated mesh library for mission 3 with new structures")
# Make sure the builder's structure selector is updated
builder.update_structure()
@ -224,20 +360,16 @@ func start_mission(mission: MissionData):
# Show learning panel if mission has a learning objective
if has_learning_objective:
print("Found learning objective. Using learning panel for mission: ", mission.id)
# Determine which panel to use based on whether full_screen_path is provided
if not mission.full_screen_path.is_empty():
# Use fullscreen panel for fullscreen missions
if fullscreen_learning_panel:
print("Using fullscreen learning panel for mission with fullscreen path")
fullscreen_learning_panel.show_fullscreen_panel(mission)
else:
print("ERROR: Fullscreen learning panel not available but mission requires it")
else:
# Use regular panel for traditional missions
if learning_panel:
print("Using regular learning panel")
learning_panel.show_learning_panel(mission)
else:
print("ERROR: Regular learning panel not available")
@ -260,6 +392,9 @@ func complete_mission(mission_id: String):
# Remove from active missions
active_missions.erase(mission_id)
# Send mission completed event to the learning companion
_on_mission_completed_for_companion(mission)
# Start next mission if specified
if mission.next_mission_id != "":
for next_mission in missions:
@ -276,7 +411,8 @@ func complete_mission(mission_id: String):
# Shows a modal when all missions are complete
func _show_completion_modal():
print("Showing completion modal - all missions finished")
# Emit signal that all missions are completed
all_missions_completed.emit()
# Create the modal overlay
var modal = ColorRect.new()
@ -385,23 +521,82 @@ func _show_completion_modal():
var canvas_layer = get_node("/root/Main/CanvasLayer")
if canvas_layer:
canvas_layer.add_child(modal)
print("Added completion modal to CanvasLayer")
else:
add_child(modal)
print("Added completion modal to MissionManager")
# Connect button signal - use a specific method for clarity and debugging
continue_button.pressed.connect(_on_completion_continue_button_pressed.bind(modal))
# Handler for the mission completion continue button
func _on_completion_continue_button_pressed(modal_to_close):
print("Completion continue button was pressed - closing modal")
if is_instance_valid(modal_to_close) and modal_to_close is Node and modal_to_close.is_inside_tree():
modal_to_close.queue_free()
print("Modal should now be closed")
else:
push_error("Invalid modal reference or modal already removed")
# Event handler functions for learning companion communication
func _on_game_started_for_companion():
if not learning_companion_connected:
return
print("Sending game started event to learning companion")
if JSBridge.has_interface():
JSBridge.get_interface().onGameStarted()
func _on_mission_started_for_companion(mission: MissionData):
if not learning_companion_connected:
return
print("Sending mission started event to learning companion for mission: " + mission.id)
if JSBridge.has_interface():
# Convert mission data to a format that can be passed to JavaScript
var mission_data = {
"id": mission.id,
"title": mission.title,
"description": mission.description,
"intro_text": mission.intro_text,
}
JSBridge.get_interface().onMissionStarted(mission_data)
func _on_mission_completed_for_companion(mission: MissionData):
if not learning_companion_connected:
return
print("Sending mission completed event to learning companion for mission: " + mission.id)
if JSBridge.has_interface():
# Convert mission data to a format that can be passed to JavaScript
var mission_data = {
"id": mission.id,
"title": mission.title,
"description": mission.description,
}
JSBridge.get_interface().onMissionCompleted(mission_data)
func _on_all_missions_completed_for_companion():
if not learning_companion_connected:
return
print("Sending all missions completed event to learning companion")
if JSBridge.has_interface():
JSBridge.get_interface().onAllMissionsCompleted()
# Function to force learning companion connection after a delay
func _force_learning_companion_connection():
print("Forcing learning companion connection after delay")
# Set the connection flag to true even if the connection might have failed
learning_companion_connected = true
# Try to ensure audio is initialized as well
if JSBridge:
if JSBridge.has_interface():
JSBridge.get_interface().ensure_audio_initialized()
# Emit game started event
_on_game_started_for_companion()
print("Force-sent game started event to learning companion")
func check_mission_progress(mission_id: String) -> bool:
if not active_missions.has(mission_id):
return false
@ -409,10 +604,10 @@ func check_mission_progress(mission_id: String) -> bool:
var mission = active_missions[mission_id]
var all_completed = true
for objective in mission.objectives:
for i in range(mission.objectives.size()):
var objective = mission.objectives[i]
if not objective.completed:
all_completed = false
break
if all_completed:
complete_mission(mission_id)
@ -427,18 +622,23 @@ func update_objective_progress(mission_id: String, objective_type: int, amount:
var mission = active_missions[mission_id]
for objective in mission.objectives:
if objective.completed:
continue
if objective.type == objective_type:
# For specific structure objectives, check structure index
if objective.type == MissionObjective.ObjectiveType.BUILD_SPECIFIC_STRUCTURE:
if structure_index != objective.structure_index:
continue
# Update progress
# Track old count for comparison
var old_count = objective.current_count
objective.progress(amount)
# Update progress (positive or negative)
if amount > 0:
objective.progress(amount)
else:
# For negative amounts (like when demolishing buildings)
objective.regress(abs(amount))
# Ensure completed flag is updated properly
objective.completed = objective.is_completed()
# Emit signal if progress changed
if old_count != objective.current_count:
@ -458,6 +658,27 @@ func _on_structure_placed(structure_index: int, position: Vector3):
var structure = builder.structures[structure_index]
# Check if this is a residential building in mission 3 (which uses construction workers)
var skip_residential_count = false
if structure.type == Structure.StructureType.RESIDENTIAL_BUILDING:
if current_mission and current_mission.id == "3":
# Skip residential count updates - will be handled after construction completes
skip_residential_count = true
# Special handling for power plant (Mission 5)
if structure.model.resource_path.contains("power_plant"):
for mission_id in active_missions:
if active_missions[mission_id].id == "5":
var mission = active_missions[mission_id]
for objective in mission.objectives:
if not objective.completed:
objective.progress(objective.target_count)
objective_progress.emit(objective, objective.current_count)
objective_completed.emit(objective)
# Force mission completion check
check_mission_progress(mission_id)
for mission_id in active_missions:
# Update generic structure objective
update_objective_progress(mission_id, MissionObjective.ObjectiveType.BUILD_STRUCTURE)
@ -467,8 +688,10 @@ func _on_structure_placed(structure_index: int, position: Vector3):
Structure.StructureType.ROAD:
update_objective_progress(mission_id, MissionObjective.ObjectiveType.BUILD_ROAD)
Structure.StructureType.RESIDENTIAL_BUILDING:
update_objective_progress(mission_id, MissionObjective.ObjectiveType.BUILD_RESIDENTIAL)
# Only update residential count if we're not in mission 3 or 1
if not skip_residential_count:
update_objective_progress(mission_id, MissionObjective.ObjectiveType.BUILD_RESIDENTIAL)
# We don't spawn characters here anymore - this is handled by the builder.gd
# for both direct placement and worker construction
@ -489,6 +712,19 @@ func update_mission_ui():
if mission_ui and current_mission:
mission_ui.update_mission_display(current_mission)
# Reset the count of a specific objective type in the current mission
func reset_objective_count(objective_type: int, new_count: int = 0):
if not current_mission:
return
for objective in current_mission.objectives:
if objective.type == objective_type:
objective.current_count = new_count
objective.completed = objective.is_completed()
objective_progress.emit(objective, objective.current_count)
update_mission_ui()
func _on_learning_completed():
# Check current mission for progress
if current_mission != null and current_mission.id != "":
@ -504,44 +740,12 @@ func _on_learning_panel_closed():
if builder:
builder.disabled = false
# Method to process keyboard input for mission skipping
func _input(event):
if event is InputEventKey and event.pressed and not event.is_echo():
if event.keycode == SKIP_KEY: # Use the configured skip key
var current_time = Time.get_ticks_msec() / 1000.0
# Reset counter if too much time has passed since last press
if current_time - last_skip_press_time > skip_key_timeout:
skip_key_presses = 0
# Update time and increment counter
last_skip_press_time = current_time
skip_key_presses += 1
# Show progress toward skipping
if mission_ui and mission_ui.has_method("show_temporary_message"):
if skip_key_presses < skip_key_required:
mission_ui.show_temporary_message("Mission skip: " + str(skip_key_presses) + "/" + str(skip_key_required) + " presses", 0.75, Color(1.0, 0.7, 0.2))
else:
mission_ui.show_temporary_message("Mission skipped!", 2.0, Color(0.2, 0.8, 0.2))
# Print feedback to console
if skip_key_presses < skip_key_required:
print("Mission skip: " + str(skip_key_presses) + "/" + str(skip_key_required) + " key presses")
# Check if we've reached the required number of presses
if skip_key_presses >= skip_key_required:
skip_key_presses = 0
_skip_current_mission()
# Method to skip the current mission
func _skip_current_mission():
if not current_mission:
print("No mission to skip")
return
var mission_id = current_mission.id
print("Skipping mission " + mission_id)
# Auto-complete all objectives in the current mission
for objective in current_mission.objectives:
@ -560,13 +764,11 @@ func _skip_current_mission():
func _spawn_character_on_road(building_position: Vector3):
if !character_scene:
print("No character scene provided!")
return
# Check if a character has already been spawned
var existing_characters = get_tree().get_nodes_in_group("characters")
if existing_characters.size() > 0 or character_spawned:
print("Character already spawned, not spawning again.")
character_spawned = true
return
@ -578,12 +780,9 @@ func _spawn_character_on_road(building_position: Vector3):
var nearest_road_position = _find_nearest_road(building_position, gridmap)
if nearest_road_position != Vector3.ZERO:
print("Found nearest road at: ", nearest_road_position)
# Make sure there are no existing characters
for existing in get_tree().get_nodes_in_group("characters"):
existing.queue_free()
print("Cleaned up existing character")
# Use the pre-made character pathing scene
var character = load("res://scenes/character_pathing.tscn").instantiate()
@ -599,18 +798,15 @@ func _spawn_character_on_road(building_position: Vector3):
if nav_region:
# Add character as a child of the NavRegion3D
nav_region.add_child(character)
print("Added character as child of NavRegion3D")
else:
# Fallback to root if NavRegion3D doesn't exist
get_tree().root.add_child(character)
print("WARNING: NavRegion3D not found, adding character to root")
# Position character just slightly above the road's surface
character.global_transform.origin = Vector3(nearest_road_position.x, 0.1, nearest_road_position.z)
# Set an initial target to get the character moving
var target_position = _find_patrol_target(nearest_road_position, gridmap, 8.0)
print("Initial target set to: ", target_position)
# Allow the character to initialize
await get_tree().process_frame
@ -623,16 +819,10 @@ func _spawn_character_on_road(building_position: Vector3):
# Set target position
nav_agent.set_target_position(target_position)
print("Navigation target set")
# Make the character start moving
if character.has_method("set_movement_target"):
character.set_movement_target(target_position)
print("Movement target set for character")
else:
print("Character does not have set_movement_target method!")
else:
print("No road found near building!")
func _setup_character_for_navigation(character, initial_target):
# Access character's script to set up navigation
@ -643,35 +833,28 @@ func _setup_character_for_navigation(character, initial_target):
if model.has_node("AnimationPlayer"):
var anim_player = model.get_node("AnimationPlayer")
anim_player.play("walk")
print("Animation player started")
else:
print("No animation player found in character model!")
# Configure navigation agent parameters
if character.has_node("NavigationAgent3D"):
var nav_agent = character.get_node("NavigationAgent3D")
nav_agent.path_desired_distance = 0.5
nav_agent.target_desired_distance = 0.5
print("Navigation agent configured")
# Force movement to start immediately
if character.has_method("set_movement_target"):
# Wait a bit to make sure the navigation mesh is ready
await get_tree().create_timer(1.0).timeout
character.set_movement_target(initial_target)
print("Initial target set for character: ", initial_target)
# Ensure auto-patrol is enabled if the character supports it
if character.get("auto_patrol") != null:
character.auto_patrol = true
print("Auto patrol enabled for character")
# Set a starting movement target if not moving
await get_tree().create_timer(2.0).timeout
if character.get("is_moving") != null and !character.is_moving:
if character.has_method("pick_random_target"):
character.pick_random_target()
print("Forcing initial movement with pick_random_target()")
func _find_patrol_target(start_position: Vector3, gridmap: GridMap, max_distance: float) -> Vector3:
# With the navigation mesh system, we can simplify this to just return a point
@ -691,7 +874,6 @@ func _find_patrol_target(start_position: Vector3, gridmap: GridMap, max_distance
# Check if there's a road at this position in the NavRegion3D
if nav_region.has_node(road_name):
print("Found target road at ", check_pos)
return check_pos
# If all else fails, just return a point 5 units away in a random direction
@ -701,7 +883,6 @@ func _find_patrol_target(start_position: Vector3, gridmap: GridMap, max_distance
randf_range(-1.0, 1.0)
).normalized() * 5.0
print("No road found, using random target")
return start_position + random_direction
# Function to find a connected road piece to determine orientation
@ -737,9 +918,6 @@ func _find_nearest_road(position: Vector3, gridmap: GridMap) -> Vector3:
var min_distance = 100.0
var best_road_length = 0.0
print("Searching for road near position: ", position)
print("Available structures: ", builder.structures.size())
# First pass: find all roads based on their presence in the NavRegion3D
var road_positions = []
@ -771,8 +949,6 @@ func _find_nearest_road(position: Vector3, gridmap: GridMap) -> Vector3:
if nav_region and nav_region.has_node(road_name):
road_positions.append(check_pos)
print("Found ", road_positions.size(), " road positions")
# Second pass: evaluate roads based on distance and connected length
for road_pos in road_positions:
var distance = position.distance_to(road_pos)
@ -786,9 +962,6 @@ func _find_nearest_road(position: Vector3, gridmap: GridMap) -> Vector3:
nearest_road = road_pos
min_distance = distance
if nearest_road != Vector3.ZERO:
print("Selected road at: ", nearest_road)
return nearest_road
func _get_connected_road_length(road_position: Vector3, gridmap: GridMap) -> float:

@ -26,3 +26,8 @@ func is_completed() -> bool:
func progress(amount: int = 1) -> void:
current_count = min(current_count + amount, target_count)
completed = is_completed()
# Function to reduce the counter (for demolition)
func regress(amount: int = 1) -> void:
current_count = max(current_count - amount, 0)
completed = is_completed()

@ -9,6 +9,10 @@ class_name MissionUI
var temp_message_label: Label
var temp_message_timer: Timer
# Preload checkbox textures
var checkbox_checked = preload("res://sprites/checkbox.png")
var checkbox_unchecked = preload("res://sprites/checkbox_outline.png")
# Use a Label node directly instead of a scene
# This assumes the ObjectiveLabel node is set up correctly and can be duplicated
@ -31,27 +35,36 @@ func update_mission_display(mission: MissionData):
# Add new objectives
for objective in mission.objectives:
# Duplicate the ObjectiveLabel from the scene
var label = $"../ObjectiveLabel".duplicate()
objectives_container.add_child(label)
# Create a container for the objective
var container = HBoxContainer.new()
container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
objectives_container.add_child(container)
# Create the checkbox texture
var checkbox = TextureRect.new()
checkbox.texture = checkbox_checked if objective.completed else checkbox_unchecked
checkbox.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
checkbox.custom_minimum_size = Vector2(20, 20)
container.add_child(checkbox)
# Make font size larger and ensure text wrapping
# Create the text label
var label = Label.new()
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.add_theme_font_size_override("font_size", 16)
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
# Format the objective text
var status = "" if objective.completed else "" # Changed to checkbox style
var progress = ""
if objective.target_count > 1:
progress = " (%d/%d)" % [objective.current_count, objective.target_count]
label.text = "%s %s%s" % [status, objective.description, progress]
label.text = "%s%s" % [objective.description, progress]
# Style completed objectives differently
if objective.completed:
label.add_theme_color_override("font_color", Color(0, 0.8, 0.2, 1)) # Brighter green
container.add_child(label)
# Method to show a temporary message on the screen
func show_temporary_message(message: String, duration: float = 2.0, color: Color = Color.WHITE):

@ -0,0 +1,504 @@
extends Node
signal music_volume_changed(new_volume)
signal sfx_volume_changed(new_volume)
signal music_muted_changed(is_muted)
signal sfx_muted_changed(is_muted)
signal audio_ready # Signal emitted when audio is initialized (important for web)
# Sound bridges for web builds
var react_sound_bridge = null # Will be instantiated from a script
var audio_bridge = null # Will be instantiated from a script
# Volume ranges from 0.0 to 1.0
var music_volume: float = 0.8
var sfx_volume: float = 0.8
# Mute states
var music_muted: bool = false
var sfx_muted: bool = false
# Bus indices for easier reference
var music_bus_index: int
var sfx_bus_index: int
# Default bus names
const MUSIC_BUS_NAME = "Music"
const SFX_BUS_NAME = "SFX"
var audio_initialized: bool = false
# Sound files dictionary - mapping simplified names to file paths
const SOUND_FILES = {
"jazzNewOrleans": "res://sounds/jazz_new_orleans.mp3",
"lofiChillJazz": "res://sounds/lofi-chill-jazz-272869.mp3",
"buildingPlacing": "res://sounds/building_placing.wav",
"construction": "res://sounds/construction.wav",
"powerDrill": "res://sounds/power_drill.mp3"
}
# Current music track
var current_music: String = ""
# Currently playing audio streams (for direct Godot playback)
var music_player: AudioStreamPlayer = null
var sfx_players: Dictionary = {}
func _ready():
# Create music player for all platforms
music_player = AudioStreamPlayer.new()
add_child(music_player)
# For web builds, we'll use Audio Bridge
if OS.has_feature("web"):
# Set a flag to track initialization
audio_initialized = false
print("Web build detected, Audio bridges will be used")
# Connect to the input events to detect user interaction as fallback
get_viewport().connect("gui_focus_changed", _on_user_interaction)
# Try to use a custom Node for audio bridge functionality
# Instead of relying on class_name registration or preload
audio_bridge = Node.new()
audio_bridge.name = "AudioBridge"
add_child(audio_bridge)
# Set up the necessary properties
audio_bridge.set_script(load("res://scripts/audio_bridge.gd"))
# Connect to the signal after the script is loaded
if audio_bridge.has_signal("bridge_connected"):
audio_bridge.bridge_connected.connect(_on_audio_bridge_connected)
# We also create the ReactSoundBridge for backward compatibility
# But using the same approach as AudioBridge to avoid class_name dependency
react_sound_bridge = Node.new()
react_sound_bridge.name = "ReactSoundBridge"
add_child(react_sound_bridge)
# Set up the necessary properties
react_sound_bridge.set_script(load("res://scripts/react_sound_bridge.gd"))
# Connect to the signal after the script is loaded
if react_sound_bridge.has_signal("audio_ready"):
react_sound_bridge.audio_ready.connect(_on_react_audio_ready)
# Signal that we're using web audio (don't create audio buses)
await get_tree().process_frame
audio_initialized = true
audio_ready.emit()
# Store a reference to this object in the main loop for JavaScript callbacks
Engine.get_main_loop().set_meta("sound_manager", self)
else:
# For non-web platforms, set up standard Godot audio
# Set up the audio buses
_setup_audio_buses()
# Set the music player bus
music_player.bus = MUSIC_BUS_NAME
# For non-web platforms, we can initialize immediately
audio_initialized = true
# Emit the audio_ready signal
audio_ready.emit()
# Setup audio buses (doesn't start audio playback)
func _setup_audio_buses():
# Initialize audio bus indices
music_bus_index = AudioServer.get_bus_index(MUSIC_BUS_NAME)
sfx_bus_index = AudioServer.get_bus_index(SFX_BUS_NAME)
# If the buses don't exist yet, create them
if music_bus_index == -1:
# Create music bus
music_bus_index = AudioServer.bus_count
AudioServer.add_bus()
AudioServer.set_bus_name(music_bus_index, MUSIC_BUS_NAME)
AudioServer.set_bus_send(music_bus_index, "Master")
if sfx_bus_index == -1:
# Create SFX bus
sfx_bus_index = AudioServer.bus_count
AudioServer.add_bus()
AudioServer.set_bus_name(sfx_bus_index, SFX_BUS_NAME)
AudioServer.set_bus_send(sfx_bus_index, "Master")
# Verify buses were created correctly
music_bus_index = AudioServer.get_bus_index(MUSIC_BUS_NAME)
sfx_bus_index = AudioServer.get_bus_index(SFX_BUS_NAME)
# Apply initial settings
_apply_music_volume()
_apply_sfx_volume()
# Make sure buses aren't muted by default
if music_bus_index != -1:
AudioServer.set_bus_mute(music_bus_index, false)
if sfx_bus_index != -1:
AudioServer.set_bus_mute(sfx_bus_index, false)
# Process sound state received from JavaScript
func process_js_audio_state(state: Dictionary):
# Update local state based on received data
if state.has("musicVolume"):
music_volume = state.musicVolume
if state.has("sfxVolume"):
sfx_volume = state.sfxVolume
if state.has("musicMuted"):
music_muted = state.musicMuted
if state.has("sfxMuted"):
sfx_muted = state.sfxMuted
if state.has("currentMusic"):
current_music = state.currentMusic
# Emit signals about changes
music_volume_changed.emit(music_volume)
sfx_volume_changed.emit(sfx_volume)
music_muted_changed.emit(music_muted)
sfx_muted_changed.emit(sfx_muted)
# Called when ReactSoundBridge reports it's ready
func _on_react_audio_ready():
print("ReactSoundBridge reports ready")
audio_initialized = true
# Update local state from React
if react_sound_bridge != null:
if react_sound_bridge.get("music_volume") != null:
music_volume = react_sound_bridge.music_volume
if react_sound_bridge.get("sfx_volume") != null:
sfx_volume = react_sound_bridge.sfx_volume
if react_sound_bridge.get("music_muted") != null:
music_muted = react_sound_bridge.music_muted
if react_sound_bridge.get("sfx_muted") != null:
sfx_muted = react_sound_bridge.sfx_muted
if react_sound_bridge.get("current_music") != null:
current_music = react_sound_bridge.current_music
# Emit the audio ready signal
audio_ready.emit()
# Called when AudioBridge connects to the platform-one sound manager
func _on_audio_bridge_connected(is_connected: bool):
print("AudioBridge connected: ", is_connected)
if is_connected:
audio_initialized = true
# Request the sound state from the platform-one sound manager
if audio_bridge.has_method("get_sound_state"):
audio_bridge.get_sound_state()
# Emit the audio ready signal
audio_ready.emit()
# Called when any user interaction happens in web builds
func _on_user_interaction(_arg=null):
if OS.has_feature("web") and not audio_initialized:
_initialize_web_audio()
# Process input events directly
func _input(event):
if OS.has_feature("web") and not audio_initialized:
if event is InputEventMouseButton or event is InputEventKey:
if event.pressed:
_initialize_web_audio()
# If this method is called from JavaScript, it will help the game to
# initialize audio properly in web builds
func init_web_audio_from_js():
if OS.has_feature("web") and not audio_initialized:
_initialize_web_audio()
# Initialize audio for web builds
func _initialize_web_audio():
if audio_initialized:
return
# For web builds, we notify JavaScript to initialize audio
if OS.has_feature("web"):
JSBridge.JavaScriptGlobal.handle_audio_action("INITIALIZE_AUDIO")
# We don't need to create any dummy players, as JavaScript will handle the audio
audio_initialized = true
audio_ready.emit()
return
# For non-web platforms, initialize Godot audio (this shouldn't get called)
if not OS.has_feature("web"):
# Set the flag to prevent multiple initializations
audio_initialized = true
audio_ready.emit()
# Play background music
func play_music(sound_name: String, loop: bool = true):
if not audio_initialized:
return
# Store the current music name
current_music = sound_name
# For web builds, try multiple bridge options
if OS.has_feature("web"):
# Try AudioBridge first (platform-one integration)
if audio_bridge != null and audio_bridge.get("is_connected") == true:
print("Using AudioBridge to play music: ", sound_name)
if audio_bridge.has_method("play_music") and audio_bridge.play_music(sound_name):
return
# Fall back to JavaScript Bridge
print("Using JavaScriptBridge to play music: ", sound_name)
JSBridge.JavaScriptGlobal.handle_audio_action("PLAY_MUSIC", sound_name)
return
# For native builds, use Godot audio
if not SOUND_FILES.has(sound_name):
return
# Get the file path
var file_path = SOUND_FILES[sound_name]
# Load the audio stream
var stream = load(file_path)
if stream == null:
return
# Stop current music if playing
if music_player.playing:
music_player.stop()
# Set up and play the music
music_player.stream = stream
if music_muted:
music_player.volume_db = linear_to_db(0)
else:
music_player.volume_db = linear_to_db(music_volume)
music_player.bus = MUSIC_BUS_NAME
# Set looping if supported by the stream
if stream is AudioStreamMP3 or stream is AudioStreamOggVorbis:
stream.loop = loop
music_player.play()
# Play a sound effect
func play_sfx(sound_name: String):
if not audio_initialized:
return
# For web builds, try multiple bridge options
if OS.has_feature("web"):
# Try AudioBridge first (platform-one integration)
if audio_bridge != null and audio_bridge.get("is_connected") == true:
print("Using AudioBridge to play sfx: ", sound_name)
if audio_bridge.has_method("play_sfx") and audio_bridge.play_sfx(sound_name):
return
# Fall back to JavaScript Bridge
print("Using JavaScriptBridge to play sfx: ", sound_name)
JSBridge.JavaScriptGlobal.handle_audio_action("PLAY_SFX", sound_name)
return
# For native builds, use Godot audio
if not SOUND_FILES.has(sound_name):
return
# Get the file path
var file_path = SOUND_FILES[sound_name]
# Load the audio stream
var stream = load(file_path)
if stream == null:
return
# Create or reuse a player for this sound
var player: AudioStreamPlayer
if not sfx_players.has(sound_name):
player = AudioStreamPlayer.new()
add_child(player)
sfx_players[sound_name] = player
else:
player = sfx_players[sound_name]
if player.playing:
player.stop()
# Set up and play the sound
player.stream = stream
if sfx_muted:
player.volume_db = linear_to_db(0)
else:
player.volume_db = linear_to_db(sfx_volume)
player.bus = SFX_BUS_NAME
player.play()
# Stop background music
func stop_music():
if not audio_initialized:
return
# For web builds, try multiple bridge options
if OS.has_feature("web"):
# Try AudioBridge first (platform-one integration)
if audio_bridge != null and audio_bridge.get("is_connected") == true:
print("Using AudioBridge to stop music")
if audio_bridge.has_method("stop_music") and audio_bridge.stop_music():
current_music = ""
return
# Fall back to JavaScript Bridge
print("Using JavaScriptBridge to stop music")
JSBridge.JavaScriptGlobal.handle_audio_action("STOP_MUSIC")
current_music = ""
return
# For native builds, use Godot audio
if music_player and music_player.playing:
music_player.stop()
current_music = ""
# Set music volume (0.0 to 1.0)
func set_music_volume(volume: float):
music_volume = clampf(volume, 0.0, 1.0)
# For web builds, try multiple bridge options
if OS.has_feature("web"):
# Try AudioBridge first (platform-one integration)
if audio_bridge != null and audio_bridge.get("is_connected") == true:
print("Using AudioBridge to set music volume: ", music_volume)
if audio_bridge.has_method("set_music_volume"):
audio_bridge.set_music_volume(music_volume)
else:
# Fall back to JavaScript Bridge
print("Using JavaScriptBridge to set music volume: ", music_volume)
JSBridge.JavaScriptGlobal.handle_audio_action("SET_MUSIC_VOLUME", "", music_volume)
else:
# Apply to local Godot audio system
_apply_music_volume()
# Emit signal
music_volume_changed.emit(music_volume)
# Set SFX volume (0.0 to 1.0)
func set_sfx_volume(volume: float):
sfx_volume = clampf(volume, 0.0, 1.0)
# For web builds, try multiple bridge options
if OS.has_feature("web"):
# Try AudioBridge first (platform-one integration)
if audio_bridge != null and audio_bridge.get("is_connected") == true:
print("Using AudioBridge to set sfx volume: ", sfx_volume)
if audio_bridge.has_method("set_sfx_volume"):
audio_bridge.set_sfx_volume(sfx_volume)
else:
# Fall back to JavaScript Bridge
print("Using JavaScriptBridge to set sfx volume: ", sfx_volume)
JSBridge.JavaScriptGlobal.handle_audio_action("SET_SFX_VOLUME", "", sfx_volume)
else:
# Apply to local Godot audio system
_apply_sfx_volume()
# Emit signal
sfx_volume_changed.emit(sfx_volume)
# Toggle music mute state
func toggle_music_mute():
music_muted = !music_muted
# For web builds, try multiple bridge options
if OS.has_feature("web"):
# Try AudioBridge first (platform-one integration)
if audio_bridge != null and audio_bridge.get("is_connected") == true:
print("Using AudioBridge to toggle music mute: ", music_muted)
if audio_bridge.has_method("toggle_music_mute"):
audio_bridge.toggle_music_mute()
else:
# Fall back to JavaScript Bridge
print("Using JavaScriptBridge to toggle music mute: ", music_muted)
JSBridge.JavaScriptGlobal.handle_audio_action("TOGGLE_MUSIC_MUTE")
else:
# Apply to local Godot audio system
_apply_music_volume()
# Emit signal
music_muted_changed.emit(music_muted)
# Toggle SFX mute state
func toggle_sfx_mute():
sfx_muted = !sfx_muted
# For web builds, try multiple bridge options
if OS.has_feature("web"):
# Try AudioBridge first (platform-one integration)
if audio_bridge != null and audio_bridge.get("is_connected") == true:
print("Using AudioBridge to toggle sfx mute: ", sfx_muted)
if audio_bridge.has_method("toggle_sfx_mute"):
audio_bridge.toggle_sfx_mute()
else:
# Fall back to JavaScript Bridge
print("Using JavaScriptBridge to toggle sfx mute: ", sfx_muted)
JSBridge.JavaScriptGlobal.handle_audio_action("TOGGLE_SFX_MUTE")
else:
# Apply to local Godot audio system
_apply_sfx_volume()
# Emit signal
sfx_muted_changed.emit(sfx_muted)
# Apply music volume settings
func _apply_music_volume():
# Skip for web builds - JavaScript Bridge handles volume
if OS.has_feature("web"):
return
# For non-web builds, use the audio buses
if music_bus_index != -1:
if music_muted:
AudioServer.set_bus_mute(music_bus_index, true)
else:
AudioServer.set_bus_mute(music_bus_index, false)
# Convert from linear to decibels (approximately -80dB to 0dB)
var db_value = linear_to_db(music_volume)
AudioServer.set_bus_volume_db(music_bus_index, db_value)
# Update music player volume if it exists
if music_player != null:
if music_muted:
music_player.volume_db = linear_to_db(0)
else:
music_player.volume_db = linear_to_db(music_volume)
# Apply SFX volume settings
func _apply_sfx_volume():
# Skip for web builds - JavaScript Bridge handles volume
if OS.has_feature("web"):
return
# For non-web builds, use the audio buses
if sfx_bus_index != -1:
if sfx_muted:
AudioServer.set_bus_mute(sfx_bus_index, true)
else:
AudioServer.set_bus_mute(sfx_bus_index, false)
# Convert from linear to decibels
var db_value = linear_to_db(sfx_volume)
AudioServer.set_bus_volume_db(sfx_bus_index, db_value)
# Update all sfx player volumes
for player in sfx_players.values():
if player != null:
if sfx_muted:
player.volume_db = linear_to_db(0)
else:
player.volume_db = linear_to_db(sfx_volume)
# Helper function to convert linear volume to decibels with a more usable range
func linear_to_db(linear_value: float) -> float:
if linear_value <= 0:
return -80.0 # Very low but not -INF
return 20.0 * log(linear_value) / log(10.0)

@ -0,0 +1,109 @@
extends PanelContainer
signal closed
# References to UI controls
@onready var music_slider = $MarginContainer/VBoxContainer/MusicSection/MusicControls/MusicSlider
@onready var sfx_slider = $MarginContainer/VBoxContainer/SFXSection/SFXControls/SFXSlider
@onready var music_mute_button = $MarginContainer/VBoxContainer/MusicSection/MusicControls/MusicMuteButton
@onready var sfx_mute_button = $MarginContainer/VBoxContainer/SFXSection/SFXControls/SFXMuteButton
@onready var music_value_label = $MarginContainer/VBoxContainer/MusicSection/MusicControls/MusicValueLabel
@onready var sfx_value_label = $MarginContainer/VBoxContainer/SFXSection/SFXControls/SFXValueLabel
func _ready():
# Hide the panel initially
visible = false
# Make sure this control blocks mouse input from passing through
mouse_filter = Control.MOUSE_FILTER_STOP
# Connect to SoundManager signals
var sound_manager = get_node_or_null("/root/SoundManager")
if sound_manager:
sound_manager.music_volume_changed.connect(_on_music_volume_changed)
sound_manager.sfx_volume_changed.connect(_on_sfx_volume_changed)
sound_manager.music_muted_changed.connect(_on_music_muted_changed)
sound_manager.sfx_muted_changed.connect(_on_sfx_muted_changed)
# Initialize UI with current values
music_slider.value = sound_manager.music_volume
sfx_slider.value = sound_manager.sfx_volume
music_mute_button.button_pressed = sound_manager.music_muted
sfx_mute_button.button_pressed = sound_manager.sfx_muted
# Update labels
_update_music_label(sound_manager.music_volume)
_update_sfx_label(sound_manager.sfx_volume)
else:
print("ERROR: SoundManager not found!")
func show_panel():
visible = true
# Pause the game when the sound panel is open to prevent
# accidental building placement while adjusting sound
get_tree().paused = true
func hide_panel():
visible = false
# Resume the game when the panel is closed
get_tree().paused = false
func _on_close_button_pressed():
hide_panel()
# Emit signal that panel was closed
closed.emit()
# Consume the event to prevent click-through
get_viewport().set_input_as_handled()
# Handle slider changes
func _on_music_slider_value_changed(value):
var sound_manager = get_node_or_null("/root/SoundManager")
if sound_manager:
sound_manager.set_music_volume(value)
_update_music_label(value)
func _on_sfx_slider_value_changed(value):
var sound_manager = get_node_or_null("/root/SoundManager")
if sound_manager:
sound_manager.set_sfx_volume(value)
_update_sfx_label(value)
# Handle mute button toggling
func _on_music_mute_button_toggled(toggled_on):
var sound_manager = get_node_or_null("/root/SoundManager")
if sound_manager:
sound_manager.music_muted = toggled_on
sound_manager._apply_music_volume()
sound_manager.music_muted_changed.emit(toggled_on)
func _on_sfx_mute_button_toggled(toggled_on):
var sound_manager = get_node_or_null("/root/SoundManager")
if sound_manager:
sound_manager.sfx_muted = toggled_on
sound_manager._apply_sfx_volume()
sound_manager.sfx_muted_changed.emit(toggled_on)
# Update UI from SoundManager events
func _on_music_volume_changed(new_volume):
music_slider.value = new_volume
_update_music_label(new_volume)
func _on_sfx_volume_changed(new_volume):
sfx_slider.value = new_volume
_update_sfx_label(new_volume)
func _on_music_muted_changed(is_muted):
music_mute_button.button_pressed = is_muted
func _on_sfx_muted_changed(is_muted):
sfx_mute_button.button_pressed = is_muted
# Helper functions to update percentage labels
func _update_music_label(value):
var percentage = int(value * 100)
music_value_label.text = str(percentage) + "%"
func _update_sfx_label(value):
var percentage = int(value * 100)
sfx_value_label.text = str(percentage) + "%"

@ -26,3 +26,6 @@ enum StructureType {
@export_subgroup("Electricity")
@export var kW_usage:float = 0.0 # How much electricity this structure uses
@export var kW_production:float = 0.0 # How much electricity this structure produces
@export_subgroup("Visual")
@export var selector_scale:float = 1.0 # Scale factor for the selector when this structure is selected

Binary file not shown.

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dsyidkx5rlck3"
path="res://.godot/imported/building_placing.wav-6750ed644c8875413c24123dee0b5984.sample"
[deps]
source_file="res://sounds/building_placing.wav"
dest_files=["res://.godot/imported/building_placing.wav-6750ed644c8875413c24123dee0b5984.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=0

Binary file not shown.

@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://b1nj2ohshhvfa"
path="res://.godot/imported/construction.wav-3edc485a6e0d52ecced83e58f1834814.sample"
[deps]
source_file="res://sounds/construction.wav"
dest_files=["res://.godot/imported/construction.wav-3edc485a6e0d52ecced83e58f1834814.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=0

Binary file not shown.

@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://cco2pa7jouvxe"
path="res://.godot/imported/jazz_new_orleans.mp3-9c30cd48932c10000290cd0aa89376ac.mp3str"
[deps]
source_file="res://sounds/jazz_new_orleans.mp3"
dest_files=["res://.godot/imported/jazz_new_orleans.mp3-9c30cd48932c10000290cd0aa89376ac.mp3str"]
[params]
loop=true
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://dc6t4dmow110x"
path="res://.godot/imported/lofi-chill-jazz-272869.mp3-b6fa96cb13fe77ea771a95fd6c8b3e74.mp3str"
[deps]
source_file="res://sounds/lofi-chill-jazz-272869.mp3"
dest_files=["res://.godot/imported/lofi-chill-jazz-272869.mp3-b6fa96cb13fe77ea771a95fd6c8b3e74.mp3str"]
[params]
loop=false
loop_offset=0.0
bpm=0.0
beat_count=0
bar_beats=4

Binary file not shown.

@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://c7ji2wofye2fo"
path="res://.godot/imported/power_drill.mp3-b237a687215d757fe9f33b1fb09636fd.mp3str"
[deps]
source_file="res://sounds/power_drill.mp3"
dest_files=["res://.godot/imported/power_drill.mp3-b237a687215d757fe9f33b1fb09636fd.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://djxd33vvtr58p"
path="res://.godot/imported/muted.png-c594e352e1c503ffa3799ecca5bea12f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://sprites/muted.png"
dest_files=["res://.godot/imported/muted.png-c594e352e1c503ffa3799ecca5bea12f.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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://hx3maksi7ma"
path="res://.godot/imported/unmuted.png-e722f5f143c0d36eb69ba2a737c9a364.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://sprites/unmuted.png"
dest_files=["res://.godot/imported/unmuted.png-e722f5f143c0d36eb69ba2a737c9a364.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

@ -7,3 +7,5 @@
script = ExtResource("2_jrinw")
model = ExtResource("1_gyclk")
price = 70
type = 1
selector_scale = 2.8

@ -11,3 +11,4 @@ price = 50
population_count = 1
kW_usage = 1.0
kW_production = 0.0
selector_scale = 2.8

@ -11,3 +11,4 @@ price = 60
population_count = 1
kW_usage = 1.0
kW_production = 0.0
selector_scale = 2.8

@ -11,3 +11,4 @@ price = 70
population_count = 1
kW_usage = 1.0
kW_production = 0.0
selector_scale = 2.8

@ -11,3 +11,4 @@ price = 70
population_count = 1
kW_usage = 1.0
kW_production = 0.0
selector_scale = 2.8

@ -8,3 +8,4 @@ script = ExtResource("2_b2sah")
model = ExtResource("1_nbdd1")
type = 6
price = 25
selector_scale = 2.8

@ -7,3 +7,5 @@
script = ExtResource("2_pnpij")
model = ExtResource("1_lcgc1")
price = 25
type = 6
selector_scale = 2.8

@ -8,3 +8,5 @@ script = ExtResource("2_1i4lf")
model = ExtResource("1_2n0ef")
type = 6
price = 10
selector_scale = 2.8

@ -7,3 +7,5 @@
script = ExtResource("2_wqyte")
model = ExtResource("1_r2n53")
price = 10
type = 6
selector_scale = 2.8

@ -7,3 +7,5 @@
script = ExtResource("2_i48go")
model = ExtResource("1_gudnj")
price = 10
type = 6
selector_scale = 2.8

@ -10,4 +10,5 @@ type = 7
price = 100
population_count = 0
kW_usage = 0.0
kW_production = 40.0
kW_production = 40.0
selector_scale = 13

@ -8,3 +8,4 @@ script = ExtResource("3_oloyn")
model = ExtResource("1_r8n8k")
type = 0
price = 25
selector_scale = 2.8

@ -7,3 +7,5 @@
script = ExtResource("2_3eq5k")
model = ExtResource("1_pnjl2")
price = 25
type = 0
selector_scale = 2.8

@ -7,3 +7,5 @@
script = ExtResource("2_xa71m")
model = ExtResource("1_ku0rj")
price = 25
type = 0
selector_scale = 2.8

@ -7,3 +7,5 @@
script = ExtResource("2_470aq")
model = ExtResource("1_i07jw")
price = 25
type = 0
selector_scale = 2.8

@ -8,3 +8,7 @@ script = ExtResource("1_5fmmh")
model = ExtResource("1_ump1f")
type = 0
price = 25
population_count = 0
kW_usage = 0.0
kW_production = 0.0
selector_scale = 2.8