Starter-Kit-City-Builder/scripts/mission/mission_manager.gd

991 lines
35 KiB
GDScript

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
@export var builder: Node3D
@export var character_scene: PackedScene
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
var last_skip_press_time: float = 0
var skip_key_timeout: float = 1.0 # Reset counter if time between presses exceeds this value
var skip_key_required: int = 5 # Number of key presses needed to skip
const SKIP_KEY = KEY_TAB # The key to press for skipping missions
# Reference for the learning panel without type hint
var learning_panel
var fullscreen_learning_panel
func _ready():
if builder:
# Connect to builder signals
builder.connect("structure_placed", _on_structure_placed)
# Find and remove existing learning panel to avoid conflicts
var old_panel = get_node_or_null("LearningPanel")
if old_panel:
old_panel.queue_free()
# Load the learning panel scene fresh each time
var learning_panel_scene = load("res://scenes/learning_panel.tscn")
if learning_panel_scene:
learning_panel = learning_panel_scene.instantiate()
learning_panel.name = "LearningPanelFromScene"
add_child(learning_panel)
else:
print("ERROR: Could not load learning_panel.tscn scene")
# Load the fullscreen learning panel scene
var fullscreen_panel_scene = load("res://scenes/fullscreen_learning_panel.tscn")
if fullscreen_panel_scene:
fullscreen_learning_panel = fullscreen_panel_scene.instantiate()
fullscreen_learning_panel.name = "FullscreenLearningPanel"
add_child(fullscreen_learning_panel)
else:
print("ERROR: Could not load fullscreen_learning_panel.tscn scene")
# Fall back to existing panels if needed
if not learning_panel:
learning_panel = get_node_or_null("/root/Main/LearningPanel")
# Connect signals for both panel types
if learning_panel:
learning_panel.completed.connect(_on_learning_completed)
learning_panel.panel_opened.connect(_on_learning_panel_opened)
learning_panel.panel_closed.connect(_on_learning_panel_closed)
else:
print("WARNING: Regular learning panel not found!")
if fullscreen_learning_panel:
fullscreen_learning_panel.completed.connect(_on_learning_completed)
fullscreen_learning_panel.panel_opened.connect(_on_learning_panel_opened)
fullscreen_learning_panel.panel_closed.connect(_on_learning_panel_closed)
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:
var found = false
for mission in missions:
if mission.id == "3":
found = true
break
if not found:
missions.append(third_mission)
# Set next_mission_id for second mission to point to third mission
for mission in missions:
if mission.id == "2":
mission.next_mission_id = "3"
# Load fourth mission if not already in the list
var fourth_mission = load("res://mission/fourth_mission.tres")
if fourth_mission:
var found = false
for mission in missions:
if mission.id == "4":
found = true
break
if not found:
missions.append(fourth_mission)
# Set next_mission_id for third mission to point to fourth mission
for mission in missions:
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:
push_error("Null mission data provided to start_mission")
return
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")
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 == "4" 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
# 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):
return
var mission = active_missions[mission_id]
# Grant rewards
if mission.rewards.has("cash") and builder:
builder.map.cash += mission.rewards.cash
builder.update_cash()
# 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:
if next_mission.id == mission.next_mission_id:
start_mission(next_mission)
break
else:
# This was the last mission - show completion modal
_show_completion_modal()
# Emit signal for mission completion
mission_completed.emit(mission)
update_mission_ui()
# Shows a modal when all missions are complete
func _show_completion_modal():
# Emit signal that all missions are completed
all_missions_completed.emit()
# Create the modal overlay
var modal = ColorRect.new()
modal.name = "CompletionModal"
modal.color = Color(0.1, 0.1, 0.2, 0.9) # Dark transparent background
modal.anchor_right = 1.0
modal.anchor_bottom = 1.0
# Create a panel container for the modal content
var panel = PanelContainer.new()
panel.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER
panel.custom_minimum_size = Vector2(800, 500)
var panel_style = StyleBoxFlat.new()
panel_style.bg_color = Color(0.15, 0.15, 0.25, 1.0)
panel_style.border_width_left = 5
panel_style.border_width_top = 5
panel_style.border_width_right = 5
panel_style.border_width_bottom = 5
panel_style.border_color = Color(0.376, 0.760, 0.658, 1.0) # Teal border
panel_style.corner_radius_top_left = 20
panel_style.corner_radius_top_right = 20
panel_style.corner_radius_bottom_right = 20
panel_style.corner_radius_bottom_left = 20
panel_style.shadow_color = Color(0, 0, 0, 0.7)
panel_style.shadow_size = 10
panel.add_theme_stylebox_override("panel", panel_style)
# Create a margin container for padding
var margin = MarginContainer.new()
margin.add_theme_constant_override("margin_left", 30)
margin.add_theme_constant_override("margin_right", 30)
margin.add_theme_constant_override("margin_top", 30)
margin.add_theme_constant_override("margin_bottom", 30)
# Create a vertical container for the content
var v_box = VBoxContainer.new()
v_box.custom_minimum_size = Vector2(700, 0)
v_box.add_theme_constant_override("separation", 30)
# Add a title label
var title_label = Label.new()
title_label.text = "CONGRATULATIONS!"
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title_label.add_theme_font_size_override("font_size", 48)
title_label.add_theme_color_override("font_color", Color(0.376, 0.760, 0.658, 1.0)) # Teal text
# Add a description label
var desc_label = Label.new()
desc_label.text = "You've completed all the missions in STEM City!\n\nYou can continue building and expanding your city or try different activities."
desc_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
desc_label.add_theme_font_size_override("font_size", 32)
desc_label.add_theme_color_override("font_color", Color(1, 1, 1, 1.0))
desc_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
# Create a continue button
var continue_button = Button.new()
continue_button.name = "ContinueButton"
continue_button.text = "CONTINUE BUILDING"
continue_button.custom_minimum_size = Vector2(400, 80)
# Style the continue button
var button_style = StyleBoxFlat.new()
button_style.bg_color = Color(0.376, 0.760, 0.658, 0.25) # Teal with transparency
button_style.border_width_left = 3
button_style.border_width_top = 3
button_style.border_width_right = 3
button_style.border_width_bottom = 3
button_style.border_color = Color(0.376, 0.760, 0.658, 1.0) # Teal border
button_style.corner_radius_top_left = 15
button_style.corner_radius_top_right = 15
button_style.corner_radius_bottom_right = 15
button_style.corner_radius_bottom_left = 15
continue_button.add_theme_stylebox_override("normal", button_style)
continue_button.add_theme_stylebox_override("hover", button_style)
continue_button.add_theme_stylebox_override("pressed", button_style)
continue_button.add_theme_font_size_override("font_size", 32)
continue_button.add_theme_color_override("font_color", Color(0.376, 0.760, 0.658, 1.0)) # Teal text
continue_button.add_theme_color_override("font_hover_color", Color(1, 1, 1, 1.0)) # White text on hover
# Center the continue button
var button_container = CenterContainer.new()
button_container.add_child(continue_button)
# Add elements to the vertical container
v_box.add_child(title_label)
v_box.add_child(desc_label)
v_box.add_child(button_container)
# Assemble the hierarchy
margin.add_child(v_box)
panel.add_child(margin)
# Center the panel in the modal
var center_container = CenterContainer.new()
center_container.anchor_right = 1.0
center_container.anchor_bottom = 1.0
center_container.add_child(panel)
modal.add_child(center_container)
# Add the modal to the scene
var canvas_layer = get_node("/root/Main/CanvasLayer")
if canvas_layer:
canvas_layer.add_child(modal)
else:
add_child(modal)
# 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):
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()
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
var mission = active_missions[mission_id]
var all_completed = true
for i in range(mission.objectives.size()):
var objective = mission.objectives[i]
if not objective.completed:
all_completed = false
if all_completed:
complete_mission(mission_id)
return true
return false
func update_objective_progress(mission_id: String, objective_type: int, amount: int = 1, structure_index: int = -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:
# For specific structure objectives, check structure index
if objective.type == MissionObjective.ObjectiveType.BUILD_SPECIFIC_STRUCTURE:
if structure_index != objective.structure_index:
continue
# Track old count for comparison
var old_count = objective.current_count
# 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:
objective_progress.emit(objective, objective.current_count)
# Check if objective was just completed
if objective.completed and old_count != objective.current_count:
objective_completed.emit(objective)
# Check if mission is now complete
check_mission_progress(mission_id)
update_mission_ui()
func _on_structure_placed(structure_index: int, position: Vector3):
if structure_index < 0 or structure_index >= builder.structures.size():
return
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)
# Update based on structure type
match structure.type:
Structure.StructureType.ROAD:
update_objective_progress(mission_id, MissionObjective.ObjectiveType.BUILD_ROAD)
Structure.StructureType.RESIDENTIAL_BUILDING:
# 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
Structure.StructureType.COMMERCIAL_BUILDING:
update_objective_progress(mission_id, MissionObjective.ObjectiveType.BUILD_COMMERCIAL)
Structure.StructureType.INDUSTRIAL_BUILDING:
update_objective_progress(mission_id, MissionObjective.ObjectiveType.BUILD_INDUSTRIAL)
# If it's a specific structure, check that too
update_objective_progress(
mission_id,
MissionObjective.ObjectiveType.BUILD_SPECIFIC_STRUCTURE,
1,
structure_index
)
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 != "":
check_mission_progress(current_mission.id)
func _on_learning_panel_opened():
# Disable building controls
if builder:
builder.disabled = true
func _on_learning_panel_closed():
# Re-enable building controls
if builder:
builder.disabled = false
# Method to skip the current mission
func _skip_current_mission():
if not current_mission:
return
var mission_id = current_mission.id
# Auto-complete all objectives in the current mission
for objective in current_mission.objectives:
objective.progress(objective.target_count - objective.current_count)
# If there's a learning panel open, close it
if learning_panel and learning_panel.visible:
learning_panel.hide_learning_panel()
# If there's a fullscreen learning panel open, close it
if fullscreen_learning_panel and fullscreen_learning_panel.visible:
fullscreen_learning_panel.hide_fullscreen_panel()
# Complete the mission
complete_mission(mission_id)
func _spawn_character_on_road(building_position: Vector3):
if !character_scene:
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:
character_spawned = true
return
# Mark as spawned to prevent multiple spawns
character_spawned = true
# Find the nearest road to the building
var gridmap = builder.gridmap
var nearest_road_position = _find_nearest_road(building_position, gridmap)
if nearest_road_position != Vector3.ZERO:
# Make sure there are no existing characters
for existing in get_tree().get_nodes_in_group("characters"):
existing.queue_free()
# Use the pre-made character pathing scene
var character = load("res://scenes/character_pathing.tscn").instantiate()
# Override with our improved navigation script
character.set_script(load("res://scripts/NavigationNPC.gd"))
# Add to a group for management
character.add_to_group("characters")
# Find the NavRegion3D (should have been created by builder)
var nav_region = builder.nav_region
if nav_region:
# Add character as a child of the NavRegion3D
nav_region.add_child(character)
else:
# Fallback to root if NavRegion3D doesn't exist
get_tree().root.add_child(character)
# 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)
# Allow the character to initialize
await get_tree().process_frame
# Make sure the navigation agent is properly set up
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
# Set target position
nav_agent.set_target_position(target_position)
# Make the character start moving
if character.has_method("set_movement_target"):
character.set_movement_target(target_position)
func _setup_character_for_navigation(character, initial_target):
# Access character's script to set up navigation
if character.has_node("character-female-d2"):
var model = character.get_node("character-female-d2")
# Set up animation
if model.has_node("AnimationPlayer"):
var anim_player = model.get_node("AnimationPlayer")
anim_player.play("walk")
# 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
# 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)
# Ensure auto-patrol is enabled if the character supports it
if character.get("auto_patrol") != null:
character.auto_patrol = true
# 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()
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
# some distance away, and the navigation system will handle finding a path
# Find a suitable target for navigation patrol
var directions = [Vector3.RIGHT, Vector3.LEFT, Vector3.FORWARD, Vector3.BACK]
# Get the navigation region
var nav_region = builder.nav_region
if nav_region:
# Try all four directions to find any road we can navigate to
for direction in directions:
for distance in range(1, int(max_distance) + 1):
var check_pos = start_position + direction * distance
var road_name = "Road_" + str(int(check_pos.x)) + "_" + str(int(check_pos.z))
# Check if there's a road at this position in the NavRegion3D
if nav_region.has_node(road_name):
return check_pos
# If all else fails, just return a point 5 units away in a random direction
var random_direction = Vector3(
randf_range(-1.0, 1.0),
0.0,
randf_range(-1.0, 1.0)
).normalized() * 5.0
return start_position + random_direction
# Function to find a connected road piece to determine orientation
func _find_connected_road(road_position: Vector3, gridmap: GridMap) -> Vector3:
var directions = [Vector3.RIGHT, Vector3.LEFT, Vector3.FORWARD, Vector3.BACK]
# First check for horizontal roads (left/right)
for direction in [Vector3.RIGHT, Vector3.LEFT]:
var check_pos = road_position + direction
var cell_item = gridmap.get_cell_item(check_pos)
# If it's a valid cell and a road
if cell_item >= 0 and cell_item < builder.structures.size():
if builder.structures[cell_item].type == Structure.StructureType.ROAD:
# Prioritize horizontal roads
return check_pos
# Then check for vertical roads (forward/back)
for direction in [Vector3.FORWARD, Vector3.BACK]:
var check_pos = road_position + direction
var cell_item = gridmap.get_cell_item(check_pos)
# If it's a valid cell and a road
if cell_item >= 0 and cell_item < builder.structures.size():
if builder.structures[cell_item].type == Structure.StructureType.ROAD:
return check_pos
return Vector3.ZERO
func _find_nearest_road(position: Vector3, gridmap: GridMap) -> Vector3:
# Check a 6x6 grid around the building for better coverage
var nearest_road = Vector3.ZERO
var min_distance = 100.0
var best_road_length = 0.0
# First pass: find all roads based on their presence in the NavRegion3D
var road_positions = []
# Get the navigation region
var nav_region = builder.nav_region
if nav_region:
# Look for road nodes in the navigation region
for child in nav_region.get_children():
if child.name.begins_with("Road_"):
# Extract position from the road name (format: "Road_X_Z")
var pos_parts = child.name.split("_")
if pos_parts.size() >= 3:
var road_x = int(pos_parts[1])
var road_z = int(pos_parts[2])
var road_pos = Vector3(road_x, 0, road_z)
# Check if this road is within range
if abs(road_pos.x - position.x) <= 3 and abs(road_pos.z - position.z) <= 3:
road_positions.append(road_pos)
# If we didn't find any roads in NavRegion3D, fall back to the old method
if road_positions.size() == 0:
for x in range(-3, 4):
for z in range(-3, 4):
var check_pos = Vector3(position.x + x, 0, position.z + z)
var road_name = "Road_" + str(int(check_pos.x)) + "_" + str(int(check_pos.z))
# Check if there's a road at this position in the NavRegion3D
if nav_region and nav_region.has_node(road_name):
road_positions.append(check_pos)
# Second pass: evaluate roads based on distance and connected length
for road_pos in road_positions:
var distance = position.distance_to(road_pos)
# Always choose the closest road initially
if nearest_road == Vector3.ZERO:
nearest_road = road_pos
min_distance = distance
# Otherwise just take the closest
elif distance < min_distance:
nearest_road = road_pos
min_distance = distance
return nearest_road
func _get_connected_road_length(road_position: Vector3, gridmap: GridMap) -> float:
# Simple function to find the length of a connected road
var road_length = 1.0
var directions = [Vector3.RIGHT, Vector3.LEFT, Vector3.FORWARD, Vector3.BACK]
# Check in all four directions
for direction in directions:
var check_pos = road_position
var connected_roads = 0
# Check up to 10 cells in this direction
for i in range(1, 11):
check_pos += direction
var cell_item = gridmap.get_cell_item(check_pos)
# Check if it's a road
if cell_item >= 0 and builder.structures[cell_item].type == Structure.StructureType.ROAD:
connected_roads += 1
else:
break
road_length = max(road_length, connected_roads + 1)
return road_length