1223 lines
41 KiB
GDScript
1223 lines
41 KiB
GDScript
extends Node3D
|
|
|
|
@export var structures: Array[Structure] = []
|
|
|
|
var map:DataMap
|
|
|
|
var index:int = 0 # Index of structure being built
|
|
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
|
|
@export var view_camera:Camera3D # Used for raycasting mouse
|
|
@export var gridmap:GridMap
|
|
@export var cash_display:Label # Reference to cash label in HUD
|
|
var hud_manager: Node
|
|
|
|
var plane:Plane # Used for raycasting mouse
|
|
var disabled: bool = false # Used to disable building functionality
|
|
|
|
signal structure_placed(structure_index, position) # For our mission flow
|
|
|
|
var invalid_placement_material: StandardMaterial3D
|
|
|
|
# Central structure management
|
|
var _structures: Array[Structure] = []
|
|
|
|
# Variable to track selection state
|
|
var selectionEnabled: bool = false;
|
|
|
|
# Getter for structures that ensures deduplication
|
|
func get_structures() -> Array[Structure]:
|
|
return _structures
|
|
|
|
# Setter for structures that ensures deduplication
|
|
func set_structures(new_structures: Array[Structure]) -> void:
|
|
print("\n=== Updating Structures ===")
|
|
_structures = new_structures
|
|
_deduplicate_structures()
|
|
# Update construction manager with deduplicated structures
|
|
if construction_manager:
|
|
construction_manager.structures = _structures
|
|
print("=== Structure Update Complete ===\n")
|
|
|
|
# Function to deduplicate structures
|
|
func _deduplicate_structures() -> void:
|
|
|
|
# Create a new array to store unique structures
|
|
var unique_structures: Array[Structure] = []
|
|
var seen_paths = {}
|
|
|
|
# Initialize all structures, skipping duplicates
|
|
for i in range(_structures.size()):
|
|
var structure = _structures[i]
|
|
if not structure:
|
|
push_error("Null structure at index " + str(i))
|
|
continue
|
|
|
|
# Skip if no model
|
|
if not structure.model:
|
|
push_error("Structure at index " + str(i) + " has no model!")
|
|
continue
|
|
|
|
var path = structure.model.resource_path
|
|
|
|
# Skip if we've seen this path before
|
|
if path in seen_paths:
|
|
continue
|
|
|
|
seen_paths[path] = true
|
|
|
|
# Initialize unlocked property only if it doesn't exist
|
|
if not "unlocked" in structure:
|
|
structure.unlocked = false
|
|
unique_structures.append(structure)
|
|
|
|
# Replace the original structures array with our deduplicated one
|
|
_structures.clear()
|
|
_structures.append_array(unique_structures)
|
|
|
|
# Print final structure list for verification
|
|
print("=== Structure Deduplication Complete ===\n")
|
|
|
|
func _ready():
|
|
map = DataMap.new()
|
|
plane = Plane(Vector3.UP, Vector3.ZERO)
|
|
hud_manager = get_node_or_null("/root/Main/CanvasLayer/HUD")
|
|
|
|
# Connect event bus for emitting structure menu stuff
|
|
EventBus.set_structure.connect(action_structure_toggle)
|
|
|
|
|
|
# Create invalid placement material
|
|
invalid_placement_material = StandardMaterial3D.new()
|
|
invalid_placement_material.albedo_color = Color(1, 0, 0, 0.5) # Semi-transparent red
|
|
invalid_placement_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
|
|
# Create construction manager
|
|
construction_manager = BuildingConstructionManager.new()
|
|
add_child(construction_manager)
|
|
|
|
# Setup the navigation region if it doesn't exist
|
|
setup_navigation_region()
|
|
|
|
# Give the construction manager references it needs
|
|
construction_manager.builder = self
|
|
construction_manager.gridmap = gridmap
|
|
|
|
# Set gridmap cell size to 3 units
|
|
if gridmap:
|
|
gridmap.cell_size = Vector3(3, 3, 3)
|
|
|
|
print("\n=== Initializing Structures ===")
|
|
|
|
# Load all structures from the structures directory
|
|
var dir = DirAccess.open("res://structures")
|
|
if dir:
|
|
dir.list_dir_begin()
|
|
var file_name = dir.get_next()
|
|
while file_name != "":
|
|
if file_name.ends_with(".tres"):
|
|
var structure = load("res://structures/" + file_name)
|
|
if structure:
|
|
structures.append(structure)
|
|
file_name = dir.get_next()
|
|
else:
|
|
push_error("Failed to open structures directory!")
|
|
|
|
# Set initial structures through our centralized system
|
|
set_structures(structures)
|
|
|
|
# Connect to mission manager's structures_unlocked signal
|
|
var mission_manager = get_node_or_null("/root/Main/MissionManager")
|
|
if mission_manager:
|
|
mission_manager.structures_unlocked.connect(_on_structures_unlocked)
|
|
|
|
# Initially hide the selector since we don't know which structure to show yet
|
|
if selector:
|
|
selector.visible = false
|
|
|
|
print("=== Structure Initialization Complete ===\n")
|
|
|
|
update_cash()
|
|
|
|
# Function to add a structure to the array
|
|
func add_structure(structure: Structure) -> void:
|
|
structures.append(structure)
|
|
_deduplicate_structures()
|
|
# Update construction manager with deduplicated structures
|
|
if construction_manager:
|
|
construction_manager.structures = structures
|
|
|
|
# Function to remove a structure from the array
|
|
func remove_structure(structure: Structure) -> void:
|
|
structures.erase(structure)
|
|
_deduplicate_structures()
|
|
# Update construction manager with deduplicated structures
|
|
if construction_manager:
|
|
construction_manager.structures = structures
|
|
|
|
func _process(delta):
|
|
# Skip all building functionality if disabled or game is paused
|
|
if disabled or get_tree().paused:
|
|
# Hide selector when disabled or paused
|
|
if selector.visible:
|
|
selector.visible = false
|
|
return
|
|
|
|
# Make sure selector is visible
|
|
if !selector.visible and selectionEnabled:
|
|
selector.visible = true
|
|
|
|
#Check for clear selection key press
|
|
action_clear_selection()
|
|
|
|
# Controls
|
|
action_rotate() # Rotates selection 90 degrees
|
|
#action_structure_toggle() # Toggles between structures
|
|
|
|
action_save() # Saving
|
|
action_load() # Loading
|
|
|
|
# Map position based on mouse
|
|
var world_position = plane.intersects_ray(
|
|
view_camera.project_ray_origin(get_viewport().get_mouse_position()),
|
|
view_camera.project_ray_normal(get_viewport().get_mouse_position()))
|
|
|
|
# Snap to 3-unit grid
|
|
var gridmap_position = Vector3(
|
|
round(world_position.x / 3.0) * 3.0,
|
|
0,
|
|
round(world_position.z / 3.0) * 3.0
|
|
)
|
|
selector.position = lerp(selector.position, gridmap_position, delta * 40)
|
|
|
|
# Check for collisions and update visual feedback
|
|
var can_place = check_can_place(gridmap_position)
|
|
update_placement_visual(can_place)
|
|
|
|
action_build(gridmap_position, can_place)
|
|
action_demolish(gridmap_position)
|
|
|
|
# Function to check if the mouse is over any UI elements
|
|
func is_mouse_over_ui() -> bool:
|
|
# Check if mouse is over the structure menu via HUD
|
|
if hud_manager and hud_manager.has_method("is_mouse_over_structure_menu") and hud_manager.is_mouse_over_structure_menu():
|
|
return true
|
|
|
|
# Get mouse position
|
|
var mouse_pos = get_viewport().get_mouse_position()
|
|
|
|
# Check building selector panel
|
|
var building_selector = get_node_or_null("/root/Main/CanvasLayer/BuildingSelector")
|
|
if building_selector and building_selector.selection_panel and building_selector.selection_panel.visible:
|
|
var panel_rect = building_selector.selection_panel.get_global_rect()
|
|
if panel_rect.has_point(mouse_pos):
|
|
return true
|
|
|
|
# Check sticky builder panel UI overlaps
|
|
if is_mouse_over_sticky_builder():
|
|
return true
|
|
|
|
# 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
|
|
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()
|
|
|
|
# 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()
|
|
|
|
# Simple approach - just check if within actual HUD content area
|
|
if hbox_rect.has_point(mouse_pos):
|
|
return true
|
|
|
|
# Skip the complex recursion for now since it's not working
|
|
|
|
# Check mission panel
|
|
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()
|
|
if panel_rect.has_point(mouse_pos):
|
|
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()
|
|
if panel_rect.has_point(mouse_pos):
|
|
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()
|
|
if panel_rect.has_point(mouse_pos):
|
|
return true
|
|
|
|
return false
|
|
|
|
func is_mouse_over_sticky_builder() -> bool:
|
|
var sticky_builder = get_node_or_null("/root/Main/CanvasLayer/StickyBuilder")
|
|
if not sticky_builder:
|
|
return false
|
|
|
|
var mouse_pos = get_viewport().get_mouse_position()
|
|
return _check_visible_controls(sticky_builder, mouse_pos)
|
|
|
|
# Helper function to check a node and it's control children's overlap with mouse position
|
|
func _check_visible_controls(node: Node, mouse_pos: Vector2) -> bool:
|
|
if node is Control and node.is_visible_in_tree():
|
|
if node.get_global_rect().has_point(mouse_pos):
|
|
return true
|
|
|
|
# check for popups
|
|
if "get_popup" in node and node.has_method("get_popup"):
|
|
var popup = node.get_popup()
|
|
if popup and popup is PopupMenu and popup.visible:
|
|
var popup_pos = popup.position
|
|
var popup_rect = Rect2(popup_pos, popup.size)
|
|
if popup_rect.has_point(mouse_pos):
|
|
return true
|
|
|
|
for child in node.get_children():
|
|
if _check_visible_controls(child, mouse_pos):
|
|
return true
|
|
|
|
return false
|
|
|
|
# Retrieve the mesh from a PackedScene, used for dynamically creating a MeshLibrary
|
|
|
|
func get_mesh(packed_scene):
|
|
# Instantiate the scene to access its properties
|
|
var scene_instance = packed_scene.instantiate()
|
|
var mesh_instance = null
|
|
|
|
# Find the first MeshInstance3D in the scene
|
|
for child in scene_instance.get_children():
|
|
if child is MeshInstance3D:
|
|
mesh_instance = child
|
|
break
|
|
|
|
# If no direct child is a MeshInstance3D, search recursively
|
|
if mesh_instance == null:
|
|
mesh_instance = find_mesh_instance(scene_instance)
|
|
|
|
var mesh = null
|
|
if mesh_instance:
|
|
mesh = mesh_instance.mesh.duplicate()
|
|
|
|
# Clean up
|
|
scene_instance.queue_free()
|
|
|
|
return mesh
|
|
|
|
# Helper function to find a MeshInstance3D recursively
|
|
func find_mesh_instance(node):
|
|
for child in node.get_children():
|
|
if child is MeshInstance3D:
|
|
return child
|
|
|
|
var result = find_mesh_instance(child)
|
|
if result:
|
|
return result
|
|
|
|
return null
|
|
|
|
# Build (place) a structure
|
|
|
|
func action_build(gridmap_position, can_place: bool):
|
|
if Input.is_action_just_pressed("build"):
|
|
#Check if selection is on
|
|
if(not selectionEnabled):
|
|
return
|
|
|
|
# Check if the mouse is over any UI elements before building
|
|
if is_mouse_over_ui():
|
|
return
|
|
|
|
# Check if we can place here
|
|
if not can_place:
|
|
return
|
|
|
|
# Check if the current structure is unlocked before allowing placement
|
|
if "unlocked" in structures[index] and not structures[index].unlocked:
|
|
print("Cannot build locked structure: " + structures[index].model.resource_path)
|
|
return
|
|
|
|
var previous_tile = gridmap.get_cell_item(gridmap_position)
|
|
|
|
# For roads, we don't add to the gridmap, but still track it in our data
|
|
var is_road = structures[index].type == Structure.StructureType.ROAD
|
|
# For residential buildings, we use the construction manager in mission 3
|
|
var is_residential = structures[index].type == Structure.StructureType.RESIDENTIAL_BUILDING
|
|
# For power plants, we handle them specially
|
|
var is_power_plant = structures[index].model.resource_path.contains("power_plant")
|
|
# For grass and trees (terrain), we need special handling
|
|
var is_terrain = structures[index].type == Structure.StructureType.LANDSCAPE
|
|
|
|
# Check if we're in mission 3 (when we should use construction workers)
|
|
var use_worker_construction = true
|
|
var mission_manager = get_node_or_null("/root/Main/MissionManager")
|
|
# 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
|
|
|
|
# If there's already a road at this position, we need to clear it
|
|
if previous_tile >= 0 and previous_tile < structures.size() and structures[previous_tile].type == Structure.StructureType.ROAD:
|
|
# Remove any existing road
|
|
_remove_road_from_navregion(gridmap_position)
|
|
|
|
# Create a visible road model as a child of the NavRegion3D
|
|
_add_road_to_navregion(gridmap_position, index)
|
|
|
|
# Also add to gridmap for mission tracking
|
|
gridmap.set_cell_item(gridmap_position, index, gridmap.get_orthogonal_index_from_basis(selector.basis))
|
|
|
|
# Rebake the navigation mesh after adding the road
|
|
rebake_navigation_mesh()
|
|
|
|
# Make sure any existing NPCs are children of the navigation region
|
|
_move_characters_to_navregion()
|
|
elif is_power_plant:
|
|
_add_power_plant(gridmap_position, index)
|
|
|
|
# We still set the cell item for collision detection
|
|
gridmap.set_cell_item(gridmap_position, index, gridmap.get_orthogonal_index_from_basis(selector.basis))
|
|
elif is_terrain:
|
|
# Special handling for terrain (grass and trees)
|
|
_add_terrain(gridmap_position, index)
|
|
|
|
# We still set the cell item for collision detection
|
|
gridmap.set_cell_item(gridmap_position, index, gridmap.get_orthogonal_index_from_basis(selector.basis))
|
|
else:
|
|
# For all other buildings, start construction process with worker
|
|
var selector_basis = selector.basis
|
|
construction_manager.start_construction(gridmap_position, index, selector_basis)
|
|
|
|
if previous_tile != index:
|
|
map.cash -= structures[index].price
|
|
update_cash()
|
|
|
|
# Emit the signal that a structure was placed
|
|
structure_placed.emit(index, gridmap_position)
|
|
|
|
func setup_navigation_region():
|
|
# Create a single NavigationRegion3D for the entire map if it doesn't exist
|
|
if not nav_region:
|
|
nav_region = NavigationRegion3D.new()
|
|
nav_region.name = "NavRegion3D"
|
|
|
|
# Create and assign a NavigationMesh resource
|
|
var nav_mesh = NavigationMesh.new()
|
|
nav_region.navigation_mesh = nav_mesh
|
|
|
|
# Configure NavigationMesh parameters for our roads
|
|
nav_mesh.cell_size = 0.25
|
|
nav_mesh.cell_height = 0.25
|
|
nav_mesh.agent_height = 1.5
|
|
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
|
|
func rebake_navigation_mesh():
|
|
# Make sure we have a navigation region first
|
|
if not nav_region:
|
|
setup_navigation_region()
|
|
|
|
# Bake the navigation mesh for the entire map
|
|
nav_region.bake_navigation_mesh()
|
|
|
|
# Demolish (remove) a structure
|
|
|
|
signal structure_removed(structure_index, position)
|
|
|
|
func action_demolish(gridmap_position):
|
|
if Input.is_action_just_pressed("demolish"):
|
|
# Check if the mouse is over any UI elements
|
|
if is_mouse_over_ui():
|
|
return
|
|
|
|
# Check if there's a road at this position
|
|
var is_road = false
|
|
var road_name = "Road_" + str(int(gridmap_position.x)) + "_" + str(int(gridmap_position.z))
|
|
|
|
if nav_region and nav_region.has_node(road_name):
|
|
is_road = true
|
|
|
|
# Check if there's a power plant at this position
|
|
var is_power_plant = false
|
|
var power_plant_name = "PowerPlant_" + str(int(gridmap_position.x)) + "_" + str(int(gridmap_position.z))
|
|
|
|
if has_node(power_plant_name):
|
|
is_power_plant = true
|
|
|
|
# Check if there's terrain at this position
|
|
var is_terrain = false
|
|
var terrain_name = "Terrain_" + str(int(gridmap_position.x)) + "_" + str(int(gridmap_position.z))
|
|
|
|
if has_node(terrain_name):
|
|
is_terrain = true
|
|
|
|
# Or check the GridMap for non-road structures
|
|
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
|
|
for i in range(structures.size()):
|
|
if structures[i].type == Structure.StructureType.ROAD:
|
|
structure_index = i
|
|
break
|
|
|
|
# Remove the road model from the NavRegion3D
|
|
_remove_road_from_navregion(gridmap_position)
|
|
# Rebake the navigation mesh after removing the road
|
|
rebake_navigation_mesh()
|
|
# Make sure any existing NPCs are children of the navigation region
|
|
_move_characters_to_navregion()
|
|
elif is_power_plant:
|
|
# Find the power plant structure index
|
|
for i in range(structures.size()):
|
|
if structures[i].type == Structure.StructureType.POWER_PLANT:
|
|
structure_index = i
|
|
break
|
|
|
|
# Remove the power plant model
|
|
_remove_power_plant(gridmap_position)
|
|
# Also remove from gridmap
|
|
gridmap.set_cell_item(gridmap_position, -1)
|
|
elif is_terrain:
|
|
# Find the terrain structure index
|
|
for i in range(structures.size()):
|
|
if structures[i].type == Structure.StructureType.LANDSCAPE:
|
|
structure_index = i
|
|
break
|
|
|
|
# Remove the terrain model
|
|
_remove_terrain(gridmap_position)
|
|
# Also remove from gridmap
|
|
gridmap.set_cell_item(gridmap_position, -1)
|
|
elif is_building:
|
|
# Get the structure index from the gridmap
|
|
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):
|
|
# With our new approach using a single nav region, we just rebake
|
|
# the entire navigation mesh when roads are added or removed
|
|
|
|
rebake_navigation_mesh()
|
|
|
|
# Rotates the 'cursor' 90 degrees
|
|
|
|
func action_rotate():
|
|
if Input.is_action_just_pressed("rotate") and selectionEnabled:
|
|
selector.rotate_y(deg_to_rad(90))
|
|
|
|
|
|
# Clear current building selection
|
|
func action_clear_selection():
|
|
if Input.is_action_just_pressed("clear_selection"):
|
|
selectionEnabled = false
|
|
if selector:
|
|
selector.visible = false
|
|
index = -1
|
|
|
|
# Toggle between structures to build
|
|
|
|
func action_structure_toggle(structure:Structure):
|
|
var found_index = -1
|
|
for i in range(_structures.size()):
|
|
if _structures[i].resource_path == structure.resource_path:
|
|
found_index = i
|
|
break
|
|
|
|
if found_index == -1:
|
|
print("No structure found!")
|
|
else:
|
|
index = found_index
|
|
selectionEnabled = true
|
|
update_structure()
|
|
|
|
|
|
|
|
# Update the structure visual in the 'cursor'
|
|
func update_structure():
|
|
# Clear previous structure preview in selector
|
|
for n in selector_container.get_children():
|
|
selector_container.remove_child(n)
|
|
|
|
# Create new structure preview in selector
|
|
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)
|
|
# Center the power plant model within the selector
|
|
_model.position = Vector3(-3.0, 0.0, 3.0) # Reset position
|
|
else:
|
|
# Scale buildings, roads, and decorative terrain to match (3x)
|
|
_model.scale = Vector3(3.0, 3.0, 3.0)
|
|
_model.position.y += 0.0 # No need for Y adjustment with scaling
|
|
|
|
# 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)
|
|
|
|
# Function to add a road model as a child of the navigation region
|
|
func _add_road_to_navregion(position: Vector3, structure_index: int):
|
|
# Make sure we have a navigation region
|
|
if not nav_region:
|
|
setup_navigation_region()
|
|
|
|
# Create a unique name for this road based on its position
|
|
var road_name = "Road_" + str(int(position.x)) + "_" + str(int(position.z))
|
|
|
|
# Check if a road with this name already exists
|
|
if nav_region.has_node(road_name):
|
|
return
|
|
|
|
# Instantiate the road model using the actual selected structure
|
|
var road_model = structures[structure_index].model.instantiate()
|
|
|
|
road_model.name = road_name
|
|
|
|
# Add the road model to the NavRegion3D
|
|
nav_region.add_child(road_model)
|
|
|
|
# Create the transform directly matching the exact one from pathing.tscn
|
|
var transform = Transform3D()
|
|
|
|
# Set scale first
|
|
transform.basis = Basis().scaled(Vector3(3.0, 3.0, 3.0))
|
|
|
|
# Then apply rotation from the selector to preserve the rotation the player chose
|
|
transform.basis = transform.basis * selector.basis
|
|
|
|
# Set position
|
|
transform.origin = position
|
|
transform.origin.y = -0.065 # From the pathing scene y offset
|
|
|
|
# Apply the complete transform in one go
|
|
road_model.transform = transform
|
|
|
|
|
|
func _add_power_plant(position: Vector3, structure_index: int):
|
|
# Create a unique name for this power plant based on its position
|
|
var power_plant_name = "PowerPlant_" + str(int(position.x)) + "_" + str(int(position.z))
|
|
|
|
# Check if a power plant with this name already exists
|
|
if has_node(power_plant_name):
|
|
return
|
|
|
|
# Instantiate the power plant model
|
|
var power_plant_model = structures[structure_index].model.instantiate()
|
|
power_plant_model.name = power_plant_name
|
|
|
|
# Add the power plant model directly to the builder (this node)
|
|
add_child(power_plant_model)
|
|
|
|
# Create the transform
|
|
var transform = Transform3D()
|
|
|
|
# Set scale (using the smaller 0.5x scale)
|
|
transform.basis = Basis().scaled(Vector3(0.5, 0.5, 0.5))
|
|
|
|
# Apply rotation from the selector to preserve the rotation the player chose
|
|
transform.basis = transform.basis * selector.basis
|
|
|
|
# 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(-3, 0, 3)
|
|
transform.origin += offset
|
|
|
|
# Apply the complete transform in one go
|
|
power_plant_model.transform = transform
|
|
|
|
|
|
# Function to remove a power plant
|
|
func _remove_power_plant(position: Vector3):
|
|
# Get the power plant name based on its position
|
|
var power_plant_name = "PowerPlant_" + str(int(position.x)) + "_" + str(int(position.z))
|
|
|
|
# Check if a power plant with this name exists
|
|
if has_node(power_plant_name):
|
|
# Get the power plant and remove it
|
|
var power_plant = get_node(power_plant_name)
|
|
power_plant.queue_free()
|
|
|
|
else:
|
|
# 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:
|
|
_on_update_population(-1)
|
|
|
|
break
|
|
|
|
func _on_update_population(count: int):
|
|
hud_manager.population_updated.emit(count)
|
|
# Function to update mission objectives when residential building is demolished
|
|
func _update_mission_objective_on_demolish():
|
|
# Get reference to mission manager
|
|
var mission_manager = get_node_or_null("/root/Main/MissionManager")
|
|
|
|
if mission_manager and mission_manager.current_mission:
|
|
# For other missions, use the normal method
|
|
var mission_id = mission_manager.current_mission.id
|
|
mission_manager.update_objective_progress(mission_id, MissionObjective.ObjectiveType, -1)
|
|
|
|
# Function to remove terrain (grass or trees)
|
|
func _remove_terrain(position: Vector3):
|
|
# Get the terrain name based on its position
|
|
var terrain_name = "Terrain_" + str(int(position.x)) + "_" + str(int(position.z))
|
|
|
|
# Check if terrain with this name exists
|
|
if has_node(terrain_name):
|
|
# Get the terrain and remove it
|
|
var terrain = get_node(terrain_name)
|
|
terrain.queue_free()
|
|
else:
|
|
# 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):
|
|
# Make sure we have a navigation region
|
|
if not nav_region:
|
|
return
|
|
|
|
# Get the road name based on its position
|
|
var road_name = "Road_" + str(int(position.x)) + "_" + str(int(position.z))
|
|
|
|
# Check if a road with this name exists
|
|
if nav_region.has_node(road_name):
|
|
# Get the road and remove it
|
|
var road = nav_region.get_node(road_name)
|
|
road.queue_free()
|
|
else:
|
|
# No road found
|
|
pass
|
|
|
|
# Function to add all existing roads to the navigation region
|
|
func _add_existing_roads_to_navregion():
|
|
# Make sure we have a navigation region
|
|
if not nav_region:
|
|
setup_navigation_region()
|
|
|
|
# Clean up any existing road models in the navigation region
|
|
for child in nav_region.get_children():
|
|
if child.name.begins_with("Road_"):
|
|
child.queue_free()
|
|
|
|
# Find all road cells in the gridmap
|
|
var added_count = 0
|
|
|
|
# We need to convert any existing roads in the GridMap to our new system
|
|
# Find existing road cells and add them to the NavRegion3D, then clear from GridMap
|
|
for cell in gridmap.get_used_cells():
|
|
var structure_index = gridmap.get_cell_item(cell)
|
|
if structure_index >= 0 and structure_index < structures.size():
|
|
if structures[structure_index].type == Structure.StructureType.ROAD:
|
|
# Add this road to the NavRegion3D
|
|
_add_road_to_navregion(cell, structure_index)
|
|
# Remove from the GridMap since we're now handling roads differently
|
|
gridmap.set_cell_item(cell, -1)
|
|
added_count += 1
|
|
|
|
# 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
|
|
if not nav_region:
|
|
setup_navigation_region()
|
|
|
|
# Find all characters in the scene
|
|
var characters = get_tree().get_nodes_in_group("characters")
|
|
for character in characters:
|
|
# Skip if already a child of nav_region
|
|
if character.get_parent() == nav_region:
|
|
continue
|
|
|
|
# Get current global position and parent
|
|
var original_parent = character.get_parent()
|
|
var global_pos = character.global_transform.origin
|
|
|
|
# Reparent to the navigation region
|
|
if original_parent:
|
|
original_parent.remove_child(character)
|
|
nav_region.add_child(character)
|
|
|
|
# Restore global position
|
|
character.global_transform.origin = global_pos
|
|
|
|
|
|
# Function to add terrain (grass or trees) as a direct child
|
|
func _add_terrain(position: Vector3, structure_index: int):
|
|
# Create a unique name for this terrain element based on its position
|
|
var terrain_name = "Terrain_" + str(int(position.x)) + "_" + str(int(position.z))
|
|
|
|
# Check if terrain with this name already exists
|
|
if has_node(terrain_name):
|
|
return
|
|
|
|
# Instantiate the terrain model
|
|
var terrain_model = structures[structure_index].model.instantiate()
|
|
terrain_model.name = terrain_name
|
|
|
|
# Add the terrain model directly to the builder (this node)
|
|
add_child(terrain_model)
|
|
|
|
# Create the transform
|
|
var transform = Transform3D()
|
|
|
|
# Set scale (using 3.0 scale as per other terrain elements)
|
|
transform.basis = Basis().scaled(Vector3(3.0, 3.0, 3.0))
|
|
|
|
# Apply rotation from the selector to preserve the rotation the player chose
|
|
transform.basis = transform.basis * selector.basis
|
|
|
|
# Set position
|
|
transform.origin = position
|
|
|
|
# Apply the complete transform in one go
|
|
terrain_model.transform = transform
|
|
|
|
# Callback for when construction is completed
|
|
func _on_construction_completed(position: Vector3):
|
|
# Get the original structure index that was selected for construction
|
|
var structure_index = -1
|
|
var rotation_index = 0
|
|
|
|
if construction_manager and construction_manager.construction_sites.has(position):
|
|
var site = construction_manager.construction_sites[position]
|
|
if site.has("structure_index"):
|
|
structure_index = site["structure_index"]
|
|
if site.has("rotation_index"):
|
|
rotation_index = site["rotation_index"]
|
|
|
|
# If we couldn't get the original structure index, fall back to finding any residential building
|
|
if structure_index < 0 or structure_index >= structures.size():
|
|
for i in range(structures.size()):
|
|
if structures[i].type == Structure.StructureType.RESIDENTIAL_BUILDING:
|
|
structure_index = i
|
|
break
|
|
|
|
if structure_index >= 0:
|
|
# Check if we need to spawn a character for mission 1
|
|
var mission_manager = get_node_or_null("/root/Main/MissionManager")
|
|
if mission_manager:
|
|
# 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:
|
|
mission_manager.character_spawned = true
|
|
mission_manager._spawn_character_on_road(position)
|
|
else:
|
|
# No residential building structure found
|
|
pass
|
|
|
|
# Make sure all characters (including newly spawned residents) are children of NavRegion3D
|
|
_move_characters_to_navregion()
|
|
|
|
# Make sure the navigation mesh is updated
|
|
rebake_navigation_mesh()
|
|
|
|
# Saving/load
|
|
|
|
func action_save():
|
|
if Input.is_action_just_pressed("save"):
|
|
map.structures.clear()
|
|
for cell in gridmap.get_used_cells():
|
|
|
|
var data_structure:DataStructure = DataStructure.new()
|
|
|
|
data_structure.position = Vector2i(cell.x, cell.z)
|
|
data_structure.orientation = gridmap.get_cell_item_orientation(cell)
|
|
data_structure.structure = gridmap.get_cell_item(cell)
|
|
|
|
map.structures.append(data_structure)
|
|
|
|
ResourceSaver.save(map, "user://map.res")
|
|
|
|
func action_load():
|
|
if Input.is_action_just_pressed("load"):
|
|
gridmap.clear()
|
|
|
|
map = ResourceLoader.load("user://map.res")
|
|
if not map:
|
|
map = DataMap.new()
|
|
for cell in map.structures:
|
|
gridmap.set_cell_item(Vector3i(cell.position.x, 0, cell.position.y), cell.structure, cell.orientation)
|
|
|
|
update_cash()
|
|
|
|
# Find and add all roads to the NavRegion3D
|
|
_add_existing_roads_to_navregion()
|
|
|
|
# After loading the map, rebake the navigation mesh to include all roads
|
|
rebake_navigation_mesh()
|
|
|
|
# Make sure any existing NPCs are children of the navigation region
|
|
_move_characters_to_navregion()
|
|
|
|
# Function to check if a structure can be placed at the given position
|
|
func check_can_place(pos: Vector3) -> bool:
|
|
# Check for existing structures in the gridmap
|
|
for cell in gridmap.get_used_cells():
|
|
var distance = Vector2(abs(cell.x - pos.x), abs(cell.z - pos.z))
|
|
var min_distance = 3.0 # Minimum distance between centers
|
|
|
|
# If either structure is a road and they're exactly adjacent, allow it
|
|
var existing_item = gridmap.get_cell_item(cell)
|
|
if (structures[index].type == Structure.StructureType.ROAD and
|
|
structures[existing_item].type == Structure.StructureType.ROAD):
|
|
if (distance.x == 1 and distance.y == 0) or (distance.x == 0 and distance.y == 1):
|
|
continue
|
|
|
|
# Check if too close
|
|
if distance.x < min_distance and distance.y < min_distance:
|
|
return false
|
|
|
|
# Check for roads in the navigation region
|
|
if nav_region:
|
|
for child in nav_region.get_children():
|
|
if child.name.begins_with("Road_"):
|
|
var road_pos = Vector3(
|
|
float(child.name.split("_")[1]),
|
|
0,
|
|
float(child.name.split("_")[2])
|
|
)
|
|
var distance = Vector2(abs(road_pos.x - pos.x), abs(road_pos.z - pos.z))
|
|
|
|
# If placing a road and they're exactly adjacent, allow it
|
|
if structures[index].type == Structure.StructureType.ROAD:
|
|
if (distance.x == 1 and distance.y == 0) or (distance.x == 0 and distance.y == 1):
|
|
continue
|
|
|
|
# Check if too close
|
|
if distance.x < 3 and distance.y < 3:
|
|
return false
|
|
|
|
# Check for power plants
|
|
for child in get_children():
|
|
if child.name.begins_with("PowerPlant_"):
|
|
var plant_pos = Vector3(
|
|
float(child.name.split("_")[1]),
|
|
0,
|
|
float(child.name.split("_")[2])
|
|
)
|
|
var distance = Vector2(abs(plant_pos.x - pos.x), abs(plant_pos.z - pos.z))
|
|
if distance.x < 3 and distance.y < 3:
|
|
return false
|
|
|
|
# Check for terrain
|
|
for child in get_children():
|
|
if child.name.begins_with("Terrain_"):
|
|
var terrain_pos = Vector3(
|
|
float(child.name.split("_")[1]),
|
|
0,
|
|
float(child.name.split("_")[2])
|
|
)
|
|
var distance = Vector2(abs(terrain_pos.x - pos.x), abs(terrain_pos.z - pos.z))
|
|
if distance.x < 2 and distance.y < 2:
|
|
return false
|
|
|
|
# Check for construction sites
|
|
if construction_manager:
|
|
for site_pos in construction_manager.construction_sites:
|
|
var distance = Vector2(abs(site_pos.x - pos.x), abs(site_pos.z - pos.z))
|
|
# Block the entire 3x3 grid space where construction is happening
|
|
if distance.x < 3 and distance.y < 3:
|
|
return false
|
|
|
|
# Check for plots (transparent previews of buildings being constructed)
|
|
for child in get_children():
|
|
if child.name.begins_with("Plot_"):
|
|
var plot_pos = Vector3(
|
|
float(child.name.split("_")[1]),
|
|
0,
|
|
float(child.name.split("_")[2])
|
|
)
|
|
var distance = Vector2(abs(plot_pos.x - pos.x), abs(plot_pos.z - pos.z))
|
|
# Block the entire 3x3 grid space where construction is happening
|
|
if distance.x < 3 and distance.y < 3:
|
|
return false
|
|
|
|
return true
|
|
|
|
# Update the visual feedback for placement
|
|
func update_placement_visual(can_place: bool):
|
|
if not selector_container or selector_container.get_child_count() == 0:
|
|
return
|
|
|
|
# Get the first child (the model)
|
|
var model = selector_container.get_child(0)
|
|
|
|
# Apply materials recursively to all mesh instances
|
|
for mesh_instance in _get_all_mesh_instances(model):
|
|
if can_place:
|
|
# Restore original materials
|
|
if mesh_instance.has_meta("original_materials"):
|
|
var original_materials = mesh_instance.get_meta("original_materials")
|
|
for i in range(original_materials.size()):
|
|
mesh_instance.set_surface_override_material(i, original_materials[i])
|
|
else:
|
|
# Store original materials if not already stored
|
|
if not mesh_instance.has_meta("original_materials"):
|
|
var materials = []
|
|
for i in range(mesh_instance.get_surface_override_material_count()):
|
|
materials.append(mesh_instance.get_surface_override_material(i))
|
|
mesh_instance.set_meta("original_materials", materials)
|
|
|
|
# Apply red transparent material
|
|
for i in range(mesh_instance.get_surface_override_material_count()):
|
|
mesh_instance.set_surface_override_material(i, invalid_placement_material)
|
|
|
|
# Helper function to get all MeshInstance3D nodes recursively
|
|
func _get_all_mesh_instances(node: Node) -> Array:
|
|
var mesh_instances = []
|
|
|
|
if node is MeshInstance3D:
|
|
mesh_instances.append(node)
|
|
|
|
for child in node.get_children():
|
|
mesh_instances.append_array(_get_all_mesh_instances(child))
|
|
|
|
return mesh_instances
|
|
|
|
# Place a structure at a specific position and rotation
|
|
func place_structure(structure_index: int, position: Vector3, rotation: float = 0.0) -> void:
|
|
if structure_index < 0 or structure_index >= structures.size():
|
|
push_error("Invalid structure index: " + str(structure_index))
|
|
return
|
|
|
|
# Store current index
|
|
var previous_index = index
|
|
|
|
# Set the structure to place
|
|
index = structure_index
|
|
|
|
# Create a gridmap position from the world position
|
|
var gridmap_position = Vector3(
|
|
round(position.x / 3.0) * 3.0,
|
|
0,
|
|
round(position.z / 3.0) * 3.0
|
|
)
|
|
|
|
# Check if we can place here
|
|
if not check_can_place(gridmap_position):
|
|
push_error("Cannot place structure at position: " + str(position))
|
|
index = previous_index
|
|
return
|
|
|
|
# Place the structure
|
|
var is_road = structures[index].type == Structure.StructureType.ROAD
|
|
var is_residential = structures[index].type == Structure.StructureType.RESIDENTIAL_BUILDING
|
|
var is_power_plant = structures[index].model.resource_path.contains("power_plant")
|
|
var is_terrain = structures[index].type == Structure.StructureType.LANDSCAPE
|
|
|
|
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
|
|
var previous_tile = gridmap.get_cell_item(gridmap_position)
|
|
if previous_tile >= 0 and previous_tile < structures.size() and structures[previous_tile].type == Structure.StructureType.ROAD:
|
|
_remove_road_from_navregion(gridmap_position)
|
|
_add_road_to_navregion(gridmap_position, index)
|
|
emit_signal("structure_placed", index, gridmap_position)
|
|
elif is_residential:
|
|
# For residential buildings, we use the construction manager
|
|
construction_manager.start_construction(gridmap_position, index)
|
|
emit_signal("structure_placed", index, gridmap_position)
|
|
elif is_power_plant:
|
|
# For power plants, we just place them directly in the gridmap
|
|
gridmap.set_cell_item(gridmap_position, index)
|
|
emit_signal("structure_placed", index, gridmap_position)
|
|
elif is_terrain:
|
|
# For grass and trees (terrain), we just place them directly in the gridmap
|
|
gridmap.set_cell_item(gridmap_position, index)
|
|
emit_signal("structure_placed", index, gridmap_position)
|
|
else:
|
|
# For other structures, we use the gridmap
|
|
gridmap.set_cell_item(gridmap_position, index)
|
|
emit_signal("structure_placed", index, gridmap_position)
|
|
|
|
# Restore previous index
|
|
index = previous_index
|
|
|
|
# New function to handle when structures are unlocked
|
|
func _on_structures_unlocked():
|
|
print("\n=== Structures Unlocked, Setting Initial Structure ===")
|
|
|
|
# Find the first unlocked structure in the array
|
|
var found_unlocked = false
|
|
print("\nChecking for unlocked structures:")
|
|
for i in range(_structures.size()):
|
|
var structure = _structures[i]
|
|
print("Structure " + str(i) + ": " + structure.model.resource_path + " - Unlocked: " + str(structure.unlocked))
|
|
if structure.unlocked:
|
|
index = i
|
|
found_unlocked = true
|
|
print("Found first unlocked structure at index " + str(i) + ": " + structure.model.resource_path)
|
|
break
|
|
|
|
if not found_unlocked:
|
|
print("WARNING: No unlocked structures found!")
|
|
# Don't set any structure as selected if none are unlocked
|
|
index = -1
|
|
# Hide the selector since we have no structures to place
|
|
if selector:
|
|
selector.visible = false
|
|
else:
|
|
# Show the selector since we have a structure to place
|
|
if selector and selectionEnabled:
|
|
selector.visible = true
|
|
|
|
update_structure()
|
|
print("=== Initial Structure Set ===\n")
|
|
|
|
# Function to deduplicate structures
|