Files
tekton/scripts/managers/stop_n_go_manager.gd
T

352 lines
12 KiB
GDScript

extends Node
class_name StopNGoManager
# StopNGoManager - Handles phase transitions, missions, and movement penalties
signal phase_changed(new_phase: String, remaining_time: float)
signal mission_status_updated(player_id: int, completed: bool)
signal player_penalized(player_id: int)
enum Phase {GO, STOP}
const GO_DURATION: float = 12.0
const STOP_DURATION: float = 6.0
var current_phase: Phase = Phase.GO
var phase_timer: float = GO_DURATION
var is_active: bool = false
var player_missions: Dictionary = {} # player_id -> {target_tile: int, required: int, current: int}
var finish_line_x: int = 21 # Right side of the map for win condition
# Tile IDs
const TILE_WALKABLE = 0
const TILE_SAFE = 2 # Green Safe Zone
const TILE_OBSTACLE = 4 # Wall
var hud_layer: CanvasLayer
var phase_label: Label
var mission_label: Label
func _ready():
set_process(false)
_setup_hud()
func _setup_hud():
hud_layer = CanvasLayer.new()
hud_layer.visible = false
add_child(hud_layer)
var vbox = VBoxContainer.new()
vbox.set_anchors_preset(Control.PRESET_TOP_RIGHT)
vbox.offset_right = -20
vbox.offset_top = 100
hud_layer.add_child(vbox)
# Style for HUD
var style = StyleBoxFlat.new()
style.bg_color = Color(0, 0, 0, 0.4)
style.content_margin_left = 10
style.content_margin_top = 10
style.content_margin_right = 10
style.content_margin_bottom = 10
var panel = PanelContainer.new()
panel.add_theme_stylebox_override("panel", style)
vbox.add_child(panel)
var inner_vbox = VBoxContainer.new()
panel.add_child(inner_vbox)
phase_label = Label.new()
phase_label.text = "PHASE: GO"
phase_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
phase_label.add_theme_font_size_override("font_size", 32)
phase_label.add_theme_color_override("font_color", Color.GREEN)
inner_vbox.add_child(phase_label)
mission_label = Label.new()
mission_label.text = "MISSION: Collect 3 Items"
mission_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
inner_vbox.add_child(mission_label)
func _process(delta):
if not is_active:
return
if multiplayer.is_server():
phase_timer -= delta
if phase_timer <= 0:
if current_phase == Phase.GO:
_start_phase(Phase.STOP)
else:
_start_phase(Phase.GO)
# Update HUD locally
_update_hud_visuals()
func _update_hud_visuals():
var phase_name = "GO" if current_phase == Phase.GO else "STOP"
if phase_label:
phase_label.text = "PHASE: %s (%.0fs)" % [phase_name, max(0, phase_timer)]
phase_label.add_theme_color_override("font_color", Color.GREEN if current_phase == Phase.GO else Color.RED)
var my_id = multiplayer.get_unique_id()
if mission_label and player_missions.has(my_id):
var mission = player_missions[my_id]
# Get Icon name or ID for display
var tile_name = "Items"
match mission["target_tile"]:
7: tile_name = "Hearts"
8: tile_name = "Diamonds"
9: tile_name = "Stars"
10: tile_name = "Coins"
mission_label.text = "Collect %d %s: %d / %d" % [mission["required"], tile_name, mission["current"], mission["required"]]
if mission["current"] >= mission["required"]:
mission_label.text = "MISSION COMPLETE! REACH FINISH!"
mission_label.add_theme_color_override("font_color", Color.GOLD)
func activate_client_side():
is_active = true
if hud_layer:
hud_layer.visible = true
set_process(true)
func start_game_mode():
# This should primarily be called by the Server
# Clients get activated via RPCs (sync_phase, etc)
if multiplayer.is_server():
activate_client_side() # Server also needs local processing
_setup_arena()
_assign_missions()
_start_phase(Phase.GO)
else:
# Clients just wait for updates, but can enable HUD if needed
# activate_client_side() can be called when first sync arrives
pass
func _start_phase(phase: Phase):
current_phase = phase
phase_timer = GO_DURATION if phase == Phase.GO else STOP_DURATION
var phase_name = "GO" if phase == Phase.GO else "STOP"
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
if multiplayer.get_peers().size() > 0:
rpc("sync_phase", phase_name, phase_timer)
emit_signal("phase_changed", phase_name, phase_timer)
@rpc("authority", "call_local", "reliable")
func sync_phase(phase_name: String, duration: float):
if not is_active:
activate_client_side()
current_phase = Phase.GO if phase_name == "GO" else Phase.STOP
phase_timer = duration
func _setup_arena():
var gridmap = get_node("/root/Main/EnhancedGridMap")
if not gridmap: return
print("[StopNGo] Setting up 22x10 Arena with Randomized Obstacles...")
# Explicitly sync dimensions and clear grid on all clients
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
if multiplayer.get_peers().size() > 0:
rpc("sync_arena_setup")
# Apply locally for Server (RPC is call_remote)
_apply_arena_setup()
@rpc("authority", "call_remote", "reliable")
func sync_arena_setup():
print("[StopNGo] Client: Syncing Arena Setup (22x10)...")
_apply_arena_setup()
func _apply_arena_setup():
# Shared logic for resizing and clearing
var gridmap = get_node("/root/Main/EnhancedGridMap")
if not gridmap: return
# Set Size for Stop n Go
gridmap.columns = 22
gridmap.rows = 10
# Clear existing items on all layers
gridmap.clear()
# Safe Zones Columns: 6, 7, 8 and 14, 15, 16
var safe_columns = [6, 7, 8, 14, 15, 16]
# Create bands based on X (Horizontal Progress)
for x in range(gridmap.columns):
var tile_id = TILE_WALKABLE
if x in safe_columns:
tile_id = TILE_SAFE
for z in range(gridmap.rows):
gridmap.set_cell_item(Vector3i(x, 0, z), tile_id)
# --- SPECIFIC OBSTACLES (Black Walls) ---
# Left Obstacles (Column 4)
# Top Vertical Bar
for z in range(0, 4): # z=0, 1, 2, 3
gridmap.set_cell_item(Vector3i(4, 0, z), TILE_OBSTACLE)
# Bottom Vertical Bar
for z in range(6, 10): # z=6, 7, 8, 9
gridmap.set_cell_item(Vector3i(4, 0, z), TILE_OBSTACLE)
# Center Obstacles (Column 11 area)
# Top Middle Vertical Bar (Offset slightly down)
for z in range(1, 5): # z=1, 2, 3, 4
gridmap.set_cell_item(Vector3i(11, 0, z), TILE_OBSTACLE)
# Bottom Middle L-Shape (Vertical + Horizontal hook)
for z in range(6, 9): # z=6, 7, 8 (Vertical part)
gridmap.set_cell_item(Vector3i(11, 0, z), TILE_OBSTACLE)
# Horizontal part of L (at z=6 to right?) - Image looks like inverted L or T?
# Let's assume right hook at top of bottom part
gridmap.set_cell_item(Vector3i(12, 0, 6), TILE_OBSTACLE)
# Right Obstacles (Column 18 area)
# Top Right L-Shape (Horizontal hook + Vertical down)
# Vertical
for z in range(0, 3): # z=0, 1, 2
gridmap.set_cell_item(Vector3i(18, 0, z), TILE_OBSTACLE)
# Horizontal hook to right
gridmap.set_cell_item(Vector3i(19, 0, 2), TILE_OBSTACLE)
gridmap.set_cell_item(Vector3i(20, 0, 2), TILE_OBSTACLE)
# Bottom Right Vertical Bar
for z in range(5, 9): # z=5, 6, 7, 8
gridmap.set_cell_item(Vector3i(18, 0, z), TILE_OBSTACLE)
gridmap.update_grid_data()
gridmap.initialize_astar()
# Spawn tiles for missions
if multiplayer.is_server():
_spawn_mission_tiles()
# Sync the WHOLE grid to all clients to ensure size and stripes are correct
var main = get_node("/root/Main")
if main and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
# Gather all floor 0 and floor 1 data
var floor0_data = gridmap.get_floor_data(0)
var floor1_data = gridmap.get_floor_data(1)
main.rpc("sync_full_grid_data_stop_n_go", floor0_data, floor1_data, gridmap.columns, gridmap.rows)
func _spawn_mission_tiles():
var gridmap = get_node("/root/Main/EnhancedGridMap")
# Tile IDs for missions: Heart(7), Diamond(8), Star(9), Coin(10)
var mission_tiles = [7, 8, 9, 10]
for tile_type in mission_tiles:
var count = 0
while count < 15: # 15 of each type (plenty for finding 3)
var x = randi() % gridmap.columns
var z = randi() % gridmap.rows
# Only spawn in walkable areas (Floor 0 must be 0 or 2, and Floor 1 empty)
var floor_item = gridmap.get_cell_item(Vector3i(x, 0, z))
if (floor_item == TILE_WALKABLE or floor_item == TILE_SAFE):
if gridmap.get_cell_item(Vector3i(x, 1, z)) == -1:
gridmap.set_cell_item(Vector3i(x, 1, z), tile_type)
count += 1
func _assign_missions():
# Assign UNIQUE target tile types to players cyclicly
var players = GameStateManager.players
var tile_types = [ScarcityModel.TILE_HEART, ScarcityModel.TILE_DIAMOND, ScarcityModel.TILE_STAR, ScarcityModel.TILE_COIN]
var idx = 0
for p_id in players:
var target = tile_types[idx % tile_types.size()]
player_missions[p_id] = {
"target_tile": target,
"required": 3,
"current": 0
}
idx += 1
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
if multiplayer.get_peers().size() > 0:
rpc("sync_missions", player_missions)
@rpc("authority", "call_local", "reliable")
func sync_missions(missions: Dictionary):
player_missions = missions
func check_movement_violation(player_id: int, from: Vector2i, to: Vector2i) -> bool:
"""Check if movement is illegal (during STOP phase and not in safe zone)."""
if current_phase == Phase.STOP:
var main = get_node("/root/Main")
var gridmap = main.get_node("EnhancedGridMap") if main else null
if gridmap:
# Check FROM position. If you were safe, you can move?
# Rules: "If a player moves during this phase".
# Usually implies checking if you ARE in a safe zone.
var tile_from = gridmap.get_cell_item(Vector3i(from.x, 0, from.y))
if tile_from != TILE_SAFE:
_penalize_player(player_id)
return true
return false
func _penalize_player(player_id: int):
if not multiplayer.is_server(): return
var main = get_node("/root/Main")
if not main: return
var player_node = main.get_node_or_null(str(player_id))
if player_node:
# Don't reset mission progress!
# Just Drop All Tiles (which already exist on player)
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
player_node.rpc("drop_all_tiles")
# Also send message
NotificationManager.send_message(player_node, "Moved during STOP! Tiles Dropped!", NotificationManager.MessageType.WARNING)
emit_signal("player_penalized", player_id)
func update_mission_progress(player_id: int, tile_id: int):
if not player_missions.has(player_id): return
var mission = player_missions[player_id]
if tile_id == mission["target_tile"]:
mission["current"] = min(mission["current"] + 1, mission["required"])
if mission["current"] >= mission["required"]:
emit_signal("mission_status_updated", player_id, true)
# FIX: NotificationManager.send_message_to_player() does NOT exist.
# We need to find the player node and use send_message(target, msg, type)
var main = get_node("/root/Main")
if main:
var player_node = main.get_node_or_null(str(player_id))
if player_node:
NotificationManager.send_message(player_node, "Mission Complete! Reach the Finish!", NotificationManager.MessageType.GOAL)
if multiplayer.is_server() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
if multiplayer.get_peers().size() > 0:
rpc("sync_mission_progress", player_id, mission["current"])
@rpc("any_peer", "call_local", "reliable")
func sync_mission_progress(player_id: int, current: int):
if player_missions.has(player_id):
player_missions[player_id]["current"] = current
func check_win_condition(player_id: int, position: Vector2i) -> bool:
if not player_missions.has(player_id): return false
var mission = player_missions[player_id]
if mission["current"] >= mission["required"]:
# Win when reaching X >= 21
if position.x >= finish_line_x:
return true
return false