experimental: remove Tekton Doors entirely
- Delete portal_mode_manager.gd, portal_door.gd, portal_door.tscn - Strip all Tekton Doors logic from main.gd, player.gd, lobby.gd, lobby_room.gd, lobby_manager.gd, camera_context_manager.gd, music_manager.gd, tekton.gd, enhanced_gridmap.gd, playerboard_manager.gd, special_tiles_manager.gd - Remove TK enum (TEKTON_DOORS=2), mode_config schema, arena area - Update tests: 3 modes instead of 4 - Strip HowToPlay tab from main.tscn
This commit is contained in:
+4
-14
@@ -1,12 +1,7 @@
|
||||
extends RefCounted
|
||||
class_name GameMode
|
||||
|
||||
enum Mode {
|
||||
FREEMODE = 0,
|
||||
STOP_N_GO = 1,
|
||||
TEKTON_DOORS = 2,
|
||||
GAUNTLET = 3
|
||||
}
|
||||
enum Mode { FREEMODE = 0, STOP_N_GO = 1, GAUNTLET = 2 }
|
||||
|
||||
static func from_string(mode: String) -> Mode:
|
||||
match mode:
|
||||
@@ -14,12 +9,9 @@ static func from_string(mode: String) -> Mode:
|
||||
return Mode.FREEMODE
|
||||
"Stop n Go":
|
||||
return Mode.STOP_N_GO
|
||||
"Tekton Doors":
|
||||
return Mode.TEKTON_DOORS
|
||||
"Candy Pump Survival":
|
||||
return Mode.GAUNTLET
|
||||
_:
|
||||
return Mode.FREEMODE
|
||||
return Mode.FREEMODE
|
||||
|
||||
static func mode_to_string(mode: Mode) -> String:
|
||||
match mode:
|
||||
@@ -27,15 +19,13 @@ static func mode_to_string(mode: Mode) -> String:
|
||||
return "Freemode"
|
||||
Mode.STOP_N_GO:
|
||||
return "Stop n Go"
|
||||
Mode.TEKTON_DOORS:
|
||||
return "Tekton Doors"
|
||||
Mode.GAUNTLET:
|
||||
return "Candy Pump Survival"
|
||||
_:
|
||||
return "Freemode"
|
||||
|
||||
static func is_restricted(mode: Mode) -> bool:
|
||||
return mode == Mode.STOP_N_GO or mode == Mode.TEKTON_DOORS or mode == Mode.GAUNTLET
|
||||
return mode == Mode.STOP_N_GO or mode == Mode.GAUNTLET
|
||||
|
||||
static func get_all_modes() -> Array[String]:
|
||||
return ["Freemode", "Stop n Go", "Tekton Doors", "Candy Pump Survival"]
|
||||
return ["Freemode", "Stop n Go", "Candy Pump Survival"]
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://cosvmn3gh5bxj
|
||||
@@ -52,9 +52,6 @@ func _calculate_target_position() -> Vector3:
|
||||
bounds = bounds_gauntlet
|
||||
elif mode == GameMode.Mode.STOP_N_GO:
|
||||
bounds = bounds_stop_n_go
|
||||
elif mode == GameMode.Mode.TEKTON_DOORS:
|
||||
bounds = bounds_doors
|
||||
target_y = 32.3 # Doors uses a higher overlook
|
||||
|
||||
# Clamp X and Z
|
||||
target_x = clamp(target_x, bounds.min_x, bounds.max_x)
|
||||
|
||||
@@ -2,7 +2,7 @@ extends Node
|
||||
class_name GauntletManager
|
||||
|
||||
# GauntletManager - Handles Candy Pump Survival (Gauntlet) game mode
|
||||
# Pattern: StopNGoManager + PortalModeManager
|
||||
# Pattern: StopNGoManager + GauntletManager
|
||||
|
||||
signal phase_changed(phase_index: int, phase_name: String)
|
||||
signal growth_tick(cells: Array)
|
||||
|
||||
@@ -62,8 +62,6 @@ func mark_goal_complete(player_id: int):
|
||||
player_completion_times[player_id].append(duration_sec)
|
||||
# Reset start time for next goal
|
||||
player_start_times[player_id] = Time.get_ticks_msec()
|
||||
|
||||
# print("Player %s completed goal in %.2fs" % [player_id, duration_sec])
|
||||
|
||||
func get_player_average_time(player_id: int) -> float:
|
||||
if not player_completion_times.has(player_id) or player_completion_times[player_id].is_empty():
|
||||
@@ -92,7 +90,7 @@ func get_boost_multiplier(player_id: int) -> float:
|
||||
var p_avg = get_player_average_time(player_id)
|
||||
var g_avg = get_global_average_time()
|
||||
|
||||
if p_avg > g_avg:
|
||||
if p_avg > g_avg:
|
||||
# Player is slower than average -> Boost fills faster
|
||||
# Scale up to 1.5x based on how much slower (capped)
|
||||
var ratio = p_avg / max(g_avg, 0.1)
|
||||
|
||||
@@ -26,11 +26,6 @@ signal sng_go_duration_changed(duration: int)
|
||||
signal sng_stop_duration_changed(duration: int)
|
||||
signal sng_required_goals_changed(goals: int)
|
||||
|
||||
# Tekton Doors settings signals
|
||||
signal doors_swap_time_changed(time: int)
|
||||
signal doors_refresh_time_changed(time: int)
|
||||
signal doors_required_goals_changed(goals: int)
|
||||
|
||||
# Gauntlet settings signals
|
||||
signal gauntlet_round_duration_changed(duration: int)
|
||||
signal gauntlet_growth_interval_changed(interval: float)
|
||||
@@ -74,11 +69,6 @@ var sng_go_duration: int = 20
|
||||
var sng_stop_duration: int = 4
|
||||
var sng_required_goals: int = 8
|
||||
|
||||
# Tekton Doors settings
|
||||
var doors_swap_time: int = 15
|
||||
var doors_refresh_time: int = 25
|
||||
var doors_required_goals: int = 8
|
||||
|
||||
# Gauntlet settings
|
||||
var gauntlet_round_duration: int = 180
|
||||
var gauntlet_growth_interval: float = 3.0 # seconds between growth ticks
|
||||
@@ -522,37 +512,6 @@ func sync_sng_required_goals(goals: int) -> void:
|
||||
sng_required_goals = goals
|
||||
emit_signal("sng_required_goals_changed", goals)
|
||||
|
||||
# =============================================================================
|
||||
# Tekton Doors Settings
|
||||
# =============================================================================
|
||||
|
||||
func set_doors_swap_time(time: int) -> void:
|
||||
doors_swap_time = time
|
||||
if is_host: rpc("sync_doors_swap_time", time)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_doors_swap_time(time: int) -> void:
|
||||
doors_swap_time = time
|
||||
emit_signal("doors_swap_time_changed", time)
|
||||
|
||||
func set_doors_refresh_time(time: int) -> void:
|
||||
doors_refresh_time = time
|
||||
if is_host: rpc("sync_doors_refresh_time", time)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_doors_refresh_time(time: int) -> void:
|
||||
doors_refresh_time = time
|
||||
emit_signal("doors_refresh_time_changed", time)
|
||||
|
||||
func set_doors_required_goals(goals: int) -> void:
|
||||
doors_required_goals = goals
|
||||
if is_host: rpc("sync_doors_required_goals", goals)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_doors_required_goals(goals: int) -> void:
|
||||
doors_required_goals = goals
|
||||
emit_signal("doors_required_goals_changed", goals)
|
||||
|
||||
# =============================================================================
|
||||
# Gauntlet Settings
|
||||
# =============================================================================
|
||||
@@ -740,8 +699,6 @@ func set_game_mode(mode: String) -> void:
|
||||
set_area("Free Mode Area")
|
||||
elif mode == "Stop n Go" and "Stop n Go Area" in available_areas:
|
||||
set_area("Stop n Go Area")
|
||||
elif mode == "Tekton Doors" and "Tekton Doors Area" in available_areas:
|
||||
set_area("Tekton Doors Area")
|
||||
elif mode == "Candy Pump Survival" and "Gauntlet Arena" in available_areas:
|
||||
set_area("Gauntlet Arena")
|
||||
|
||||
@@ -756,8 +713,6 @@ func sync_game_mode(mode: String) -> void:
|
||||
selected_area = "Free Mode Area"
|
||||
elif mode == "Stop n Go" and "Stop n Go Area" in available_areas:
|
||||
selected_area = "Stop n Go Area"
|
||||
elif mode == "Tekton Doors" and "Tekton Doors Area" in available_areas:
|
||||
selected_area = "Tekton Doors Area"
|
||||
elif mode == "Candy Pump Survival" and "Gauntlet Arena" in available_areas:
|
||||
selected_area = "Gauntlet Arena"
|
||||
elif selected_area not in available_areas:
|
||||
@@ -785,9 +740,6 @@ func start_game(force: bool = false) -> void:
|
||||
rpc("sync_sng_go_duration", sng_go_duration)
|
||||
rpc("sync_sng_stop_duration", sng_stop_duration)
|
||||
rpc("sync_sng_required_goals", sng_required_goals)
|
||||
rpc("sync_doors_swap_time", doors_swap_time)
|
||||
rpc("sync_doors_refresh_time", doors_refresh_time)
|
||||
rpc("sync_doors_required_goals", doors_required_goals)
|
||||
# Sync gauntlet settings
|
||||
rpc("sync_gauntlet_round_duration", gauntlet_round_duration)
|
||||
rpc("sync_gauntlet_growth_interval", gauntlet_growth_interval)
|
||||
@@ -864,9 +816,6 @@ func request_room_info(requester_id: int, requester_name: String, requester_char
|
||||
rpc_id(requester_id, "sync_sng_go_duration", sng_go_duration)
|
||||
rpc_id(requester_id, "sync_sng_stop_duration", sng_stop_duration)
|
||||
rpc_id(requester_id, "sync_sng_required_goals", sng_required_goals)
|
||||
rpc_id(requester_id, "sync_doors_swap_time", doors_swap_time)
|
||||
rpc_id(requester_id, "sync_doors_refresh_time", doors_refresh_time)
|
||||
rpc_id(requester_id, "sync_doors_required_goals", doors_required_goals)
|
||||
rpc_id(requester_id, "sync_gauntlet_round_duration", gauntlet_round_duration)
|
||||
rpc_id(requester_id, "sync_gauntlet_growth_interval", gauntlet_growth_interval)
|
||||
rpc_id(requester_id, "sync_gauntlet_cells_per_tick", gauntlet_cells_per_tick)
|
||||
@@ -1018,6 +967,3 @@ func reset() -> void:
|
||||
sng_go_duration = 20
|
||||
sng_stop_duration = 4
|
||||
sng_required_goals = 8
|
||||
doors_swap_time = 15
|
||||
doors_refresh_time = 25
|
||||
doors_required_goals = 8
|
||||
|
||||
@@ -47,7 +47,7 @@ func start_music():
|
||||
match game_mode:
|
||||
"Stop n Go":
|
||||
track_path = "res://assets/sounds/stop_n_go.wav"
|
||||
"Freemode", "Tekton Doors", _:
|
||||
"Freemode", _:
|
||||
track_path = "res://assets/sounds/level_bridge.wav"
|
||||
|
||||
play_track(track_path)
|
||||
|
||||
@@ -230,10 +230,6 @@ func _check_and_refill_grid_if_needed(server_gridmap: Node):
|
||||
break
|
||||
|
||||
if not has_items:
|
||||
if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS):
|
||||
# Tekton Doors handles its own wall-aware refill in PortalModeManager
|
||||
return
|
||||
|
||||
print("[PlayerboardManager] Floor 1 empty! Respawning tiles with Scarcity...")
|
||||
# Call randomize_floor on floor 1 using ScarcityController
|
||||
# ScarcityController is a global class, so we can pass its static function as a Callable
|
||||
@@ -372,18 +368,7 @@ func auto_put_item() -> bool:
|
||||
var pos = neighbor.position
|
||||
var cell_3d = Vector3i(pos.x, 1, pos.y)
|
||||
if enhanced_gridmap.get_cell_item(cell_3d) == -1 and not player.is_position_occupied(pos):
|
||||
# TEKTON DOORS: Avoid portal doors
|
||||
var is_on_portal = false
|
||||
if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS):
|
||||
var doors = get_tree().get_nodes_in_group("PortalDoors")
|
||||
for door in doors:
|
||||
var door_grid = enhanced_gridmap.local_to_map(enhanced_gridmap.to_local(door.global_position))
|
||||
if Vector2i(door_grid.x, door_grid.z) == pos:
|
||||
is_on_portal = true
|
||||
break
|
||||
|
||||
if not is_on_portal:
|
||||
valid_put_positions.append(pos)
|
||||
valid_put_positions.append(pos)
|
||||
|
||||
if valid_put_positions.is_empty():
|
||||
return false
|
||||
|
||||
@@ -1,585 +0,0 @@
|
||||
extends Node
|
||||
|
||||
# PortalModeManager - Handles "Tekton Doors" mode logic
|
||||
# Manages room partitioning, portal connections, and mode-specific timers.
|
||||
|
||||
var main: Node
|
||||
var gridmap: Node
|
||||
|
||||
# Room layout config
|
||||
const ROOM_COUNT = 4
|
||||
const GRID_SIZE = 14
|
||||
const ROOM_DIM = 7
|
||||
|
||||
# State
|
||||
var connections = {} # room_id -> {door_id -> {target_room, target_door}}
|
||||
var doors = [] # List of PortalDoor nodes
|
||||
var swap_timer: Timer
|
||||
var tile_refresh_timer: Timer
|
||||
var finish_spawned: bool = false
|
||||
var arena_setup_done: bool = false
|
||||
var player_portal_cooldowns: Dictionary = {}
|
||||
|
||||
var hud_layer: CanvasLayer
|
||||
var mission_label: Label
|
||||
var _has_notified_mission_complete: bool = false
|
||||
|
||||
func initialize(p_main: Node, p_gridmap: Node):
|
||||
main = p_main
|
||||
gridmap = p_gridmap
|
||||
|
||||
if gridmap:
|
||||
# Ensure walls (4) are strictly treated as non-walkable for all internal checks
|
||||
# Use explicit type to avoid Array vs Array[int] mismatch error
|
||||
var non_walkable: Array[int] = [4]
|
||||
gridmap.non_walkable_items = non_walkable
|
||||
|
||||
# Create Stands container if it doesn't exist
|
||||
print("[PortalModeManager] Initialized")
|
||||
|
||||
# Connection Swap Timer (15s)
|
||||
swap_timer = Timer.new()
|
||||
swap_timer.name = "PortalSwapTimer"
|
||||
# Initial wait time; gets reset when started based on game mode settings
|
||||
swap_timer.wait_time = 15.0
|
||||
swap_timer.timeout.connect(_on_swap_timer_timeout)
|
||||
add_child(swap_timer)
|
||||
|
||||
# Tile Refresh Timer (25s)
|
||||
tile_refresh_timer = Timer.new()
|
||||
tile_refresh_timer.name = "TileRefreshTimer"
|
||||
# Initial wait time; gets reset when started based on game mode settings
|
||||
tile_refresh_timer.wait_time = 25.0
|
||||
tile_refresh_timer.timeout.connect(_on_tile_refresh_timer_timeout)
|
||||
add_child(tile_refresh_timer)
|
||||
|
||||
# Connect to mission tracking
|
||||
var gcm = main.get_node_or_null("GoalsCycleManager")
|
||||
if gcm:
|
||||
gcm.global_timer_updated.connect(_on_global_timer_updated)
|
||||
gcm.goal_count_updated.connect(_on_goal_count_updated)
|
||||
|
||||
_setup_hud()
|
||||
|
||||
func _on_global_timer_updated(time_remaining: float):
|
||||
if not multiplayer.is_server(): return
|
||||
|
||||
# Last 30 seconds: Reveal Finish Room
|
||||
if time_remaining <= 30.0 and not finish_spawned:
|
||||
_spawn_finish_room()
|
||||
|
||||
func start_game_mode():
|
||||
if not multiplayer.is_server(): return
|
||||
|
||||
if arena_setup_done and not doors.is_empty():
|
||||
print("[PortalModeManager] Arena already setup, starting timers and refresh only.")
|
||||
else:
|
||||
print("[PortalModeManager] Starting Portal Game Mode with full setup...")
|
||||
setup_arena_locally()
|
||||
_randomize_connections()
|
||||
|
||||
# Configure dynamic timings from LobbyManager before starting
|
||||
swap_timer.wait_time = float(LobbyManager.doors_swap_time)
|
||||
tile_refresh_timer.wait_time = float(LobbyManager.doors_refresh_time)
|
||||
|
||||
# Start Timers
|
||||
if swap_timer.is_stopped():
|
||||
swap_timer.start()
|
||||
if tile_refresh_timer.is_stopped():
|
||||
tile_refresh_timer.start()
|
||||
|
||||
# Initial Tile Spawn
|
||||
_refresh_tiles()
|
||||
|
||||
# Show HUD
|
||||
_activate_hud()
|
||||
|
||||
func _activate_hud():
|
||||
if hud_layer:
|
||||
hud_layer.visible = true
|
||||
_update_hud_visuals()
|
||||
|
||||
func activate_client_side():
|
||||
"""Called on clients to show HUD and prepare local state."""
|
||||
print("[PortalModeManager] Activating client-side HUD")
|
||||
_activate_hud()
|
||||
# Initial update to catch any missed goal counts
|
||||
_update_hud_visuals()
|
||||
|
||||
func setup_arena_locally():
|
||||
"""Sets up GridMap size and walls. Called on host and clients."""
|
||||
if arena_setup_done:
|
||||
print("[PortalModeManager] Arena already setup locally, skipping.")
|
||||
return
|
||||
|
||||
print("[PortalModeManager] Setting up arena locally...")
|
||||
_setup_arena_size()
|
||||
_setup_room_partitions()
|
||||
_spawn_portal_doors()
|
||||
|
||||
# PRE-FILL TILES: Ensure all floor tiles have items before the countdown starts
|
||||
if multiplayer.is_server():
|
||||
_refresh_tiles()
|
||||
|
||||
arena_setup_done = true
|
||||
|
||||
func _setup_arena_size():
|
||||
if not gridmap: return
|
||||
gridmap.columns = GRID_SIZE
|
||||
gridmap.rows = GRID_SIZE
|
||||
gridmap.clear()
|
||||
# Explicitly clear Floor 1 to prevent legacy tiles from previous rounds
|
||||
if gridmap.has_method("clear_grid"):
|
||||
gridmap.clear_grid(1)
|
||||
|
||||
# Fill Floor 0 with standard floor (Item ID 0)
|
||||
for x in range(GRID_SIZE):
|
||||
for z in range(GRID_SIZE):
|
||||
gridmap.set_cell_item(Vector3i(x, 0, z), 0)
|
||||
|
||||
func get_spawn_points() -> Array[Vector2i]:
|
||||
# One point per quadrant
|
||||
return [
|
||||
Vector2i(3, 3), # Room 0
|
||||
Vector2i(10, 3), # Room 1
|
||||
Vector2i(3, 10), # Room 2
|
||||
Vector2i(10, 10) # Room 3
|
||||
]
|
||||
|
||||
func _setup_hud():
|
||||
hud_layer = CanvasLayer.new()
|
||||
hud_layer.layer = 5
|
||||
hud_layer.visible = false
|
||||
add_child(hud_layer)
|
||||
|
||||
var bottom_container = CenterContainer.new()
|
||||
bottom_container.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
|
||||
bottom_container.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||
bottom_container.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
||||
bottom_container.offset_bottom = -50
|
||||
hud_layer.add_child(bottom_container)
|
||||
|
||||
mission_label = Label.new()
|
||||
mission_label.text = "GOALS (0/8)"
|
||||
mission_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
|
||||
var custom_font = load("res://assets/fonts/Nougat-ExtraBlack.ttf")
|
||||
if custom_font: mission_label.add_theme_font_override("font", custom_font)
|
||||
|
||||
mission_label.add_theme_font_size_override("font_size", 28)
|
||||
mission_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||
mission_label.add_theme_constant_override("outline_size", 8)
|
||||
bottom_container.add_child(mission_label)
|
||||
|
||||
# Initial update
|
||||
_update_hud_visuals()
|
||||
|
||||
func _update_hud_visuals():
|
||||
if not mission_label: return
|
||||
|
||||
var my_id = multiplayer.get_unique_id()
|
||||
var gcm = main.get_node_or_null("GoalsCycleManager")
|
||||
var completed_count = gcm.player_goal_counts.get(my_id, 0) if gcm else 0
|
||||
|
||||
mission_label.text = "GOALS (%d/%d)" % [completed_count, LobbyManager.doors_required_goals]
|
||||
|
||||
if completed_count >= LobbyManager.doors_required_goals:
|
||||
mission_label.text = "ALL GOALS COMPLETE!\nFIND THE FINISH ROOM!"
|
||||
mission_label.add_theme_color_override("font_color", Color.GOLD)
|
||||
|
||||
if not _has_notified_mission_complete:
|
||||
_has_notified_mission_complete = true
|
||||
var player_node = main.get_node_or_null(str(my_id))
|
||||
if player_node:
|
||||
NotificationManager.send_message(player_node, "ALL GOALS COMPLETE!", NotificationManager.MessageType.GOAL)
|
||||
else:
|
||||
mission_label.add_theme_color_override("font_color", Color.WHITE)
|
||||
_has_notified_mission_complete = false
|
||||
|
||||
func is_mission_complete(peer_id: int) -> bool:
|
||||
var gcm = main.get_node_or_null("GoalsCycleManager")
|
||||
if not gcm: return false
|
||||
return gcm.player_goal_counts.get(peer_id, 0) >= LobbyManager.doors_required_goals
|
||||
|
||||
func check_win_condition(player_id: int, pos: Vector2i) -> bool:
|
||||
# 1. Check if on finish tile
|
||||
var tile = gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
|
||||
if tile != 3: return false
|
||||
|
||||
# 2. Check missions
|
||||
return is_mission_complete(player_id)
|
||||
|
||||
func _setup_room_partitions():
|
||||
for i in range(GRID_SIZE):
|
||||
# Vertical wall (middle columns)
|
||||
gridmap.set_cell_item(Vector3i(6, 0, i), 4) # Wall item
|
||||
gridmap.set_cell_item(Vector3i(7, 0, i), 4)
|
||||
|
||||
# Horizontal wall (middle rows)
|
||||
gridmap.set_cell_item(Vector3i(i, 0, 6), 4)
|
||||
gridmap.set_cell_item(Vector3i(i, 0, 7), 4)
|
||||
|
||||
var _pending_sync_data = null
|
||||
|
||||
func _spawn_portal_doors():
|
||||
# 1. Use synced configs if they exist (passed via main.rpc("sync_portal_configs"))
|
||||
var door_configs = get_meta("door_configs") if has_meta("door_configs") else []
|
||||
|
||||
# 2. If no synced configs (e.g. Server start), generate base + extras
|
||||
if door_configs.is_empty():
|
||||
if not multiplayer.is_server():
|
||||
print("[PortalModeManager] Client waiting for portal configs sync...")
|
||||
return
|
||||
|
||||
door_configs = [
|
||||
# BASE DOORS (2 per room)
|
||||
{"room": 0, "pos": Vector2i(6, 2), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East
|
||||
{"room": 0, "pos": Vector2i(2, 6), "rot": 0, "offset": Vector2i(0, -1)}, # South
|
||||
{"room": 1, "pos": Vector2i(7, 2), "rot": PI / 2, "offset": Vector2i(1, 0)}, # West
|
||||
{"room": 1, "pos": Vector2i(11, 6), "rot": 0, "offset": Vector2i(0, -1)}, # South
|
||||
{"room": 2, "pos": Vector2i(2, 7), "rot": 0, "offset": Vector2i(0, 1)}, # North
|
||||
{"room": 2, "pos": Vector2i(6, 11), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East
|
||||
{"room": 3, "pos": Vector2i(11, 7), "rot": 0, "offset": Vector2i(0, 1)}, # North
|
||||
{"room": 3, "pos": Vector2i(7, 11), "rot": PI / 2, "offset": Vector2i(1, 0)} # West
|
||||
]
|
||||
|
||||
# Server adds extras
|
||||
var extra_options = [
|
||||
{"room": 0, "pos": Vector2i(6, 5), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East (Gap from 6,2)
|
||||
{"room": 1, "pos": Vector2i(7, 5), "rot": PI / 2, "offset": Vector2i(1, 0)}, # West (Gap from 7,2)
|
||||
{"room": 2, "pos": Vector2i(6, 8), "rot": PI / 2, "offset": Vector2i(-1, 0)}, # East (Gap from 6,11)
|
||||
{"room": 3, "pos": Vector2i(7, 8), "rot": PI / 2, "offset": Vector2i(1, 0)} # West (Gap from 7,11)
|
||||
]
|
||||
extra_options.shuffle()
|
||||
door_configs.append(extra_options[0])
|
||||
door_configs.append(extra_options[1])
|
||||
|
||||
# Broadcast to clients
|
||||
main.rpc("sync_portal_configs", door_configs)
|
||||
|
||||
# 3. Spawn the doors
|
||||
if not doors.is_empty(): return # Guard against double spawn
|
||||
|
||||
print("[PortalModeManager] Spawning %d doors. Peer: %d" % [door_configs.size(), multiplayer.get_unique_id()])
|
||||
|
||||
var portal_scene = load("res://scenes/portal_door.tscn")
|
||||
var stands_container = main.get_node_or_null("Stands")
|
||||
if not stands_container:
|
||||
stands_container = Node3D.new()
|
||||
stands_container.name = "Stands"
|
||||
main.add_child(stands_container)
|
||||
|
||||
for i in range(door_configs.size()):
|
||||
var cfg = door_configs[i]
|
||||
if not portal_scene:
|
||||
print("[PortalModeManager] Error: Failed to load portal_door.tscn")
|
||||
break
|
||||
|
||||
var door = portal_scene.instantiate()
|
||||
door.name = "Portal_%d" % i
|
||||
door.room_id = cfg["room"]
|
||||
door.door_id = i
|
||||
door.set_meta("spawn_offset", cfg["offset"]) # Store offset for teleport
|
||||
|
||||
# Position
|
||||
var world_pos = gridmap.map_to_local(Vector3i(cfg["pos"].x, 0, cfg["pos"].y))
|
||||
door.transform.origin = world_pos
|
||||
door.rotation.y = cfg["rot"]
|
||||
|
||||
stands_container.add_child(door, true)
|
||||
doors.append(door)
|
||||
|
||||
# Server-only interaction logic
|
||||
if multiplayer.is_server():
|
||||
door.player_entered_portal.connect(handle_portal_interaction)
|
||||
|
||||
gridmap.set_cell_item(Vector3i(cfg["pos"].x, 0, cfg["pos"].y), 0) # Normal floor
|
||||
|
||||
print("[PortalModeManager] Finished spawning %d doors" % doors.size())
|
||||
|
||||
# Apply pending sync if it arrived early
|
||||
if _pending_sync_data:
|
||||
print("[PortalModeManager] Applying pending sync data...")
|
||||
sync_portal_data(_pending_sync_data)
|
||||
_pending_sync_data = null
|
||||
|
||||
const PORTAL_COLORS = [
|
||||
Color(0, 1, 1), # Cyan
|
||||
Color(1, 0, 1), # Magenta
|
||||
Color(1, 0, 0), # Red
|
||||
Color(0, 1, 0), # Green
|
||||
Color(1, 0.5, 0) # Orange
|
||||
]
|
||||
|
||||
func _randomize_connections():
|
||||
if not multiplayer.is_server(): return
|
||||
|
||||
print("[PortalModeManager] Swapping portal connections...")
|
||||
connections.clear()
|
||||
|
||||
var door_indices = []
|
||||
for i in range(doors.size()):
|
||||
door_indices.append(i)
|
||||
|
||||
# Shuffle and Validate: ensure no pairs are in the same room
|
||||
var valid_pairing = false
|
||||
var attempts = 0
|
||||
while not valid_pairing and attempts < 100:
|
||||
attempts += 1
|
||||
door_indices.shuffle()
|
||||
valid_pairing = true
|
||||
for i in range(0, door_indices.size(), 2):
|
||||
var a = door_indices[i]
|
||||
var b = door_indices[i + 1]
|
||||
if doors[a].room_id == doors[b].room_id:
|
||||
valid_pairing = false
|
||||
break
|
||||
|
||||
# Prepare sync data
|
||||
var sync_data = [] # [[door_a_id, door_b_id, color], ...]
|
||||
|
||||
# Pair them up and assign colors
|
||||
for i in range(0, door_indices.size(), 2):
|
||||
var a = door_indices[i]
|
||||
var b = door_indices[i + 1]
|
||||
connections[a] = b
|
||||
connections[b] = a
|
||||
|
||||
var color = PORTAL_COLORS[int(i / 2.0) % PORTAL_COLORS.size()]
|
||||
sync_data.append([a, b, color])
|
||||
|
||||
doors[a].target_door_id = b
|
||||
doors[a].portal_color = color
|
||||
|
||||
doors[b].target_door_id = a
|
||||
doors[b].portal_color = color
|
||||
|
||||
# Sync to all clients
|
||||
rpc("sync_portal_data", sync_data)
|
||||
main.rpc("display_message", "PORTALS SWITCHED!")
|
||||
|
||||
func sync_to_client(peer_id: int):
|
||||
"""Syncs current portal connections to a specific client."""
|
||||
var sync_data = []
|
||||
# connections is id -> id
|
||||
# We need to rebuild the pair-based data for the RPC
|
||||
var handled = []
|
||||
for a_id in connections:
|
||||
if a_id in handled: continue
|
||||
var b_id = connections[a_id]
|
||||
var color = doors[a_id].portal_color
|
||||
sync_data.append([a_id, b_id, color])
|
||||
handled.append(a_id)
|
||||
handled.append(b_id)
|
||||
|
||||
rpc_id(peer_id, "sync_portal_data", sync_data)
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_portal_data(data: Array):
|
||||
"""Syncs portal connections and colors to all clients."""
|
||||
print("[PortalModeManager] Received portal sync data. Peed ID: ", multiplayer.get_unique_id())
|
||||
|
||||
# If doors array is empty on client, try to repopulate from Stands group
|
||||
if doors.is_empty():
|
||||
var stands = get_tree().get_nodes_in_group("PortalDoors")
|
||||
# Sort by name to ensure consistent indexing
|
||||
stands.sort_custom(func(a, b): return a.name < b.name)
|
||||
doors = stands
|
||||
|
||||
# If still empty, defer sync until doors are spawned locally
|
||||
if doors.is_empty():
|
||||
print("[PortalModeManager] Doors not yet ready, deferring sync data...")
|
||||
_pending_sync_data = data
|
||||
return
|
||||
|
||||
connections.clear()
|
||||
for pair in data:
|
||||
var a_id = pair[0]
|
||||
var b_id = pair[1]
|
||||
var color = pair[2]
|
||||
|
||||
connections[a_id] = b_id
|
||||
connections[b_id] = a_id
|
||||
|
||||
if a_id < doors.size() and b_id < doors.size():
|
||||
if is_instance_valid(doors[a_id]):
|
||||
doors[a_id].target_door_id = b_id
|
||||
doors[a_id].portal_color = color
|
||||
if is_instance_valid(doors[b_id]):
|
||||
doors[b_id].target_door_id = a_id
|
||||
doors[b_id].portal_color = color
|
||||
else:
|
||||
print("[PortalModeManager] Warning: Door index %d or %d out of range during sync" % [a_id, b_id])
|
||||
|
||||
|
||||
func _on_global_goal_count_updated(_peer_id: int, _count: int):
|
||||
# Mission requirement removed in favor of time-based finish reveal
|
||||
pass
|
||||
|
||||
func _on_goal_count_updated(peer_id: int, _count: int):
|
||||
# Update HUD if relevant (always check if it's the local player whose count changed)
|
||||
if peer_id == multiplayer.get_unique_id():
|
||||
_update_hud_visuals()
|
||||
|
||||
func _spawn_finish_room():
|
||||
print("[PortalModeManager] Time is running out! Revealing Finish Room...")
|
||||
finish_spawned = true
|
||||
|
||||
# Choose a random room quadrant index (0 to 3)
|
||||
var room_idx = randi() % 4
|
||||
|
||||
# Determine center for the selected room quadrant (7x7 rooms)
|
||||
var x_center = 3 if (room_idx == 0 or room_idx == 2) else 10
|
||||
var z_center = 3 if (room_idx == 0 or room_idx == 1) else 10
|
||||
|
||||
# Determine 3x3 bounds around the center
|
||||
var x_start = x_center - 1
|
||||
var x_end = x_center + 2 # exclusive for range()
|
||||
var z_start = z_center - 1
|
||||
var z_end = z_center + 2 # exclusive for range()
|
||||
|
||||
print("[PortalModeManager] Converting 3x3 area in Room %d (X:%d-%d, Z:%d-%d) to Finish Tiles" % [room_idx, x_start, x_end-1, z_start, z_end-1])
|
||||
|
||||
# Iterate through the 3x3 area
|
||||
for x in range(x_start, x_end):
|
||||
for z in range(z_start, z_end):
|
||||
# Only convert walkable floor tiles (Item ID 0) on Floor 0
|
||||
var floor_0_item = gridmap.get_cell_item(Vector3i(x, 0, z))
|
||||
if floor_0_item == 0:
|
||||
# Change Floor 0 tile to Finish Tile (ID 3)
|
||||
main.rpc("sync_grid_item", x, 0, z, 3)
|
||||
|
||||
# Clear any item on Floor 1 above this tile
|
||||
main.rpc("sync_grid_item", x, 1, z, -1)
|
||||
|
||||
# Visual update for server
|
||||
if gridmap.has_method("update_grid_data"):
|
||||
gridmap.update_grid_data()
|
||||
|
||||
main.rpc("display_message", "[ALARM] THE FINISH ROOM HAS APPEARED!")
|
||||
main.rpc("broadcast_message", "SYSTEM", "A 3x3 Finish Zone has appeared in Room %d!" % room_idx, 4) # 4 = MessageType.WARNING
|
||||
|
||||
func _get_room_index(pos: Vector2i) -> int:
|
||||
if pos.x < 7 and pos.y < 7: return 0
|
||||
if pos.x >= 7 and pos.y < 7: return 1
|
||||
if pos.x < 7 and pos.y >= 7: return 2
|
||||
return 3
|
||||
|
||||
func _on_swap_timer_timeout():
|
||||
_randomize_connections()
|
||||
|
||||
func _on_tile_refresh_timer_timeout():
|
||||
_refresh_tiles()
|
||||
main.rpc("display_message", "TILES REPLENISHED!")
|
||||
|
||||
func _refresh_tiles():
|
||||
# GridMap Floor 0 has the walls (ID 4) and floors (ID 0)
|
||||
# GridMap Floor 1 should have the items (Heart, Star, etc)
|
||||
# Cache door positions to avoid spawning under them
|
||||
var door_positions = []
|
||||
for door in doors:
|
||||
if is_instance_valid(door):
|
||||
var local_pos = gridmap.local_to_map(gridmap.to_local(door.global_position))
|
||||
door_positions.append(Vector2i(local_pos.x, local_pos.z))
|
||||
|
||||
for x in range(GRID_SIZE):
|
||||
for z in range(GRID_SIZE):
|
||||
# 1. Check if Floor 0 is a wall or void
|
||||
var floor_0_item = gridmap.get_cell_item(Vector3i(x, 0, z))
|
||||
if floor_0_item in [4, -1]:
|
||||
continue
|
||||
|
||||
# 1.5. Prevent spawning directly under portal doors
|
||||
if door_positions.has(Vector2i(x, z)):
|
||||
continue
|
||||
|
||||
# 2. Check if Floor 1 is already occupied
|
||||
if gridmap.get_cell_item(Vector3i(x, 1, z)) != -1:
|
||||
continue
|
||||
|
||||
# 3. Spawn a tile (60% chance per valid floor cell)
|
||||
if randf() < 0.6:
|
||||
var weights = ScarcityModel.get_tile_weights()
|
||||
var tile_id = _pick_weighted_tile(weights)
|
||||
# Update GridMap Floor 1 via RPC for sync
|
||||
main.rpc("sync_grid_item", x, 1, z, tile_id)
|
||||
|
||||
func _pick_weighted_tile(weights: Dictionary) -> int:
|
||||
var total_weight = 0
|
||||
for w in weights.values(): total_weight += w
|
||||
|
||||
var r = randi() % total_weight
|
||||
var cumulative = 0
|
||||
for tile in weights:
|
||||
cumulative += weights[tile]
|
||||
if r < cumulative:
|
||||
return tile
|
||||
return 7 # Default Heart
|
||||
|
||||
func handle_portal_interaction(player, door):
|
||||
if not multiplayer.is_server(): return
|
||||
|
||||
var current_time = Time.get_ticks_msec()
|
||||
if player_portal_cooldowns.has(player.name):
|
||||
# Reduce cooldown to 200ms (more responsive than 1s, but enough to avoid jitter)
|
||||
if current_time - player_portal_cooldowns[player.name] < 200:
|
||||
return
|
||||
player_portal_cooldowns[player.name] = current_time
|
||||
|
||||
var source_id = door.door_id
|
||||
if not connections.has(source_id): return
|
||||
|
||||
var target_id = connections[source_id]
|
||||
var target_door = doors[target_id]
|
||||
|
||||
# Use stored offset to avoid infinite loop (spawn inside the target room)
|
||||
var offset = target_door.get_meta("spawn_offset") if target_door.has_meta("spawn_offset") else Vector2i(0, 0)
|
||||
|
||||
var target_world = target_door.global_position
|
||||
var target_grid_3d = gridmap.local_to_map(target_world)
|
||||
var target_grid = Vector2i(target_grid_3d.x, target_grid_3d.z) + offset
|
||||
|
||||
# Check for overlaps at the target_grid
|
||||
var final_target = target_grid
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
var is_occupied = true
|
||||
var search_radius = 0
|
||||
var max_search_radius = 2
|
||||
|
||||
while is_occupied and search_radius <= max_search_radius:
|
||||
is_occupied = false
|
||||
for p in all_players:
|
||||
if p != player and p.current_position == final_target:
|
||||
is_occupied = true
|
||||
break
|
||||
|
||||
if is_occupied:
|
||||
# Try to find an adjacent cell
|
||||
search_radius += 1
|
||||
var found_empty = false
|
||||
# Check immediate neighbors first
|
||||
var offsets = [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1),
|
||||
Vector2i(1, 1), Vector2i(-1, 1), Vector2i(1, -1), Vector2i(-1, -1)]
|
||||
for offset_vec in offsets:
|
||||
var test_pos = final_target + offset_vec
|
||||
# Check if it's strictly a floor tile (ID 0) on Floor 0, not a wall
|
||||
if gridmap.get_cell_item(Vector3i(test_pos.x, 0, test_pos.y)) == 0:
|
||||
# Verify no player is on this test_pos
|
||||
var test_occupied = false
|
||||
for p in all_players:
|
||||
if p != player and p.current_position == test_pos:
|
||||
test_occupied = true
|
||||
break
|
||||
if not test_occupied:
|
||||
final_target = test_pos
|
||||
found_empty = true
|
||||
break
|
||||
|
||||
if found_empty:
|
||||
is_occupied = false
|
||||
|
||||
print("[Portal] Teleporting %s to Room %d, Pos %s (via Door %d)" % [player.name, target_door.room_id, final_target, target_id])
|
||||
|
||||
# Snap player
|
||||
if player.has_method("set_spawn_position"):
|
||||
player.rpc("set_spawn_position", final_target)
|
||||
@@ -558,11 +558,7 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true, only_c
|
||||
item_id = rng.randi_range(7, 10)
|
||||
else:
|
||||
# 20% Chance for PowerUp
|
||||
if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS):
|
||||
# Restrict to Speed (11) and Ghost (14) for Tekton Doors
|
||||
item_id = [11, 14].pick_random()
|
||||
else:
|
||||
item_id = rng.randi_range(11, 14)
|
||||
item_id = rng.randi_range(11, 14)
|
||||
|
||||
var cell = Vector3i(pos.x, 1, pos.y)
|
||||
|
||||
|
||||
@@ -18,12 +18,6 @@ const SCHEMA = {
|
||||
"sng_stop_duration": {"type": TYPE_INT, "default": 4, "min": 2, "max": 10},
|
||||
"sng_required_goals": {"type": TYPE_INT, "default": 8, "min": 3, "max": 20}
|
||||
},
|
||||
"Tekton Doors": {
|
||||
"match_duration": {"type": TYPE_INT, "default": 180, "min": 60, "max": 600},
|
||||
"doors_swap_time": {"type": TYPE_INT, "default": 15, "min": 10, "max": 30},
|
||||
"doors_refresh_time": {"type": TYPE_INT, "default": 25, "min": 15, "max": 40},
|
||||
"doors_required_goals": {"type": TYPE_INT, "default": 8, "min": 5, "max": 12}
|
||||
},
|
||||
"Candy Pump Survival": {
|
||||
"match_duration": {"type": TYPE_INT, "default": 180, "min": 60, "max": 600},
|
||||
"gauntlet_growth_interval": {"type": TYPE_FLOAT, "default": 3.0, "min": 1.0, "max": 10.0},
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
extends StaticBody3D
|
||||
|
||||
# PortalDoor.gd
|
||||
# Specialized door for "Tekton Doors" mode.
|
||||
# Teleports players to a target room/door when they step into it.
|
||||
|
||||
signal player_entered_portal(player_node, door_node)
|
||||
|
||||
@export var room_id: int = 0
|
||||
@export var door_id: int = 0 # 0: North, 1: South, 2: East, 3: West
|
||||
|
||||
# State synced by PortalModeManager
|
||||
var target_room_id: int = -1
|
||||
var target_door_id: int = -1
|
||||
var is_active: bool = true
|
||||
var portal_color: Color = Color.WHITE: set = set_portal_color
|
||||
|
||||
func set_portal_color(value: Color):
|
||||
portal_color = value
|
||||
_update_visuals()
|
||||
|
||||
@onready var detection_area: Area3D = $Area3D
|
||||
|
||||
func _ready():
|
||||
add_to_group("PortalDoors")
|
||||
if detection_area:
|
||||
detection_area.body_entered.connect(_on_body_entered)
|
||||
|
||||
# Visual feedback: indicate door is active
|
||||
_update_visuals()
|
||||
|
||||
# Adjust GroundIndicator position based on spawn_offset metadata
|
||||
_adjust_indicator_position()
|
||||
|
||||
func _on_body_entered(body: Node3D):
|
||||
if not is_active: return
|
||||
|
||||
if body.is_in_group("Players") or body.get("is_bot"):
|
||||
var current_time = Time.get_ticks_msec()
|
||||
if body.has_meta("last_portal_time"):
|
||||
# Reduce cooldown to 200ms to match manager logic and allow fast re-entry
|
||||
if current_time - body.get_meta("last_portal_time") < 200:
|
||||
return
|
||||
|
||||
body.set_meta("last_portal_time", current_time)
|
||||
|
||||
print("[PortalDoor] Player %s entered Door %d in Room %d" % [body.name, door_id, room_id])
|
||||
emit_signal("player_entered_portal", body, self )
|
||||
|
||||
var _materials_initialized: bool = false
|
||||
|
||||
func _update_visuals():
|
||||
# Removed is_node_ready() check to allow early setter calls to prepare variables,
|
||||
# but we still need the nodes to exist to apply them.
|
||||
if not is_inside_tree(): return
|
||||
|
||||
var vortex = get_node_or_null("Vortex")
|
||||
var frame_left = get_node_or_null("Frame_Left")
|
||||
|
||||
# If children aren't there yet, we can't update visuals.
|
||||
# This usually happens if called before or during early _ready.
|
||||
if not vortex or not frame_left: return
|
||||
|
||||
if not _materials_initialized:
|
||||
_initialize_unique_materials()
|
||||
_materials_initialized = true
|
||||
|
||||
if vortex:
|
||||
var mat = vortex.get_surface_override_material(0)
|
||||
if mat:
|
||||
mat.albedo_color = portal_color
|
||||
mat.albedo_color.a = 0.5
|
||||
if mat.has_method("set_emission"):
|
||||
mat.set("emission", portal_color)
|
||||
|
||||
for part_name in ["Frame_Left", "Frame_Right", "Frame_Top"]:
|
||||
var frame = get_node_or_null(part_name)
|
||||
if frame:
|
||||
var mat = frame.get_surface_override_material(0)
|
||||
if mat:
|
||||
mat.albedo_color = portal_color.lerp(Color.BLACK, 0.4)
|
||||
|
||||
var ground = get_node_or_null("GroundIndicator")
|
||||
if ground:
|
||||
var mat = ground.get_surface_override_material(0)
|
||||
if mat:
|
||||
mat.albedo_color = portal_color
|
||||
mat.albedo_color.a = 0.4
|
||||
mat.emission = portal_color
|
||||
mat.emission_energy_multiplier = 2.0
|
||||
|
||||
func _initialize_unique_materials():
|
||||
var vortex = get_node_or_null("Vortex")
|
||||
if vortex:
|
||||
var mat = vortex.get_surface_override_material(0)
|
||||
if not mat:
|
||||
mat = vortex.mesh.surface_get_material(0)
|
||||
|
||||
if mat:
|
||||
vortex.set_surface_override_material(0, mat.duplicate())
|
||||
|
||||
for part_name in ["Frame_Left", "Frame_Right", "Frame_Top"]:
|
||||
var frame = get_node_or_null(part_name)
|
||||
if frame:
|
||||
var mat = frame.get_surface_override_material(0)
|
||||
if not mat:
|
||||
mat = frame.mesh.surface_get_material(0)
|
||||
|
||||
if mat:
|
||||
frame.set_surface_override_material(0, mat.duplicate())
|
||||
|
||||
var ground = get_node_or_null("GroundIndicator")
|
||||
if ground:
|
||||
var mat = ground.get_surface_override_material(0)
|
||||
if not mat:
|
||||
mat = ground.mesh.surface_get_material(0)
|
||||
if mat:
|
||||
ground.set_surface_override_material(0, mat.duplicate())
|
||||
|
||||
func _adjust_indicator_position():
|
||||
# This uses the spawn_offset metadata set by PortalModeManager
|
||||
# to push the ground indicator "into" the room.
|
||||
if not has_meta("spawn_offset"): return
|
||||
|
||||
var ground = get_node_or_null("GroundIndicator")
|
||||
if not ground: return
|
||||
|
||||
var offset_2d = get_meta("spawn_offset") # Vector2i
|
||||
var offset_3d = Vector3(offset_2d.x, 0, offset_2d.y)
|
||||
|
||||
# Convert the global direction (into the room) to local coordinates
|
||||
var local_dir = to_local(global_position + offset_3d).normalized()
|
||||
|
||||
# Nudge the indicator in that direction
|
||||
ground.position = local_dir * 0.5 # Reduced from 0.6 to close the gap
|
||||
ground.position.y = 0.05 # Keep it just above the floor
|
||||
+1
-3
@@ -514,9 +514,7 @@ func spawn_tiles_around(count: int = 4):
|
||||
if LobbyManager:
|
||||
mode = LobbyManager.get_game_mode()
|
||||
|
||||
if LobbyManager and LobbyManager.get_game_mode() == GameMode.Mode.TEKTON_DOORS:
|
||||
item_id = [11, 14].pick_random()
|
||||
elif mode == GameMode.Mode.FREEMODE:
|
||||
if mode == GameMode.Mode.FREEMODE:
|
||||
item_id = rng.randi_range(7, 10) # No powerups in freemode either, just normal tiles
|
||||
else:
|
||||
item_id = rng.randi_range(11, 14)
|
||||
|
||||
Reference in New Issue
Block a user