590 lines
20 KiB
GDScript
590 lines
20 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
|
|
|
|
|
|
@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
|
|
|
|
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
|
|
|
|
func _ready():
|
|
|
|
map = DataMap.new()
|
|
plane = Plane(Vector3.UP, Vector3.ZERO)
|
|
|
|
# Create new MeshLibrary dynamically, can also be done in the editor
|
|
# See: https://docs.godotengine.org/en/stable/tutorials/3d/using_gridmaps.html
|
|
|
|
var mesh_library = MeshLibrary.new()
|
|
|
|
# Setup the navigation region if it doesn't exist
|
|
setup_navigation_region()
|
|
|
|
# Setup construction manager
|
|
construction_manager = BuildingConstructionManager.new()
|
|
add_child(construction_manager)
|
|
|
|
# Connect to the construction completion signal
|
|
construction_manager.construction_completed.connect(_on_construction_completed)
|
|
|
|
# Give the construction manager references it needs
|
|
construction_manager.builder = self
|
|
construction_manager.nav_region = nav_region
|
|
|
|
for structure in structures:
|
|
|
|
var id = mesh_library.get_last_unused_item_id()
|
|
|
|
mesh_library.create_item(id)
|
|
mesh_library.set_item_mesh(id, get_mesh(structure.model))
|
|
|
|
# Apply appropriate scaling for buildings and roads
|
|
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:
|
|
# Scale buildings and roads to be consistent (3x)
|
|
transform = transform.scaled(Vector3(3.0, 3.0, 3.0))
|
|
|
|
mesh_library.set_item_mesh_transform(id, transform)
|
|
|
|
gridmap.mesh_library = mesh_library
|
|
|
|
update_structure()
|
|
update_cash()
|
|
|
|
func _process(delta):
|
|
# Skip all building functionality if disabled
|
|
if disabled:
|
|
# Hide selector when disabled
|
|
if selector.visible:
|
|
selector.visible = false
|
|
return
|
|
|
|
# Make sure selector is visible
|
|
if !selector.visible:
|
|
selector.visible = true
|
|
|
|
# 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()))
|
|
|
|
var gridmap_position = Vector3(round(world_position.x), 0, round(world_position.z))
|
|
selector.position = lerp(selector.position, gridmap_position, delta * 40)
|
|
|
|
action_build(gridmap_position)
|
|
action_demolish(gridmap_position)
|
|
|
|
# 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):
|
|
if Input.is_action_just_pressed("build"):
|
|
|
|
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")
|
|
|
|
# Check if we're in mission 3 (when we should use construction workers)
|
|
var use_worker_construction = false
|
|
var mission_manager = get_node_or_null("/root/Main/MissionManager")
|
|
if mission_manager and mission_manager.current_mission:
|
|
var mission_id = mission_manager.current_mission.id
|
|
if mission_id == "3" or (mission_id == "1" and is_residential):
|
|
use_worker_construction = true
|
|
|
|
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)
|
|
|
|
# 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:
|
|
# Special handling for power plants - add directly as a child of the builder
|
|
_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_residential and use_worker_construction:
|
|
# For residential buildings in mission 3, use construction workers
|
|
# Pass the current selector basis to preserve rotation
|
|
var selector_basis = selector.basis
|
|
construction_manager.start_construction(gridmap_position, index, selector_basis)
|
|
|
|
# Don't place the building immediately - it will be placed when construction completes
|
|
# We leave gridmap empty for now
|
|
else:
|
|
# For non-road structures or not in mission 3, add to the gridmap as usual
|
|
gridmap.set_cell_item(gridmap_position, index, gridmap.get_orthogonal_index_from_basis(selector.basis))
|
|
|
|
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)
|
|
print("Created global navigation region with navigation mesh")
|
|
|
|
|
|
# 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()
|
|
print("Navigation mesh rebaked")
|
|
|
|
# Demolish (remove) a structure
|
|
|
|
func action_demolish(gridmap_position):
|
|
if Input.is_action_just_pressed("demolish"):
|
|
# 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
|
|
|
|
# Or check the GridMap for non-road structures
|
|
var current_item = gridmap.get_cell_item(gridmap_position)
|
|
var is_building = current_item >= 0
|
|
|
|
# Remove the appropriate item
|
|
if is_road:
|
|
# 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:
|
|
# Remove the power plant model
|
|
_remove_power_plant(gridmap_position)
|
|
# Also remove from gridmap
|
|
gridmap.set_cell_item(gridmap_position, -1)
|
|
elif is_building:
|
|
# Remove the building from the gridmap
|
|
gridmap.set_cell_item(gridmap_position, -1)
|
|
|
|
# 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
|
|
print("Road removed at: ", position)
|
|
rebake_navigation_mesh()
|
|
|
|
# Rotates the 'cursor' 90 degrees
|
|
|
|
func action_rotate():
|
|
if Input.is_action_just_pressed("rotate"):
|
|
selector.rotate_y(deg_to_rad(90))
|
|
|
|
# Toggle between structures to build
|
|
|
|
func action_structure_toggle():
|
|
if Input.is_action_just_pressed("structure_next"):
|
|
index = wrap(index + 1, 0, structures.size())
|
|
|
|
if Input.is_action_just_pressed("structure_previous"):
|
|
index = wrap(index - 1, 0, structures.size())
|
|
|
|
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)
|
|
|
|
# Apply appropriate scaling based on structure type
|
|
if structures[index].model.resource_path.contains("power_plant"):
|
|
# Scale power plant model to be much smaller (0.5x)
|
|
_model.scale = Vector3(0.5, 0.5, 0.5)
|
|
_model.position.y += 0.0 # No need for Y adjustment with scaling
|
|
elif (structures[index].type == Structure.StructureType.RESIDENTIAL_BUILDING
|
|
or structures[index].type == Structure.StructureType.ROAD
|
|
or structures[index].type == Structure.StructureType.TERRAIN
|
|
or structures[index].model.resource_path.contains("grass")):
|
|
# 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
|
|
else:
|
|
# Standard positioning for other structures
|
|
_model.position.y += 0.25
|
|
|
|
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):
|
|
print("Road already exists at position: ", position)
|
|
return
|
|
|
|
# Instantiate the road model - get the actual model based on road type
|
|
var road_model
|
|
var model_path = structures[structure_index].model.resource_path
|
|
if model_path.contains("road-straight"):
|
|
# Use the specific road-straight model that works with navmesh
|
|
road_model = load("res://models/road-straight.glb").instantiate()
|
|
elif model_path.contains("road-corner"):
|
|
# Use the specific road-corner model
|
|
road_model = load("res://models/road-corner.glb").instantiate()
|
|
else:
|
|
# Fall back to the structure's model for other road types
|
|
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
|
|
|
|
print("Added road model at position ", position, " to NavRegion3D")
|
|
|
|
# Function to add a power plant as a direct child of the builder
|
|
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):
|
|
print("Power plant already exists at position: ", position)
|
|
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
|
|
transform.origin = position
|
|
|
|
# Apply the complete transform in one go
|
|
power_plant_model.transform = transform
|
|
|
|
print("Added power plant at position ", position, " as direct child of builder")
|
|
|
|
# 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()
|
|
print("Removed power plant at position ", position)
|
|
else:
|
|
print("No power plant found at position ", position)
|
|
|
|
# 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()
|
|
print("Removed road at position ", position, " from NavRegion3D")
|
|
else:
|
|
print("No road found at position ", position, " in NavRegion3D")
|
|
|
|
# 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
|
|
print("Finding and adding all existing roads to NavRegion3D...")
|
|
var added_count = 0
|
|
|
|
# We need to convert any existing roads in the GridMap to our new system
|
|
# 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
|
|
|
|
print("Added ", added_count, " existing roads to NavRegion3D")
|
|
|
|
# Function to move all character NPCs to be children of the navigation region
|
|
func _move_characters_to_navregion():
|
|
# Make sure we have a navigation region
|
|
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
|
|
print("Moved character to NavRegion3D")
|
|
|
|
# Callback for when construction is completed
|
|
func _on_construction_completed(position: Vector3):
|
|
# We need to find a residential structure index to add to gridmap
|
|
var residential_index = -1
|
|
for i in range(structures.size()):
|
|
if structures[i].type == Structure.StructureType.RESIDENTIAL_BUILDING:
|
|
residential_index = i
|
|
break
|
|
|
|
if residential_index >= 0:
|
|
# Get the rotation index from the construction manager if available
|
|
var rotation_index = 0
|
|
|
|
# Try to get the rotation index from the construction manager
|
|
if construction_manager and construction_manager.construction_sites.has(position):
|
|
var site = construction_manager.construction_sites[position]
|
|
if site.has("rotation_index"):
|
|
rotation_index = site["rotation_index"]
|
|
print("Using saved rotation index: ", rotation_index)
|
|
|
|
# Add the completed residential building to the gridmap with the correct rotation
|
|
gridmap.set_cell_item(position, residential_index, rotation_index)
|
|
print("Construction completed: added building to gridmap at ", position, " with rotation index ", rotation_index)
|
|
|
|
# Check if we need to spawn a character for mission 1
|
|
var mission_manager = get_node_or_null("/root/Main/MissionManager")
|
|
if mission_manager:
|
|
# First, emit the structure_placed signal to update mission objectives
|
|
structure_placed.emit(residential_index, position)
|
|
print("Emitted structure_placed signal to update mission objectives")
|
|
|
|
# Now check if we need to manually handle mission 1 character spawning
|
|
if mission_manager.current_mission and mission_manager.current_mission.id == "1" and not mission_manager.character_spawned:
|
|
print("This is the first residential building in mission 1, spawning character")
|
|
mission_manager.character_spawned = true
|
|
mission_manager._spawn_character_on_road(position)
|
|
else:
|
|
# Just emit the signal if mission manager not found
|
|
structure_placed.emit(residential_index, position)
|
|
else:
|
|
print("ERROR: No residential building structure found!")
|
|
|
|
# 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()
|
|
|
|
print("Verified all characters are under NavRegion3D after construction")
|
|
|
|
# Saving/load
|
|
|
|
func action_save():
|
|
if Input.is_action_just_pressed("save"):
|
|
print("Saving map...")
|
|
|
|
map.structures.clear()
|
|
for cell in gridmap.get_used_cells():
|
|
|
|
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"):
|
|
print("Loading map...")
|
|
|
|
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()
|