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 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() # 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) 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 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 == 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 # 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) var main = get_node("/root/Main") if main and can_rpc(): main.rpc("sync_grid_item", 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 can_rpc(): 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 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: # Don't reset mission progress! # Just Drop All Tiles (which already exist on player) if can_rpc(): 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 can_rpc(): 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