feat: update

This commit is contained in:
2026-04-28 01:22:38 +08:00
parent b76dd2e737
commit 1585b91509
9 changed files with 529 additions and 229 deletions
+267 -85
View File
@@ -21,6 +21,8 @@ var vfx_manager
# Minimal local state
var _connection_check_timer: float = 0.0
var reserved_static_positions: Array[Vector2i] = []
var _unstuck_cooldown_remaining: float = 0.0
const UNSTUCK_COOLDOWN = 120.0 # 2 minutes
func _can_rpc() -> bool:
if not is_inside_tree(): return false
@@ -523,6 +525,19 @@ func _process(delta):
if not is_inside_tree(): return
if not check_multiplayer(): return
# Tick down unstuck cooldown and update button label
if _unstuck_cooldown_remaining > 0.0:
_unstuck_cooldown_remaining -= delta
var unstuck_btn = get_node_or_null("PauseMenu/Panel/VBox/UnstuckBtn")
if unstuck_btn:
if _unstuck_cooldown_remaining > 0.0:
unstuck_btn.text = "Unstuck (%ds)" % ceil(_unstuck_cooldown_remaining)
unstuck_btn.disabled = true
else:
_unstuck_cooldown_remaining = 0.0
unstuck_btn.text = "Unstuck"
unstuck_btn.disabled = false
if ui_manager and get_tree():
var all_players = get_tree().get_nodes_in_group("Players")
if all_players.size() > 0:
@@ -849,17 +864,91 @@ func _start_game():
spawn_tekton_npc()
# =============================================================================
# Spawn Zone System - Prevents edge gaps with 1-tile perimeter buffer
# =============================================================================
const PERIMETER_BUFFER = 1 # 1-tile safe zone on all sides
enum SpawnZone {
TOP_LEFT_CORNER,
TOP_CENTER,
TOP_RIGHT_CORNER,
MIDDLE_LEFT,
MIDDLE_CENTER,
MIDDLE_RIGHT,
BOTTOM_LEFT_CORNER,
BOTTOM_CENTER,
BOTTOM_RIGHT_CORNER
}
func _get_spawn_zones(gridmap: Node) -> Dictionary:
"""Returns a dictionary of spawn zones based on 3x3 grid layout.
Corner zones are for Tektons, middle zones are for players."""
var width = gridmap.columns
var height = gridmap.rows
# Apply perimeter buffer
var safe_width = width - (PERIMETER_BUFFER * 2)
var safe_height = height - (PERIMETER_BUFFER * 2)
# Divide safe area into 3x3 grid
var zone_w = safe_width / 3
var zone_h = safe_height / 3
var zones = {}
# Define 9 zones with buffer offsets
for row in range(3):
for col in range(3):
var zone_idx = row * 3 + col
var zone_rect = Rect2i(
PERIMETER_BUFFER + (col * zone_w),
PERIMETER_BUFFER + (row * zone_h),
zone_w,
zone_h
)
zones[zone_idx] = zone_rect
return zones
func _is_position_in_zone(pos: Vector2i, zone: Rect2i) -> bool:
"""Check if position is within a spawn zone."""
return zone.has_point(pos)
func _get_tekton_spawn_zones(zones: Dictionary) -> Array:
"""Returns corner zones for Tekton spawning."""
return [
zones[SpawnZone.TOP_LEFT_CORNER],
zones[SpawnZone.TOP_RIGHT_CORNER],
zones[SpawnZone.BOTTOM_LEFT_CORNER],
zones[SpawnZone.BOTTOM_RIGHT_CORNER]
]
func _get_player_spawn_zones(zones: Dictionary) -> Array:
"""Returns middle zones for player spawning."""
return [
zones[SpawnZone.TOP_CENTER],
zones[SpawnZone.MIDDLE_LEFT],
zones[SpawnZone.MIDDLE_CENTER],
zones[SpawnZone.MIDDLE_RIGHT],
zones[SpawnZone.BOTTOM_CENTER]
]
func _assign_random_spawn_positions():
"""Assign spawn positions distributed to 4 corners (2 per corner for 8 players)."""
"""Assign spawn positions distributed across middle zones (avoiding corners reserved for Tektons)."""
var enhanced_gridmap = $EnhancedGridMap
if not enhanced_gridmap:
return
# Lists for each quadrant
var spawns_TL = [] # Top-Left
var spawns_TR = [] # Top-Right
var spawns_BL = [] # Bottom-Left
var spawns_BR = [] # Bottom-Right
# Get spawn zones with perimeter buffer
var spawn_zones = _get_spawn_zones(enhanced_gridmap)
var player_zones = _get_player_spawn_zones(spawn_zones)
# Lists for player spawns in each zone
var spawns_by_zone = {} # zone_rect -> [positions]
for zone in player_zones:
spawns_by_zone[zone] = []
var all_spawns = [] # Fallback
# Stop n Go Custom Spawn Logic
@@ -874,23 +963,20 @@ func _assign_random_spawn_positions():
_assign_portal_mode_spawn_positions(all_players)
return
var mid_x = enhanced_gridmap.columns / 2
var mid_z = enhanced_gridmap.rows / 2
# If static positions were not calculated yet, do it now to avoid players spawning in them
if reserved_static_positions.is_empty() and LobbyManager.game_mode != "Stop n Go":
if not static_tekton_manager:
static_tekton_manager = preload("res://scripts/managers/static_tekton_manager.gd").new()
reserved_static_positions = static_tekton_manager.calculate_spawn_points(3, enhanced_gridmap)
for x in range(enhanced_gridmap.columns):
for z in range(enhanced_gridmap.rows):
# Scan grid for walkable positions within player zones (respecting buffer)
for x in range(PERIMETER_BUFFER, enhanced_gridmap.columns - PERIMETER_BUFFER):
for z in range(PERIMETER_BUFFER, enhanced_gridmap.rows - PERIMETER_BUFFER):
var ground = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
if ground == 0: # Walkable
var pos = Vector2i(x, z)
# SAFETY CHECK: Is this reserved for a Static Tekton Stand?
# Stand clears exactly 3x3 area
var is_safe = true
for reserved in reserved_static_positions:
if abs(x - reserved.x) <= 1 and abs(z - reserved.y) <= 1:
@@ -900,32 +986,21 @@ func _assign_random_spawn_positions():
if not is_safe:
continue
all_spawns.append(pos)
# Check if position is in any player zone
var in_player_zone = false
for zone in player_zones:
if _is_position_in_zone(pos, zone):
spawns_by_zone[zone].append(pos)
in_player_zone = true
break
if x < mid_x and z < mid_z:
spawns_TL.append(pos)
elif x >= mid_x and z < mid_z:
spawns_TR.append(pos)
elif x < mid_x and z >= mid_z:
spawns_BL.append(pos)
else:
spawns_BR.append(pos)
# Add to fallback list regardless of zone
if in_player_zone:
all_spawns.append(pos)
# Sort lists by distance to corners (closest to corner should be last, to be popped first)
# TL: Close to (0,0) -> Sort descending distance (so closest is at end)
spawns_TL.sort_custom(func(a, b): return a.length_squared() > b.length_squared())
# TR: Close to (13, 0)
var tr_corner = Vector2i(enhanced_gridmap.columns - 1, 0)
spawns_TR.sort_custom(func(a, b): return a.distance_squared_to(tr_corner) > b.distance_squared_to(tr_corner))
# BL: Close to (0, 13)
var bl_corner = Vector2i(0, enhanced_gridmap.rows - 1)
spawns_BL.sort_custom(func(a, b): return a.distance_squared_to(bl_corner) > b.distance_squared_to(bl_corner))
# BR: Close to (13, 13)
var br_corner = Vector2i(enhanced_gridmap.columns - 1, enhanced_gridmap.rows - 1)
spawns_BR.sort_custom(func(a, b): return a.distance_squared_to(br_corner) > b.distance_squared_to(br_corner))
# Shuffle each zone's spawn list for randomization
for zone in spawns_by_zone:
spawns_by_zone[zone].shuffle()
# Fallback shuffle
all_spawns.shuffle()
@@ -936,24 +1011,24 @@ func _assign_random_spawn_positions():
var spawn_index = 0
# Round-robin assignment to corners: TL, TR, BR, BL, TL, TR, BR, BL...
# Order: TL -> TR -> BR -> BL (Clockwise-ish)
var quadrants = [spawns_TL, spawns_TR, spawns_BR, spawns_BL]
# Round-robin assignment across player zones
var zone_list = spawns_by_zone.keys()
var zone_arrays = spawns_by_zone.values()
for player in all_players:
var assigned_pos = Vector2i(-1, -1)
# Try to get from the current quadrant
var quadrant_idx = spawn_index % 4
var quadrant = quadrants[quadrant_idx]
# Try to get from the current zone (round-robin)
var zone_idx = spawn_index % zone_arrays.size()
var zone_spawns = zone_arrays[zone_idx]
if quadrant.size() > 0:
assigned_pos = quadrant.pop_back()
if zone_spawns.size() > 0:
assigned_pos = zone_spawns.pop_back()
else:
# Fallback: Try other quadrants if preferred one is empty
for q in quadrants:
if q.size() > 0:
assigned_pos = q.pop_back()
# Fallback: Try other zones if preferred one is empty
for zone_arr in zone_arrays:
if zone_arr.size() > 0:
assigned_pos = zone_arr.pop_back()
break
# Ultimate fallback: Random from anywhere
@@ -1091,51 +1166,54 @@ const StaticTektonManager = preload("res://scripts/managers/static_tekton_manage
var static_tekton_manager
func spawn_tekton_npc():
"""Spawn a Tekton NPC at a random location."""
"""Spawn Tektons in corner zones only (avoiding player spawn areas)."""
if not multiplayer.is_server(): return
# Find random valid position
var enhanced_gridmap = $EnhancedGridMap
if not enhanced_gridmap: return
# Spawn 3 Roaming Tektons
var spawned_count = 0
var attempts = 0
# Get corner zones for Tekton spawning
var spawn_zones = _get_spawn_zones(enhanced_gridmap)
var tekton_zones = _get_tekton_spawn_zones(spawn_zones)
while spawned_count < 3 and attempts < 50:
attempts += 1
# Collect valid spawn positions in corner zones
var valid_positions = []
for zone in tekton_zones:
for x in range(zone.position.x, zone.position.x + zone.size.x):
for y in range(zone.position.y, zone.position.y + zone.size.y):
var cell = Vector3i(x, 0, y)
if enhanced_gridmap.get_cell_item(cell) == 0: # Walkable floor
# Ensure not occupied by static tekton stand
var item_id = enhanced_gridmap.get_cell_item(Vector3i(x, 1, y))
if item_id == 4: continue # Wall/Stand
# Check RESERVED positions (static tekton stands)
var is_safe = true
for reserved in reserved_static_positions:
if abs(x - reserved.x) <= 1 and abs(y - reserved.y) <= 1:
is_safe = false
break
if not is_safe: continue
valid_positions.append(Vector2i(x, y))
# Shuffle and spawn 3 Roaming Tektons
valid_positions.shuffle()
var spawned_count = 0
for pos in valid_positions:
if spawned_count >= 3: break
# Find random valid position
var valid_pos = Vector2i(-1, -1)
var x = randi() % enhanced_gridmap.columns
var y = randi() % enhanced_gridmap.rows
var cell = Vector3i(x, 0, y)
# Generate a consistent ID/Name for sync
var tekton_id = Time.get_ticks_msec() + spawned_count
_create_tekton(pos, tekton_id)
# Check if walkable and no existing Tekton nearby?
if enhanced_gridmap.get_cell_item(cell) == 0: # Walkable floor
# Ensure not occupied by static tekton stand (Item 4)
var item_id = enhanced_gridmap.get_cell_item(Vector3i(x, 1, y))
if item_id == 4: continue # Wall/Stand
# Also check RESERVED positions (if they haven't spawned yet or for safety)
var is_safe = true
for reserved in reserved_static_positions:
if abs(x - reserved.x) <= 1 and abs(y - reserved.y) <= 1:
is_safe = false
break
if not is_safe: continue
valid_pos = Vector2i(x, y)
# Generate a consistent ID/Name for sync (add index to ensure uniqueness)
var tekton_id = Time.get_ticks_msec() + spawned_count
_create_tekton(valid_pos, tekton_id)
# Only broadcast to clients if there are remote peers connected
if can_rpc() and multiplayer.get_peers().size() > 0:
rpc("sync_spawn_tekton", valid_pos, tekton_id)
spawned_count += 1
print("[Main] Spawned Tekton %d at %s" % [spawned_count, valid_pos])
# Only broadcast to clients if there are remote peers connected
if can_rpc() and multiplayer.get_peers().size() > 0:
rpc("sync_spawn_tekton", pos, tekton_id)
spawned_count += 1
print("[Main] Spawned Tekton %d at %s (Corner Zone)" % [spawned_count, pos])
@rpc("call_remote", "reliable")
func sync_spawn_tekton(pos: Vector2i, tekton_id: int):
@@ -2527,6 +2605,110 @@ func _on_settings_back_pressed():
if pause_menu:
pause_menu.visible = true
func _on_unstuck_pressed():
"""Teleport the local player to a safe spawn position when stuck."""
if _unstuck_cooldown_remaining > 0.0:
print("[Unstuck] On cooldown: %.0fs remaining" % _unstuck_cooldown_remaining)
return
var enhanced_gridmap = $EnhancedGridMap
if not enhanced_gridmap:
print("[Unstuck] No gridmap found")
return
# Find the local player
var local_player = null
var all_players = get_tree().get_nodes_in_group("Players")
for player in all_players:
if player.is_multiplayer_authority():
local_player = player
break
if not local_player:
print("[Unstuck] No local player found")
return
# Find a safe spawn position using the spawn zone system
var safe_pos = _find_safe_spawn_position(enhanced_gridmap, local_player)
if safe_pos == Vector2i(-1, -1):
print("[Unstuck] Failed to find safe position")
return
# Teleport the player to the safe position
local_player.current_position = safe_pos
local_player.position = local_player.grid_to_world(safe_pos)
local_player.is_player_moving = false
# Sync the new position to all clients
if local_player.has_method("rpc"):
local_player.rpc("sync_position", safe_pos)
print("[Unstuck] Teleported player to safe position: %s" % safe_pos)
# Start cooldown
_unstuck_cooldown_remaining = UNSTUCK_COOLDOWN
var unstuck_btn = get_node_or_null("PauseMenu/Panel/VBox/UnstuckBtn")
if unstuck_btn:
unstuck_btn.disabled = true
# Close the pause menu
var pause_menu = get_node_or_null("PauseMenu")
if pause_menu:
pause_menu.visible = false
func _find_safe_spawn_position(gridmap: Node, player: Node) -> Vector2i:
"""Find a safe spawn position using the existing spawn zone system.
Prioritizes player zones but will fall back to any walkable position."""
# Get spawn zones with perimeter buffer
var spawn_zones = _get_spawn_zones(gridmap)
var player_zones = _get_player_spawn_zones(spawn_zones)
# Collect valid positions from player zones
var valid_positions = []
for zone in player_zones:
for x in range(zone.position.x, zone.position.x + zone.size.x):
for z in range(zone.position.y, zone.position.y + zone.size.y):
var cell = Vector3i(x, 0, z)
if gridmap.get_cell_item(cell) == 0: # Walkable floor
# Check for obstacles on layer 1
var layer1_item = gridmap.get_cell_item(Vector3i(x, 1, z))
if layer1_item == -1 or layer1_item in [7, 8, 9, 10, 11, 12, 13, 14]: # Empty or pickable tiles
# Check if position is not occupied by another player
var occupied = false
var all_players = get_tree().get_nodes_in_group("Players")
for p in all_players:
if p != player and p.current_position == Vector2i(x, z):
occupied = true
break
if not occupied:
valid_positions.append(Vector2i(x, z))
# If we found valid positions, pick a random one
if valid_positions.size() > 0:
valid_positions.shuffle()
return valid_positions[0]
# Fallback: Search the entire grid with buffer for ANY walkable position
for x in range(PERIMETER_BUFFER, gridmap.columns - PERIMETER_BUFFER):
for z in range(PERIMETER_BUFFER, gridmap.rows - PERIMETER_BUFFER):
var cell = Vector3i(x, 0, z)
if gridmap.get_cell_item(cell) == 0: # Walkable
var layer1_item = gridmap.get_cell_item(Vector3i(x, 1, z))
if layer1_item == -1 or layer1_item in [7, 8, 9, 10, 11, 12, 13, 14]:
var occupied = false
var all_players = get_tree().get_nodes_in_group("Players")
for p in all_players:
if p != player and p.current_position == Vector2i(x, z):
occupied = true
break
if not occupied:
return Vector2i(x, z)
# Ultimate fallback: center of the map
return Vector2i(gridmap.columns / 2, gridmap.rows / 2)
func _on_button_size_changed(value: float):
if touch_controls:
touch_controls.button_size = value