feat: Add EnhancedGridMap for dynamic grid generation, pathfinding, and network synchronization, and introduce core game scripts for Tekton entities and managers.
This commit is contained in:
@@ -580,7 +580,9 @@ func clear_path_visualization(floor_index: int = 0):
|
|||||||
# Cost calculation and updates
|
# Cost calculation and updates
|
||||||
func get_cell_cost(x: int, z: int, floor_index: int = 0) -> float:
|
func get_cell_cost(x: int, z: int, floor_index: int = 0) -> float:
|
||||||
var cell_item = get_cell_item(Vector3i(x, floor_index, z))
|
var cell_item = get_cell_item(Vector3i(x, floor_index, z))
|
||||||
if cell_item in non_walkable_items:
|
|
||||||
|
# Block -1 (Void) and Non-Walkable Items
|
||||||
|
if cell_item == -1 or cell_item in non_walkable_items:
|
||||||
return INF
|
return INF
|
||||||
elif cell_item == hover_item:
|
elif cell_item == hover_item:
|
||||||
return 0.5
|
return 0.5
|
||||||
|
|||||||
+65
-18
@@ -55,6 +55,18 @@ func _ready():
|
|||||||
if em:
|
if em:
|
||||||
em.cell_size = Vector3(1, 0.05, 1)
|
em.cell_size = Vector3(1, 0.05, 1)
|
||||||
|
|
||||||
|
# Setup MultiplayerSpawner for Static Tekton Stands
|
||||||
|
# Create a container node for strict pathing
|
||||||
|
var stands_container = Node3D.new()
|
||||||
|
stands_container.name = "Stands"
|
||||||
|
add_child(stands_container)
|
||||||
|
|
||||||
|
var stand_spawner = MultiplayerSpawner.new()
|
||||||
|
stand_spawner.name = "StandSpawner"
|
||||||
|
stand_spawner.spawn_path = NodePath("../Stands") # Relative to Spawner, finding sibling
|
||||||
|
stand_spawner.add_spawnable_scene("res://scenes/static_tekton_stand.tscn")
|
||||||
|
add_child(stand_spawner)
|
||||||
|
|
||||||
func _on_goal_count_updated(peer_id: int, count: int):
|
func _on_goal_count_updated(peer_id: int, count: int):
|
||||||
# Only update for local player
|
# Only update for local player
|
||||||
if peer_id == multiplayer.get_unique_id():
|
if peer_id == multiplayer.get_unique_id():
|
||||||
@@ -699,28 +711,44 @@ func spawn_static_tektons():
|
|||||||
# ID: 99000 + i (Consistent IDs for Static Tektons)
|
# ID: 99000 + i (Consistent IDs for Static Tektons)
|
||||||
var id = 99000 + i
|
var id = 99000 + i
|
||||||
|
|
||||||
|
# Pick Shape on Server (0:Cyl, 1:Box, 2:Prism, 3:Sphere)
|
||||||
|
var shape_idx = randi() % 4
|
||||||
|
|
||||||
# Spawn on Server
|
# Spawn on Server
|
||||||
_create_static_setup(pos, id)
|
_create_static_setup(pos, id, shape_idx)
|
||||||
|
|
||||||
# Sync to Clients
|
# Sync to Clients
|
||||||
rpc("sync_spawn_static_setup", pos, id)
|
rpc("sync_spawn_static_setup", pos, id, shape_idx)
|
||||||
|
|
||||||
@rpc("call_remote", "reliable")
|
@rpc("call_local", "reliable")
|
||||||
func sync_spawn_static_setup(pos: Vector2i, tekton_id: int):
|
func sync_spawn_static_setup(pos: Vector2i, tekton_id: int, shape_idx: int):
|
||||||
_create_static_setup(pos, tekton_id)
|
# Call local creation logic on all peers.
|
||||||
|
# Server: Spawns Stand + Void + Tekton
|
||||||
|
# Client: Avoids Stand (Spawner) + Void + Tekton
|
||||||
|
_create_static_setup(pos, tekton_id, shape_idx)
|
||||||
|
|
||||||
func _create_static_setup(pos: Vector2i, tekton_id: int):
|
func _create_static_setup(pos: Vector2i, tekton_id: int, shape_idx: int):
|
||||||
"""Creates both the Stand and the Static Tekton at the position."""
|
"""Creates both the Stand and the Static Tekton at the position."""
|
||||||
var enhanced_gridmap = $EnhancedGridMap
|
var enhanced_gridmap = $EnhancedGridMap
|
||||||
|
|
||||||
# 1. Create Stand
|
# 1. Create Stand (Server Only - Synced via Spawner)
|
||||||
|
# IMPORTANT: Clients receive the Stand via MultiplayerSpawner.
|
||||||
|
# They MUST NOT spawn it manually here or we get duplicates.
|
||||||
|
if multiplayer.is_server():
|
||||||
|
var stands_container = get_node_or_null("Stands")
|
||||||
|
if stands_container:
|
||||||
var stand_name = "StaticStand_%d" % tekton_id
|
var stand_name = "StaticStand_%d" % tekton_id
|
||||||
if not has_node(stand_name):
|
if not stands_container.has_node(stand_name):
|
||||||
var stand_scene = load("res://scenes/static_tekton_stand.tscn")
|
var stand_scene = load("res://scenes/static_tekton_stand.tscn")
|
||||||
if stand_scene:
|
if stand_scene:
|
||||||
var stand = stand_scene.instantiate()
|
var stand = stand_scene.instantiate()
|
||||||
stand.name = stand_name
|
stand.name = stand_name
|
||||||
add_child(stand)
|
|
||||||
|
# Set Shape Index BEFORE adding to tree (so _ready picks it up/syncs)
|
||||||
|
if "shape_index" in stand:
|
||||||
|
stand.shape_index = shape_idx
|
||||||
|
|
||||||
|
stands_container.add_child(stand)
|
||||||
|
|
||||||
# Position Stand
|
# Position Stand
|
||||||
if enhanced_gridmap:
|
if enhanced_gridmap:
|
||||||
@@ -734,26 +762,45 @@ func _create_static_setup(pos: Vector2i, tekton_id: int):
|
|||||||
)
|
)
|
||||||
stand.global_position = world_pos
|
stand.global_position = world_pos
|
||||||
|
|
||||||
# Update GridMap to block pathfinding (Item 4 = Wall)
|
# 2. Modify Base (Void) - Runs on ALL peers to update local GridMap visual/collision
|
||||||
# Mark entire 3x3 area as immutable obstacles on FLOOR 0 (Ground Level)
|
if enhanced_gridmap:
|
||||||
# This overwrites the ground tile to ensure PlayerMovementManager sees it as blocked.
|
var floor_count = 3
|
||||||
|
if "floors" in enhanced_gridmap:
|
||||||
|
floor_count = enhanced_gridmap.floors
|
||||||
|
|
||||||
for dx in range(-1, 2):
|
for dx in range(-1, 2):
|
||||||
for dy in range(-1, 2):
|
for dy in range(-1, 2):
|
||||||
var tile_pos = Vector3i(pos.x + dx, 0, pos.y + dy)
|
var tile_pos_x = pos.x + dx
|
||||||
enhanced_gridmap.set_cell_item(tile_pos, 4)
|
var tile_pos_z = pos.y + dy
|
||||||
|
|
||||||
|
# Clear ALL vertical layers (Ground, Items, etc.)
|
||||||
|
for f in range(floor_count):
|
||||||
|
var tile_pos = Vector3i(tile_pos_x, f, tile_pos_z)
|
||||||
|
enhanced_gridmap.set_cell_item(tile_pos, -1) # -1 = Empty/Void
|
||||||
|
|
||||||
# CRITICAL: Force AStar update so Bots and Pathfinding know about the new walls
|
# CRITICAL: Force AStar update so Bots and Pathfinding know about the new walls
|
||||||
if enhanced_gridmap.has_method("update_astar_costs"):
|
if enhanced_gridmap.has_method("update_astar_costs"):
|
||||||
enhanced_gridmap.update_astar_costs()
|
enhanced_gridmap.update_astar_costs()
|
||||||
|
|
||||||
# 2. Create Tekton
|
# 3. Create Tekton Visual - Runs on ALL peers
|
||||||
# Reuse _create_tekton logic but force params
|
# NOTE: Tekton NPC is currently not managed by a specialized Spawner for static setup?
|
||||||
|
# Or it is? If _create_tekton adds it to a path watched by a spawner, we should duplicate check.
|
||||||
|
# _create_tekton instantiates 'tekton.tscn' and adds to 'Main'.
|
||||||
|
# Main usually has a MultiplayerSpawner for 'Players' etc., but let's check.
|
||||||
|
# The original logic spawned it everywhere, so we keep that behavior to be safe.
|
||||||
|
# But we add a check to avoid duplicates if it already came in via sync.
|
||||||
|
if not has_node("Tekton_%d" % tekton_id):
|
||||||
_create_tekton(pos, tekton_id, true)
|
_create_tekton(pos, tekton_id, true)
|
||||||
|
|
||||||
# Force Tekton height UP to sit on stand
|
# Force Tekton height UP to sit on stand on ALL peers
|
||||||
var tekton = get_node_or_null("Tekton_%d" % tekton_id)
|
var tekton = get_node_or_null("Tekton_%d" % tekton_id)
|
||||||
if tekton:
|
if tekton:
|
||||||
tekton.position.y += 0.6 # Stand Height
|
var height_offset = 0.6
|
||||||
|
# If Sphere (Index 3), it is taller (Dome)
|
||||||
|
if shape_idx == 3:
|
||||||
|
height_offset = 1.3
|
||||||
|
|
||||||
|
tekton.position.y += height_offset
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
[gd_scene load_steps=3 format=3 uid="uid://static_tekton_stand_001"]
|
[gd_scene load_steps=6 format=3 uid="uid://static_tekton_stand_001"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/static_tekton_stand.gd" id="1_script"]
|
||||||
|
|
||||||
[sub_resource type="CylinderMesh" id="CylinderMesh_stand"]
|
[sub_resource type="CylinderMesh" id="CylinderMesh_stand"]
|
||||||
top_radius = 1.4
|
top_radius = 1.4
|
||||||
@@ -10,12 +12,23 @@ albedo_color = Color(0.15, 0.15, 0.2, 1)
|
|||||||
metallic = 0.6
|
metallic = 0.6
|
||||||
roughness = 0.4
|
roughness = 0.4
|
||||||
|
|
||||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_stand"]
|
[sub_resource type="BoxShape3D" id="BoxShape3D_stand"]
|
||||||
height = 0.6
|
size = Vector3(3.2, 0.6, 3.2)
|
||||||
radius = 1.4
|
|
||||||
|
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_stand"]
|
||||||
|
properties/0/path = NodePath(":shape_index")
|
||||||
|
properties/0/spawn = true
|
||||||
|
properties/0/replication_mode = 2
|
||||||
|
properties/1/path = NodePath(":global_position")
|
||||||
|
properties/1/spawn = true
|
||||||
|
properties/1/replication_mode = 2
|
||||||
|
|
||||||
[node name="StaticTektonStand" type="StaticBody3D"]
|
[node name="StaticTektonStand" type="StaticBody3D"]
|
||||||
collision_mask = 0
|
collision_mask = 0
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
|
||||||
|
replication_config = SubResource("SceneReplicationConfig_stand")
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.3, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.3, 0)
|
||||||
@@ -24,4 +37,4 @@ surface_material_override/0 = SubResource("StandardMaterial3D_stand")
|
|||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.3, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.3, 0)
|
||||||
shape = SubResource("CylinderShape3D_stand")
|
shape = SubResource("BoxShape3D_stand")
|
||||||
|
|||||||
+35
-12
@@ -399,24 +399,16 @@ func _try_move() -> bool:
|
|||||||
if dist == 1:
|
if dist == 1:
|
||||||
next_step = final_target
|
next_step = final_target
|
||||||
else:
|
else:
|
||||||
return false
|
# PATHFINDING FAILED! (Likely stuck on wall/stand)
|
||||||
|
# Attempt UNSTUCK move to any adjacent valid tile
|
||||||
# Redundant safety check (simple_move_to also checks this)
|
print("[BotController] Pathfinding failed for %s. Attempting UNSTUCK move." % actor.name)
|
||||||
# Removed to allow PUSHING mechanics (simple_move_to handles occupancy/pushing)
|
return await _try_unstuck_move()
|
||||||
# if actor.is_position_occupied(next_step):
|
|
||||||
# return false
|
|
||||||
|
|
||||||
# Execute SINGLE STEP movement using player manager
|
# Execute SINGLE STEP movement using player manager
|
||||||
if actor.movement_manager.simple_move_to(next_step):
|
if actor.movement_manager.simple_move_to(next_step):
|
||||||
_is_processing_action = true
|
_is_processing_action = true
|
||||||
_current_action = "moving"
|
_current_action = "moving"
|
||||||
|
|
||||||
# Wait for movement to finish or timeout (safety)
|
|
||||||
# Race: Signal vs Timeout
|
|
||||||
# Since Godot 4 doesn't support 'await' racing easily without helper,
|
|
||||||
# we'll just wait for the signal but ensure movement manager emits it.
|
|
||||||
# safer approach: check if is_moving goes false
|
|
||||||
|
|
||||||
# Safety timeout to prevent infinite loop
|
# Safety timeout to prevent infinite loop
|
||||||
var max_wait_time = 2.0
|
var max_wait_time = 2.0
|
||||||
var elapsed = 0.0
|
var elapsed = 0.0
|
||||||
@@ -435,6 +427,37 @@ func _try_move() -> bool:
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
func _try_unstuck_move() -> bool:
|
||||||
|
"""Randomly move to ANY adjacent valid tile to escape sticky situations."""
|
||||||
|
var neighbors = enhanced_gridmap.get_neighbors(actor.current_position, 0)
|
||||||
|
neighbors.shuffle() # Randomize to avoid oscillating
|
||||||
|
|
||||||
|
for n in neighbors:
|
||||||
|
if not n.is_walkable: continue
|
||||||
|
|
||||||
|
var cell = Vector3i(n.position.x, 0, n.position.y) # Check Floor 0
|
||||||
|
var item = enhanced_gridmap.get_cell_item(cell)
|
||||||
|
|
||||||
|
# Ensure we don't walk into a wall (Item 4) or Void (-1)
|
||||||
|
# Obstacles should be checked by is_walkable but let's be sure
|
||||||
|
if item == 4 or item == -1: continue
|
||||||
|
|
||||||
|
# Attempt move
|
||||||
|
if actor.movement_manager.simple_move_to(n.position):
|
||||||
|
_is_processing_action = true
|
||||||
|
_current_action = "moving_unstuck"
|
||||||
|
print("[BotController] Unstuck move to %s" % n.position)
|
||||||
|
|
||||||
|
# Wait for move
|
||||||
|
await _wait_with_variance(action_delay)
|
||||||
|
if not is_instance_valid(self): return true
|
||||||
|
_is_processing_action = false
|
||||||
|
_current_action = "idle"
|
||||||
|
return true
|
||||||
|
|
||||||
|
print("[BotController] %s is TRULY stuck! No valid neighbors." % actor.name)
|
||||||
|
return false
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Put Tiles Back
|
# Put Tiles Back
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -77,6 +77,27 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
|||||||
if (cell_item == -1 or cell_item in enhanced_gridmap.non_walkable_items) and not is_wall_passable:
|
if (cell_item == -1 or cell_item in enhanced_gridmap.non_walkable_items) and not is_wall_passable:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
# PHYSICS CHECK: Ensure no static obstacles (like Stands) are blocking the path
|
||||||
|
# GridMap logic handles cells, but Objects/Bodies might be placed on top (like StaticTektonStand)
|
||||||
|
var space_state = player.get_world_3d().direct_space_state
|
||||||
|
# RAYCAST HEIGHT: 0.3 (Center of the 0.6m tall stand)
|
||||||
|
# Check from CENTER using +0.5
|
||||||
|
var from = Vector3(player.current_position.x + 0.5, 0.3, player.current_position.y + 0.5)
|
||||||
|
var to = Vector3(grid_position.x + 0.5, 0.3, grid_position.y + 0.5)
|
||||||
|
|
||||||
|
# Check center of target tile
|
||||||
|
var query = PhysicsRayQueryParameters3D.create(from, to)
|
||||||
|
query.collide_with_areas = false
|
||||||
|
query.collide_with_bodies = true
|
||||||
|
# query.collision_mask = 1 # Default mask usually covers static bodies
|
||||||
|
|
||||||
|
var result = space_state.intersect_ray(query)
|
||||||
|
if result:
|
||||||
|
# If we hit something static that isn't ourselves
|
||||||
|
if result.collider != player:
|
||||||
|
print("Movement Blocked by Physics Body: ", result.collider.name)
|
||||||
|
return false
|
||||||
|
|
||||||
if player.is_position_occupied(grid_position):
|
if player.is_position_occupied(grid_position):
|
||||||
var push_dir = grid_position - player.current_position
|
var push_dir = grid_position - player.current_position
|
||||||
if not try_push(grid_position, push_dir):
|
if not try_push(grid_position, push_dir):
|
||||||
|
|||||||
@@ -20,38 +20,109 @@ func calculate_spawn_points(count: int, gridmap: Node) -> Array:
|
|||||||
"""
|
"""
|
||||||
if count <= 0 or not gridmap: return []
|
if count <= 0 or not gridmap: return []
|
||||||
|
|
||||||
print("[StaticTektonManager] Calculating %d static tekton positions..." % count)
|
print("[StaticTektonManager] Calculating static tekton positions (Fixed 5-Zone + Center)...")
|
||||||
|
|
||||||
# 1. Define Zones
|
# 1. Define Zones (3x3 Grid)
|
||||||
var width = gridmap.columns
|
var width = gridmap.columns
|
||||||
var depth = gridmap.rows
|
var depth = gridmap.rows
|
||||||
|
|
||||||
# Simple 3x3 grid partition
|
|
||||||
var zone_w = width / 3
|
var zone_w = width / 3
|
||||||
var zone_d = depth / 3
|
var zone_d = depth / 3
|
||||||
|
|
||||||
var all_zones = []
|
var zones = []
|
||||||
for z in range(3):
|
for z in range(3):
|
||||||
for x in range(3):
|
for x in range(3):
|
||||||
# Create Rect2i for each zone (x, y, w, h)
|
zones.append(Rect2i(x * zone_w, z * zone_d, zone_w, zone_d))
|
||||||
var zone_rect = Rect2i(x * zone_w, z * zone_d, zone_w, zone_d)
|
|
||||||
all_zones.append(zone_rect)
|
|
||||||
|
|
||||||
# 2. Select Zones (Random Distinct)
|
# 2. Select Fixed Targets: TL(0), TR(2), Center(4), BL(6), BR(8)
|
||||||
all_zones.shuffle()
|
# This ensures they are never adjacent (always separated by a middle zone)
|
||||||
var selected_zones = all_zones.slice(0, count)
|
var target_indices = [0, 2, 6, 8, 4]
|
||||||
|
|
||||||
|
# If count < 5, we prioritize corners then center
|
||||||
|
# If count > 5, we only return 5 because that's the max safe non-adjacent set in 3x3
|
||||||
var spawn_points = []
|
var spawn_points = []
|
||||||
|
|
||||||
# 3. Pick Point in each Selected Zone
|
var iterations = min(count, target_indices.size())
|
||||||
for i in range(selected_zones.size()):
|
for i in range(iterations):
|
||||||
var zone = selected_zones[i]
|
var zone_idx = target_indices[i]
|
||||||
var pos = _pick_spot_in_zone(zone, gridmap)
|
var zone = zones[zone_idx]
|
||||||
|
|
||||||
|
# Determine Position Type for Bias
|
||||||
|
# 0:TL, 1:TR, 2:BL, 3:BR, 4:Center
|
||||||
|
var pos_type = -1
|
||||||
|
match zone_idx:
|
||||||
|
0: pos_type = 0 # TL
|
||||||
|
2: pos_type = 1 # TR
|
||||||
|
6: pos_type = 2 # BL
|
||||||
|
8: pos_type = 3 # BR
|
||||||
|
4: pos_type = 4 # Center
|
||||||
|
|
||||||
|
var pos = _pick_spot_in_zone_biased(zone, gridmap, pos_type)
|
||||||
if pos != Vector2i(-1, -1):
|
if pos != Vector2i(-1, -1):
|
||||||
spawn_points.append(pos)
|
spawn_points.append(pos)
|
||||||
|
|
||||||
return spawn_points
|
return spawn_points
|
||||||
|
|
||||||
|
func _pick_spot_in_zone_biased(zone: Rect2i, gridmap: Node, type: int) -> Vector2i:
|
||||||
|
# type: 0=TL, 1=TR, 2=BL, 3=BR, 4=Center
|
||||||
|
|
||||||
|
# ideal target relative to map
|
||||||
|
var target = Vector2i.ZERO
|
||||||
|
match type:
|
||||||
|
0: target = Vector2i(0, 0)
|
||||||
|
1: target = Vector2i(gridmap.columns, 0)
|
||||||
|
2: target = Vector2i(0, gridmap.rows)
|
||||||
|
3: target = Vector2i(gridmap.columns, gridmap.rows)
|
||||||
|
4: target = Vector2i(gridmap.columns / 2, gridmap.rows / 2)
|
||||||
|
|
||||||
|
# Clamp target to be inside valid area (taking 3x3 margin into account)
|
||||||
|
# Center of 3x3 must be at least 1 tile from edge
|
||||||
|
var min_x = max(1, zone.position.x + 1)
|
||||||
|
var max_x = min(gridmap.columns - 2, zone.position.x + zone.size.x - 2)
|
||||||
|
var min_y = max(1, zone.position.y + 1)
|
||||||
|
var max_y = min(gridmap.rows - 2, zone.position.y + zone.size.y - 2)
|
||||||
|
|
||||||
|
if min_x > max_x or min_y > max_y:
|
||||||
|
return Vector2i(-1, -1)
|
||||||
|
|
||||||
|
var clamped_target = Vector2i(
|
||||||
|
clamp(target.x, min_x, max_x),
|
||||||
|
clamp(target.y, min_y, max_y)
|
||||||
|
)
|
||||||
|
|
||||||
|
# BFS to find nearest valid 3x3 spot to clamped_target
|
||||||
|
var queue = [clamped_target]
|
||||||
|
var visited = {clamped_target: true}
|
||||||
|
|
||||||
|
# Limit search to avoid hanging
|
||||||
|
var checks = 0
|
||||||
|
while not queue.is_empty() and checks < 200:
|
||||||
|
var current = queue.pop_front()
|
||||||
|
checks += 1
|
||||||
|
|
||||||
|
if _is_valid_3x3(current, gridmap):
|
||||||
|
return current
|
||||||
|
|
||||||
|
var neighbors = [
|
||||||
|
Vector2i(0, 1), Vector2i(0, -1), Vector2i(1, 0), Vector2i(-1, 0)
|
||||||
|
]
|
||||||
|
|
||||||
|
for n in neighbors:
|
||||||
|
var next = current + n
|
||||||
|
if next.x >= min_x and next.x <= max_x and next.y >= min_y and next.y <= max_y:
|
||||||
|
if not visited.has(next):
|
||||||
|
visited[next] = true
|
||||||
|
queue.append(next)
|
||||||
|
|
||||||
|
return Vector2i(-1, -1)
|
||||||
|
|
||||||
|
func _is_valid_3x3(center: Vector2i, gridmap: Node) -> bool:
|
||||||
|
for dx in range(-1, 2):
|
||||||
|
for dy in range(-1, 2):
|
||||||
|
var check_pos = Vector3i(center.x + dx, 0, center.y + dy)
|
||||||
|
if gridmap.get_cell_item(check_pos) == -1:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
func _pick_spot_in_zone(zone: Rect2i, gridmap: Node) -> Vector2i:
|
func _pick_spot_in_zone(zone: Rect2i, gridmap: Node) -> Vector2i:
|
||||||
# Find a valid 3x3 spot in the zone
|
# Find a valid 3x3 spot in the zone
|
||||||
# The returned position is the CENTER of the 3x3 area
|
# The returned position is the CENTER of the 3x3 area
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
extends StaticBody3D
|
||||||
|
|
||||||
|
@onready var mesh_instance: MeshInstance3D = $MeshInstance3D
|
||||||
|
|
||||||
|
# Sync the chosen shape so all clients see the same one
|
||||||
|
@export var shape_index: int = -1:
|
||||||
|
set(value):
|
||||||
|
shape_index = value
|
||||||
|
if is_inside_tree():
|
||||||
|
_update_mesh_from_index()
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
if multiplayer.is_server():
|
||||||
|
# Only randomize if not already set (Main.gd sets it now)
|
||||||
|
if shape_index == -1:
|
||||||
|
shape_index = randi() % 4
|
||||||
|
_update_mesh_from_index()
|
||||||
|
else:
|
||||||
|
# Client side:
|
||||||
|
if shape_index != -1:
|
||||||
|
_update_mesh_from_index()
|
||||||
|
else:
|
||||||
|
# If we spawned but data hasn't arrived (unlikely with spawn=true but possible),
|
||||||
|
# we wait. The setter will trigger update when data arrives.
|
||||||
|
# But just in case, we can try to request it or use a default
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _update_mesh_from_index():
|
||||||
|
if not mesh_instance: return
|
||||||
|
|
||||||
|
var shapes = [
|
||||||
|
_create_cylinder(),
|
||||||
|
_create_box(),
|
||||||
|
_create_prism(),
|
||||||
|
_create_sphere()
|
||||||
|
]
|
||||||
|
|
||||||
|
var idx = shape_index % shapes.size()
|
||||||
|
var selected_mesh = shapes[idx]
|
||||||
|
|
||||||
|
# Apply Material
|
||||||
|
var mat = StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = Color(0.2, 0.2, 0.25)
|
||||||
|
mat.metallic = 0.5
|
||||||
|
mat.roughness = 0.5
|
||||||
|
selected_mesh.material = mat
|
||||||
|
|
||||||
|
mesh_instance.mesh = selected_mesh
|
||||||
|
|
||||||
|
# Deprecated: _randomize_shape (Logic moved to server init and sync)
|
||||||
|
# func _randomize_shape(): ...
|
||||||
|
|
||||||
|
func _create_cylinder() -> CylinderMesh:
|
||||||
|
var mesh = CylinderMesh.new()
|
||||||
|
mesh.top_radius = 1.4
|
||||||
|
mesh.bottom_radius = 1.4
|
||||||
|
mesh.height = 0.6
|
||||||
|
return mesh
|
||||||
|
|
||||||
|
func _create_box() -> BoxMesh:
|
||||||
|
var mesh = BoxMesh.new()
|
||||||
|
mesh.size = Vector3(3.2, 0.6, 3.2)
|
||||||
|
return mesh
|
||||||
|
|
||||||
|
func _create_prism() -> PrismMesh:
|
||||||
|
var mesh = PrismMesh.new()
|
||||||
|
mesh.size = Vector3(3.2, 0.6, 3.2)
|
||||||
|
return mesh
|
||||||
|
|
||||||
|
func _create_sphere() -> SphereMesh:
|
||||||
|
# A flattened sphere acting like a dome stand
|
||||||
|
var mesh = SphereMesh.new()
|
||||||
|
mesh.radius = 1.4
|
||||||
|
mesh.height = 1.0 # Flattened
|
||||||
|
mesh.is_hemisphere = true
|
||||||
|
return mesh
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dny2mjwk1vo0u
|
||||||
@@ -48,9 +48,34 @@ func move_to(target_pos: Vector2i):
|
|||||||
if is_moving or is_carried or is_thrown: return
|
if is_moving or is_carried or is_thrown: return
|
||||||
|
|
||||||
# Validate
|
# Validate
|
||||||
|
# Validate Grid
|
||||||
if not enhanced_gridmap.is_position_valid(target_pos):
|
if not enhanced_gridmap.is_position_valid(target_pos):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Validate Physics (Block on Stands)
|
||||||
|
var space_state = get_world_3d().direct_space_state
|
||||||
|
# Tekton is slightly offset, so query center of its current and target tiles
|
||||||
|
# Just checking TARGET center is usually enough to stop entering a blocked tile
|
||||||
|
# RAY HEIGHT: 0.3 to hit the 0.6m tall stand
|
||||||
|
var from = Vector3(target_pos.x + 0.5, 0.3, target_pos.y + 0.5) + Vector3.UP * 5.0
|
||||||
|
var to = Vector3(target_pos.x + 0.5, 0.3, target_pos.y + 0.5) + Vector3.DOWN * 5.0
|
||||||
|
|
||||||
|
# Raycast VERTICALLY at target to see if it's occupied by a Stand
|
||||||
|
# Actually, vertical ray from high up is fine IF it goes low enough.
|
||||||
|
# But let's check intersection at body height to be sure.
|
||||||
|
from = Vector3(target_pos.x + 0.5, 5.0, target_pos.y + 0.5)
|
||||||
|
to = Vector3(target_pos.x + 0.5, 0.1, target_pos.y + 0.5) # Go almost to floor
|
||||||
|
var query = PhysicsRayQueryParameters3D.create(from, to)
|
||||||
|
query.collide_with_areas = false
|
||||||
|
query.collide_with_bodies = true
|
||||||
|
|
||||||
|
var result = space_state.intersect_ray(query)
|
||||||
|
if result:
|
||||||
|
# If we hit a StaticTektonStand (or any other static blocking body)
|
||||||
|
if result.collider != self:
|
||||||
|
# print("Tekton movement blocked by: ", result.collider.name)
|
||||||
|
return
|
||||||
|
|
||||||
is_moving = true
|
is_moving = true
|
||||||
|
|
||||||
var floor_y = 0.05
|
var floor_y = 0.05
|
||||||
|
|||||||
Reference in New Issue
Block a user