Update Mission workflow and add event bus

pull/18/head
Wade 2025-04-17 20:19:01 +07:00
parent a36338ef93
commit 234337a0e4
18 changed files with 145 additions and 292 deletions

@ -0,0 +1,8 @@
# Mission Configs
enum ObjectiveType {
BUILD_STRUCTURE,
BUILD_RESIDENTIAL,
REACH_CASH_AMOUNT,
REACH_POPULATION,
LEARNING,
}

@ -0,0 +1,10 @@
extends Node
signal population_update(new_population:int)
var current_scene = null
func _ready():
var root = get_tree().root
# Using a negative index counts from the end, so this gets the last child node of `root`.
current_scene = root.get_child(-1)

@ -9,7 +9,6 @@ type = 7
target_count = 1
current_count = 0
description = "Calculate how many power plants are needed to supply electricity to 40 houses."
structure_index = -1
completed = false
[resource]

@ -1,16 +1,17 @@
[gd_resource type="Resource" script_class="MissionData" load_steps=4 format=3 uid="uid://x5h4xutbldq3"]
[gd_resource type="Resource" script_class="MissionData" load_steps=5 format=3 uid="uid://x5h4xutbldq3"]
[ext_resource type="Script" path="res://scripts/mission/mission_data.gd" id="1_nv6c6"]
[ext_resource type="Script" path="res://scripts/mission/mission_objective.gd" id="1_yfbrc"]
[ext_resource type="Resource" uid="uid://dv14kkhb6umkv" path="res://structures/road-straight.tres" id="2_d0ffl"]
[sub_resource type="Resource" id="Resource_ywws1"]
script = ExtResource("1_yfbrc")
type = 2
target_count = 1
type = 0
target_count = 3
current_count = 0
description = "Build a road"
structure_index = -1
description = "Build 3 connecting roads"
completed = false
structure = ExtResource("2_d0ffl")
[resource]
script = ExtResource("1_nv6c6")

@ -9,7 +9,6 @@ type = 3
target_count = 40
current_count = 0
description = "Build 40 residential buildings with construction workers"
structure_index = -1
completed = false
[resource]

@ -1,21 +1,22 @@
[gd_resource type="Resource" script_class="MissionData" load_steps=4 format=3 uid="uid://cjr36hqnmyn0x"]
[gd_resource type="Resource" script_class="MissionData" load_steps=5 format=3 uid="uid://cjr36hqnmyn0x"]
[ext_resource type="Script" path="res://scripts/mission/mission_objective.gd" id="1_dhx01"]
[ext_resource type="Resource" uid="uid://cntgl86ianngh" path="res://structures/building-small-a.tres" id="2_em5vq"]
[ext_resource type="Script" path="res://scripts/mission/mission_data.gd" id="2_mum3p"]
[sub_resource type="Resource" id="Resource_7c02e"]
script = ExtResource("1_dhx01")
type = 3
target_count = 1
type = 0
target_count = 3
current_count = 0
description = "Build a residential building"
structure_index = -1
completed = false
structure = ExtResource("2_em5vq")
[resource]
script = ExtResource("2_mum3p")
id = "2"
title = "Building Homes"
title = "Census Planning"
description = "Now that we have a road, let's build a residential building for our citizens!"
objectives = Array[ExtResource("1_dhx01")]([SubResource("Resource_7c02e")])
rewards = {

@ -9,7 +9,6 @@ type = 10
target_count = 1
current_count = 0
description = "Build a power plant to provide electricity to your city"
structure_index = 7
completed = false
[resource]

@ -5,17 +5,16 @@
[sub_resource type="Resource" id="Resource_c06be"]
script = ExtResource("1_l3spi")
type = 7
target_count = 1
type = 2
target_count = 50
current_count = 0
description = "Compare two construction companies and determine which one is more efficient for building 40 houses in a week."
structure_index = -1
description = "Reach a population of 50 citizens"
completed = false
[resource]
script = ExtResource("2_b4llw")
id = "3"
title = "Compare Construction Companies"
title = "Census Planning"
description = "As your city grows, you need to choose the most efficient construction company."
objectives = Array[ExtResource("1_l3spi")]([SubResource("Resource_c06be")])
rewards = {

@ -25,6 +25,7 @@ general/default_playback_type.web=0
[autoload]
SoundManager="*res://scripts/sound_manager.gd"
EventBus="*res://global/event_bus.gd"
[display]

@ -31,20 +31,13 @@
script = ExtResource("14_76jlq")
panel_type = 0
title = "Welcome to Stem City "
body_text = "Hi League Community,
body_text = "Welcome to Stem City!
Your goal is to build a thriving community from the ground up. As you complete missions, you'll unlock new structures to expand and improve your city.
Each mission introduces important Math concepts used in urban planning and city management. You'll apply mathematics while watching your city grow.
As the new city planner, you need to establish a baseline understanding of your growing community. The mayor has requested a comprehensive census to guide future development.
You are the very first group of students who get to test this. So keep in mind there will be bugs, but do note them.
We are aware of the following bugs:
- Population count may be off by 1
- Lighting Baking in Web Builds are too bright
- We don't restrict building off of roads which will cause workers not to reach buildings
- Building overlap
- No builders for Power Plant
Ready to start planning your city? Click Close to see the controls and begin your first mission!

@ -106,7 +106,6 @@ func connect_to_sound_manager() -> bool:
})();
"""
// Set up the callback
js.set_callback("godot_audio_state_callback", Callable(self, "_on_audio_state_received"))
result = js.eval(post_message_script)

@ -646,11 +646,9 @@ func _update_mission_objective_on_demolish():
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)
mission_manager.update_objective_progress(mission_id, MissionObjective.ObjectiveType, -1)
# Function to remove terrain (grass or trees)
func _remove_terrain(position: Vector3):

@ -1,6 +1,11 @@
extends Node
# This script handles overall game management tasks
# This script handles overall game management tasks, including audio management and UI interactions.
var config = ConfigFile.new()
var music_player: AudioStreamPlayer
var building_sfx: AudioStreamPlayer
@ -14,6 +19,11 @@ var construction_sfx: AudioStreamPlayer
func _ready():
# Load data from a file.
var err = config.load("global://config.cfg")
# If the file didn't load, ignore it.
if err != OK:
return
# 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"))
@ -299,6 +309,10 @@ func _on_mission_manager_all_missions_completed() -> void:
func _on_mission_manager_mission_started(mission: MissionData) -> void:
var mission_manager: Node = get_node_or_null("/root/Main/MissionManager")
if mission_manager and mission_manager.mission_ui:
mission_manager.mission_ui.update_mission_display(mission)
var mission_text = GenericText.new()
mission_text.panel_type = 2
mission_text.title = mission.title

@ -1,16 +1,15 @@
extends Node
# Signals
signal population_updated(new_population)
signal electricity_updated(usage, production)
signal electricity_updated(usage, production)
# Variables
var total_population: int = 0
var total_kW_usage: float = 0.0
var total_kW_production: float = 0.0
# References
var builder
var buildeJuj
var building_construction_manager
var population_label: Label
var electricity_label: Label
@ -19,14 +18,17 @@ var population_tooltip: Control
var electricity_tooltip: Control
var controls_panel: PanelContainer
var sound_panel: PanelContainer
var builder:Node
func _ready():
# Connect to signals from the builder
builder = get_node_or_null("/root/Main/Builder")
population_updated.connect(update_population_count)
if builder:
builder.structure_placed.connect(_on_structure_placed)
builder.structure_removed.connect(_on_structure_removed)
EventBus.population_update.connect(set_population_count)
# Initialize UI elements
population_label = $HBoxContainer/PopulationItem/PopulationLabel
@ -119,7 +121,7 @@ func _on_structure_removed(structure_index, position):
# Update Population
func update_population_count(count: int):
func set_population_count(count: int):
total_population += count
population_label.text = str(total_population)

@ -1,13 +1,19 @@
extends Node
class_name BuildingConstructionManager
# Constants
const ObjectiveType = preload("res://configs/data.config.gd").ObjectiveType
# Signals
signal construction_completed(position)
signal worker_construction_started
signal worker_construction_ended
const CONSTRUCTION_TIME = 10.0 # seconds to build a building
# References to necessary scenes and resources
var worker_scene: PackedScene
var hud_manager: Node
@ -259,8 +265,8 @@ func _on_worker_construction_started():
func _on_worker_construction_ended():
# Forward the signal for mission managers/other systems that need it
worker_construction_ended.emit()
func _on_update_population(count: int):
hud_manager.population_updated.emit(count)
func update_population(count: int):
EventBus.population_update.emit(count)
@ -307,40 +313,11 @@ func _complete_construction(position: Vector3):
# Spawn a resident from the new building (except for first building in mission 1)
if should_spawn_resident:
_spawn_resident_from_building(position)
# Update population in the HUD when construction is complete
# Try different possible paths to find the HUD
var hud = get_node_or_null("/root/Main/CanvasLayer/HUD")
# If not found, try to find it by group (we added the HUD to "hud" group)
if not hud:
var hud_nodes = get_tree().get_nodes_in_group("hud")
if hud_nodes.size() > 0:
hud = hud_nodes[0]
# If not found, try other common paths
if not 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")
break
# Last resort - try to find using builder's cash_display
if not hud and builder and builder.cash_display:
var parent = builder.cash_display.get_parent()
while parent and parent.get_parent():
if "HUD" in parent.name:
hud = parent
break
parent = parent.get_parent()
if hud and site["structure_index"] >= 0 and site["structure_index"] < builder.structures.size():
var structure = builder.structures[site["structure_index"]]
if structure.type == Structure.StructureType.RESIDENTIAL_BUILDING and structure.population_count > 0:
_on_update_population(structure.population_count)
var structure = builder.structures[site["structure_index"]]
if structure.type == Structure.StructureType.RESIDENTIAL_BUILDING and structure.population_count > 0:
update_population(structure.population_count)
# hud.total_population += structure.population_count
# hud.update_hud()
# hud.population_updated.emit(hud.total_population)
@ -375,25 +352,13 @@ func _update_mission_objective_on_completion(structure_index: int):
# 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(
if mission_manager.current_objective.type == ObjectiveType.BUILD_RESIDENTIAL && structure.type == Structure.StructureType.RESIDENTIAL_BUILDING:
mission_manager.update_objective_progress(
mission_manager.current_mission.id,
MissionObjective.ObjectiveType.BUILD_RESIDENTIAL
1
)
# Trigger an immediate progress check
mission_manager.check_mission_progress(mission_manager.current_mission.id)
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):

@ -4,6 +4,8 @@ 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")
const ObjectiveType = preload("res://configs/data.config.gd").ObjectiveType
signal mission_started(mission: MissionData)
signal mission_completed(mission: MissionData)
@ -18,14 +20,15 @@ signal all_missions_completed()
@export var character_scene: PackedScene
var current_mission: MissionData
var current_objective: MissionObjective
var active_missions: Dictionary = {} # mission_id: MissionData
var character_spawned: bool = false
var learning_companion_connected: bool = false
# Panel state tracking
var is_unlocked_panel_showing = false
var delayed_mission_start_queue = [] # Queue of missions to start after unlocked panel closes
var is_unlocked_panel_showing: bool = false
var delayed_mission_start_queue = [] # Queue of missions to start after unlocked panel closes
# Mission skip variables
var skip_key_presses: int = 0
@ -225,138 +228,14 @@ func start_mission(mission: MissionData):
current_mission = mission
active_missions[mission.id] = mission
mission_started.emit(current_mission)
# Send mission started event to the learning companion
# This will also send the companion dialog data
_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")
if is_construction_or_expansion and builder:
# Check if we need to add the road-corner and decoration structures
var has_road_corner = false
var has_grass_trees_tall = false
var has_grass = false
# Look through existing structures to see if we already have them
for structure in builder.structures:
if structure.model.resource_path.contains("road-corner"):
has_road_corner = true
elif structure.model.resource_path.contains("grass-trees-tall"):
has_grass_trees_tall = true
elif structure.model.resource_path.contains("grass") and not structure.model.resource_path.contains("trees"):
has_grass = true
# Add the road-corner if missing
if not has_road_corner:
var road_corner = load("res://structures/road-corner.tres")
if road_corner:
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:
builder.structures.append(grass_trees_tall)
# Add the grass if missing
if not has_grass:
var grass = load("res://structures/grass.tres")
if grass:
builder.structures.append(grass)
# Special handling for power plant mission: add power plant
# Use more robust checking for power missions - check power_math_content as well
elif (mission.id == "5" or mission.power_math_content != "") and builder:
# Check if we need to add the power plant
var has_power_plant = false
# Look through existing structures to see if we already have it
for structure in builder.structures:
if structure.model.resource_path.contains("power_plant"):
has_power_plant = true
break
# Add the power plant if missing
if not has_power_plant:
var power_plant = load("res://structures/power-plant.tres")
if power_plant:
builder.structures.append(power_plant)
# Update the mesh library to include the new structures
if builder.gridmap and builder.gridmap.mesh_library:
var mesh_library = builder.gridmap.mesh_library
# Update mesh library for any new structures
for i in range(builder.structures.size()):
var structure = builder.structures[i]
if i >= mesh_library.get_item_list().size():
var id = mesh_library.get_last_unused_item_id()
mesh_library.create_item(id)
mesh_library.set_item_mesh(id, builder.get_mesh(structure.model))
# Apply appropriate scaling for all road types, buildings, and terrain
var transform = Transform3D()
if structure.model.resource_path.contains("power_plant"):
# Scale power plant model to be much smaller (0.5x)
transform = transform.scaled(Vector3(0.5, 0.5, 0.5))
elif (structure.type == Structure.StructureType.RESIDENTIAL_BUILDING
or structure.type == Structure.StructureType.ROAD
or structure.type == Structure.StructureType.TERRAIN
or structure.model.resource_path.contains("grass")):
# Scale buildings, roads, and decorative terrain to be consistent (3x)
transform = transform.scaled(Vector3(3.0, 3.0, 3.0))
mesh_library.set_item_mesh_transform(id, transform)
# Make sure the builder's structure selector is updated
builder.update_structure()
# Check if mission has a learning objective
var has_learning_objective = false
# Make sure mission has valid objectives data
if mission != null and mission.objectives != null:
for objective in mission.objectives:
if objective != null and objective.type == MissionObjective.ObjectiveType.LEARNING:
has_learning_objective = true
break
# Set the first objective as the current objective
if mission.objectives.size() > 0:
current_objective = mission.objectives[0]
print("Set current objective: " + str(current_objective.type) + " - " + current_objective.description)
# Show learning panel if mission has a learning objective
if has_learning_objective:
# 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:
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:
learning_panel.show_learning_panel(mission)
else:
print("ERROR: Regular learning panel not available")
# Emit signal and update UI
mission_started.emit(mission)
update_mission_ui()
func complete_mission(mission_id: String):
if not active_missions.has(mission_id):
@ -403,33 +282,25 @@ func complete_mission(mission_id: String):
# Send the "end" event to the companion
await get_tree().create_timer(2.0).timeout
func update_objective_progress(mission_id, objective_type, count_change = 1):
func update_objective_progress(mission_id,count_change = 1):
if not active_missions.has(mission_id):
return
var mission = active_missions[mission_id]
for objective in mission.objectives:
if objective.type == objective_type:
objective.current_count += count_change
# Only update to completed if we've reached the target
if objective.current_count >= objective.target_count and not objective.completed:
objective.completed = true
objective_completed.emit(objective)
# Send dialog event if available
var dialog_key = "objective_completed_" + str(objective.type)
_send_companion_dialog(dialog_key, mission)
# Update UI
update_mission_ui()
# Emit progress signal for objective
objective_progress.emit(objective, objective.current_count)
# Check if the mission is complete
check_mission_completion(mission_id)
break
current_objective.current_count += count_change
# IF this is true then objectives are completed
if current_objective.target_count <= current_objective.current_count:
current_objective.completed = true
objective_completed.emit(current_objective)
var dialog_key = "objective_completed_" + str(current_objective.type)
_send_companion_dialog(dialog_key, current_mission) ## So Companion can react
update_current_objective(current_mission) # Go ahead and progress to nex objective if it exists.
update_mission_ui()
objective_progress.emit(current_objective, current_objective.current_count)
check_mission_completion(mission_id)
func check_objective_completion(mission_id, objective_type):
if not active_missions.has(mission_id):
@ -469,6 +340,9 @@ func reset_objective_count(objective_type, new_count):
# Send dialog event if available
var dialog_key = "objective_completed_" + str(objective.type)
_send_companion_dialog(dialog_key, mission)
# Update current objective to next incomplete one
update_current_objective(mission)
# Update UI
update_mission_ui()
@ -512,15 +386,19 @@ func _on_structure_placed(structure_index, position):
print("Structure placed: " + structure.model.resource_path)
# Update objectives based on structure type
match current_objective.type:
ObjectiveType.BUILD_STRUCTURE:
update_objective_progress(current_mission.id, 1)
if current_mission:
if structure.type == Structure.StructureType.ROAD:
update_objective_progress(current_mission.id, MissionObjective.ObjectiveType.BUILD_ROAD)
elif structure.type == Structure.StructureType.RESIDENTIAL_BUILDING:
if structure.type == Structure.StructureType.RESIDENTIAL_BUILDING:
# Note: for mission 3, the objective update happens after construction is complete
# See builder.gd -> _on_construction_completed
# Special check for mission 1 since we might need to manually spawn a character
if current_mission.id == "1" and not character_spawned:
if character_spawned:
# Only spawn a new character if:
# 1. This is mission 1
# 2. We haven't spawned a character yet
@ -535,16 +413,6 @@ func _on_structure_placed(structure_index, position):
if spawn_character:
# This will be done after construction completes in mission_manager._on_construction_completed
print("Character will be spawned after construction completes")
else:
# Update the objective progress for building a residential structure
update_objective_progress(current_mission.id, MissionObjective.ObjectiveType.BUILD_RESIDENTIAL)
else:
# Normal case - not mission 1 or character already spawned
update_objective_progress(current_mission.id, MissionObjective.ObjectiveType.BUILD_RESIDENTIAL)
elif structure.type == Structure.StructureType.POWER_PLANT:
# For mission 5, we update the economy/power objective when a power plant is built
if current_mission.id == "6":
update_objective_progress(current_mission.id, MissionObjective.ObjectiveType.ECONOMY)
# Check for power plant unlocking in normal gameplay
if structure.type == Structure.StructureType.POWER_PLANT:
@ -558,21 +426,6 @@ func _on_structure_placed(structure_index, position):
hud.total_kW_production += power_produced
hud.update_hud()
# # Check for residential building placement to update population
# if structure.type == Structure.StructureType.RESIDENTIAL_BUILDING:
# # This should increase the city's population
# var population_added = structure.population_count
# if population_added > 0:
# # Get the HUD if available
# var hud = get_node_or_null("/root/Main/CanvasLayer/HUD")
# if hud:
# # Update the population display
# hud.total_population += population_added
# hud.update_hud()
#
# # Emit signal for population update
# hud.population_updated.emit(hud.total_population)
# Only used for mission 3, to disable builder functionality during the companion dialog
func _on_learning_panel_opened():
if builder:
@ -765,9 +618,6 @@ func _spawn_character_on_road(building_position: Vector3):
# Set character as spawned to prevent multiple spawns
character_spawned = true
# Update the objective progress for meeting a character
if current_mission and current_mission.id == "1":
update_objective_progress(current_mission.id, MissionObjective.ObjectiveType.MEET_CHARACTER)
# Make sure the character has auto-patrol is enabled if the character supports it
if character.get("auto_patrol") != null:
@ -1223,6 +1073,27 @@ func _send_companion_dialog(dialog_key, mission):
JSBridge.get_interface().sendCompanionDialog(dialog_key, dialog_data)
return true
return false
# Helper function to update the current objective to the next incomplete one
func update_current_objective(mission = null):
# If no mission was provided, use the current mission
if mission == null:
mission = current_mission
if not mission:
return
# Look for the first incomplete objective
for objective in mission.objectives:
if not objective.completed:
current_objective = objective
print("Updated current objective: " + str(current_objective.type) + " - " + current_objective.description)
return
# If all objectives are complete, keep the last one as current
if mission.objectives.size() > 0:
current_objective = mission.objectives[-1]
print("All objectives complete, keeping last one as current objective")
# Fallback to force a connection if the normal method doesn't work
func _force_learning_companion_connection():
@ -1239,3 +1110,7 @@ func _force_learning_companion_connection():
# Send initial event if we've already started
if current_mission:
_on_mission_started_for_companion(current_mission)
func _on_hud_population_updated(new_population: Variant) -> void:
update_objective_progress(current_mission.id, new_population)

@ -1,27 +1,17 @@
extends Resource
class_name MissionObjective
enum ObjectiveType {
BUILD_STRUCTURE,
BUILD_SPECIFIC_STRUCTURE,
BUILD_ROAD,
BUILD_RESIDENTIAL,
BUILD_COMMERCIAL,
BUILD_INDUSTRIAL,
REACH_CASH_AMOUNT,
LEARNING,
CUSTOM,
MEET_CHARACTER,
ECONOMY
}
const ObjectiveType = preload("res://configs/data.config.gd").ObjectiveType
@export var type: ObjectiveType
@export var target_count: int = 1
@export var current_count: int = 0
@export var description: String = ""
@export var structure_index: int = -1 # For BUILD_SPECIFIC_STRUCTURE type
@export var completed: bool = false
@export_subgroup("Structure")
@export var structure: Structure
func is_completed() -> bool:
return current_count >= target_count

@ -11,7 +11,7 @@ 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.1
var music_volume: float = 0.0
var sfx_volume: float = 0.1
# Mute states