378 lines
12 KiB
GDScript
378 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_START = 2 # Start Line
|
|
const TILE_SAFE = 2 # Green Safe Zone
|
|
const TILE_OBSTACLE = 4 # Wall
|
|
|
|
var hud_layer: CanvasLayer
|
|
var phase_label: Label
|
|
var mission_label: Label
|
|
var red_tint_overlay: ColorRect
|
|
|
|
func _ready():
|
|
set_process(false)
|
|
_setup_hud()
|
|
|
|
func _setup_hud():
|
|
hud_layer = CanvasLayer.new()
|
|
hud_layer.visible = false
|
|
add_child(hud_layer)
|
|
|
|
# Full-screen red tint overlay (below everything else in this layer, but above game)
|
|
red_tint_overlay = ColorRect.new()
|
|
red_tint_overlay.color = Color(1.0, 0.0, 0.0, 0.25) # Transparent red
|
|
red_tint_overlay.set_anchors_preset(Control.PRESET_FULL_RECT) # Cover whole screen
|
|
red_tint_overlay.visible = false # Hidden initially
|
|
hud_layer.add_child(red_tint_overlay)
|
|
|
|
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)
|
|
|
|
# Toggle Red Screen Tint
|
|
if red_tint_overlay:
|
|
red_tint_overlay.visible = (current_phase == Phase.STOP)
|
|
|
|
var my_id = multiplayer.get_unique_id()
|
|
if mission_label:
|
|
var main = get_node_or_null("/root/Main")
|
|
var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager") if main else null
|
|
|
|
# Get count from GoalsCycleManager (Source of truth for PlayerBoardLabel)
|
|
var completed_count = goals_cycle_manager.player_goal_counts.get(my_id, 0) if goals_cycle_manager else 0
|
|
|
|
mission_label.text = "GOALS (%d/3)" % completed_count
|
|
|
|
if completed_count >= 3:
|
|
mission_label.text = "ALL GOALS COMPLETE!\nREACH THE FINISH!"
|
|
mission_label.add_theme_color_override("font_color", Color.GOLD)
|
|
else:
|
|
mission_label.add_theme_color_override("font_color", Color.WHITE)
|
|
|
|
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() # REMOVED: Already explicitly called in main.gd _setup_host_game to prepare floor before spawns!
|
|
_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 can_rpc():
|
|
rpc("sync_phase", phase_name, phase_timer)
|
|
|
|
# If GO phase starts, clear all STOP phase freezes
|
|
if phase == Phase.GO:
|
|
var all_players = get_tree().get_nodes_in_group("Players")
|
|
for p in all_players:
|
|
if p.has_method("sync_stop_freeze"):
|
|
p.rpc("sync_stop_freeze", false)
|
|
|
|
emit_signal("phase_changed", phase_name, phase_timer)
|
|
|
|
func can_rpc() -> bool:
|
|
if not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED:
|
|
return false
|
|
return true
|
|
|
|
@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_parent().get_node_or_null("EnhancedGridMap")
|
|
if not gridmap:
|
|
gridmap = get_node_or_null("/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 can_rpc():
|
|
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_parent().get_node_or_null("EnhancedGridMap")
|
|
if not gridmap:
|
|
# Fallback just in case
|
|
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
|
if not gridmap: return
|
|
|
|
# Set Size for Stop n Go explicitly, bypassing setters that wipe the map
|
|
gridmap.set("columns", 22)
|
|
gridmap.set("rows", 10)
|
|
|
|
# Clear existing items on all layers
|
|
gridmap.clear()
|
|
|
|
# Safe Zones Columns: 6, 7, 8 (Only one band now)
|
|
var safe_columns = [6, 7, 8]
|
|
|
|
# Create bands based on X (Horizontal Progress)
|
|
for x in range(gridmap.columns):
|
|
var tile_id = TILE_WALKABLE
|
|
if x == 0:
|
|
tile_id = TILE_START
|
|
elif 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()
|
|
# Client already constructs the base arena locally via _apply_arena_setup()
|
|
# So no need to blast huge 5KB arrays across the network.
|
|
|
|
# For any specifically spawned tiles (like missions), they are sent individually
|
|
# by sync_grid_item inside _spawn_mission_tiles.
|
|
|
|
func _spawn_mission_tiles():
|
|
var gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
|
if not gridmap:
|
|
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
|
if not gridmap: return
|
|
|
|
# Forbidden Zones (Start, Safe, Finish) - No items here
|
|
var forbidden_x = [0, 6, 7, 8, 21]
|
|
|
|
# Goal items: Heart(7), Diamond(8), Star(9), Coin(10)
|
|
var goal_items = [7, 8, 9, 10]
|
|
|
|
for x in range(gridmap.columns):
|
|
if x in forbidden_x:
|
|
continue # Clear zone
|
|
|
|
for z in range(gridmap.rows):
|
|
# Ensure we don't spawn on obstacles
|
|
var base_tile = gridmap.get_cell_item(Vector3i(x, 0, z))
|
|
if base_tile == TILE_OBSTACLE:
|
|
continue
|
|
|
|
# Randomly populate other floors with goal tiles
|
|
# 30% chance to have a tile to avoid overcrowding
|
|
if randf() < 0.3:
|
|
var tile_type = goal_items[randi() % goal_items.size()]
|
|
gridmap.set_cell_item(Vector3i(x, 1, z), tile_type)
|
|
|
|
# Sync to clients
|
|
var main = get_node("/root/Main")
|
|
if main:
|
|
main.rpc("sync_grid_item", x, 1, z, tile_type)
|
|
|
|
func _assign_missions():
|
|
# NO-OP: Missions are now achievement-based (Complete 3 Goals)
|
|
# which is tracked natively by GoalsCycleManager.
|
|
pass
|
|
|
|
@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 gridmap = get_parent().get_node_or_null("EnhancedGridMap")
|
|
if not gridmap:
|
|
gridmap = get_node_or_null("/root/Main/EnhancedGridMap")
|
|
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:
|
|
# We now use the 'on_stop_phase_violation' RPC which scatters tiles around the player
|
|
# like they got attacked, instead of just dropping them in one pile.
|
|
if can_rpc():
|
|
player_node.rpc("on_stop_phase_violation")
|
|
|
|
# Notification is also handled inside on_stop_phase_violation on the player node
|
|
emit_signal("player_penalized", player_id)
|
|
|
|
func update_mission_progress(_player_id: int, _tile_id: int):
|
|
# Redundant in Board-based mode, but kept for compatibility.
|
|
# The board is synced separately via sync_playerboard in playerboard_manager.gd.
|
|
pass
|
|
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func sync_mission_progress(_player_id: int, _mission_index: int, _current: int):
|
|
# Deprecated
|
|
pass
|
|
|
|
func check_win_condition(player_id: int, position: Vector2i) -> bool:
|
|
# 1. Must reach the finish line (Column 21)
|
|
if position.x < finish_line_x:
|
|
return false
|
|
|
|
# 2. Must have 3 Goal Completions (tracked by GoalsCycleManager)
|
|
var main = get_node_or_null("/root/Main")
|
|
if not main: return false
|
|
|
|
var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager")
|
|
if not goals_cycle_manager: return false
|
|
|
|
var completed_count = goals_cycle_manager.player_goal_counts.get(player_id, 0)
|
|
|
|
if completed_count >= 3:
|
|
print("[StopNGo] Player %d REACHED FINISH with %d goals complete!" % [player_id, completed_count])
|
|
return true
|
|
else:
|
|
# Inform the player locally if they reach the end without goals
|
|
var player_node = main.get_node_or_null(str(player_id))
|
|
if player_node:
|
|
NotificationManager.send_message(player_node, "Incomplete! Achieve 3 goals (x3) to win!", NotificationManager.MessageType.WARNING)
|
|
|
|
print("[StopNGo] Player %d reached finish but goal count too low: %d/3" % [player_id, completed_count])
|
|
return false
|