From a7a8106b7e1c877bef00bd477ded9c7e936bff53 Mon Sep 17 00:00:00 2001 From: adtpdn Date: Thu, 19 Feb 2026 03:42:59 +0800 Subject: [PATCH] attempt to make stop n go gamemode --- scenes/lobby.gd | 25 ++ scenes/lobby.tscn | 23 ++ scenes/main.gd | 69 ++++- scenes/player.gd | 70 ++++++ scripts/managers/lobby_manager.gd | 31 +++ scripts/managers/player_movement_manager.gd | 20 +- scripts/managers/playerboard_manager.gd | 12 + scripts/managers/special_tiles_manager.gd | 14 +- scripts/managers/stop_n_go_manager.gd | 265 ++++++++++++++++++++ scripts/managers/stop_n_go_manager.gd.uid | 1 + scripts/nakama_manager.gd | 14 +- 11 files changed, 516 insertions(+), 28 deletions(-) create mode 100644 scripts/managers/stop_n_go_manager.gd create mode 100644 scripts/managers/stop_n_go_manager.gd.uid diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 2d8ae09..f52ebd6 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -40,6 +40,8 @@ extends Control @onready var enable_timer_label = $LobbyPanel/TopBar/SettingsSection/EnableTimerLabel @onready var scarcity_option = $LobbyPanel/TopBar/SettingsSection/ScarcityOption @onready var scarcity_label = $LobbyPanel/TopBar/SettingsSection/ScarcityLabel +@onready var game_mode_option = $LobbyPanel/TopBar/SettingsSection/GameModeOption +@onready var game_mode_text_label = $LobbyPanel/TopBar/SettingsSection/GameModeTextLabel # UI References - Player Slots @onready var players_container = $LobbyPanel/PlayersContainer @@ -145,6 +147,9 @@ func _ready(): if scarcity_option: scarcity_option.item_selected.connect(_on_scarcity_selected) + if game_mode_option: + game_mode_option.item_selected.connect(_on_game_mode_selected) + # Connect LobbyManager signals LobbyManager.room_list_updated.connect(_on_room_list_updated) LobbyManager.room_joined.connect(_on_room_joined) @@ -160,6 +165,7 @@ func _ready(): LobbyManager.character_changed.connect(_on_character_changed) LobbyManager.area_changed.connect(_on_area_changed) LobbyManager.scarcity_mode_changed.connect(_on_scarcity_mode_changed) + LobbyManager.game_mode_changed.connect(_on_game_mode_changed) LobbyManager.player_list_changed.connect(_update_player_slots) # Connect NakamaManager signals @@ -354,6 +360,20 @@ func _on_scarcity_mode_changed(mode: String) -> void: if scarcity_label: scarcity_label.text = mode +func _on_game_mode_selected(index: int) -> void: + if not LobbyManager.is_host: return + var mode = game_mode_option.get_item_text(index) + LobbyManager.set_game_mode(mode) + +func _on_game_mode_changed(mode: String) -> void: + if game_mode_option: + for i in range(game_mode_option.item_count): + if game_mode_option.get_item_text(i) == mode: + game_mode_option.selected = i + break + if game_mode_text_label: + game_mode_text_label.text = mode + func _update_random_spawn_label(enabled: bool) -> void: if random_spawn_label: @@ -430,6 +450,11 @@ func _on_room_joined(room_data: Dictionary) -> void: area_right_btn.disabled = not is_host area_name_label.text = LobbyManager.get_selected_area() + # Game Mode Update + if game_mode_option: game_mode_option.visible = is_host + if game_mode_text_label: game_mode_text_label.visible = not is_host + _on_game_mode_changed(LobbyManager.game_mode) + _update_player_slots() connection_status.text = "Connected to room" diff --git a/scenes/lobby.tscn b/scenes/lobby.tscn index d6270f9..a0ff867 100644 --- a/scenes/lobby.tscn +++ b/scenes/lobby.tscn @@ -411,6 +411,29 @@ theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1) theme_override_font_sizes/font_size = 11 text = "Normal" +[node name="GameModeSpacer" type="Control" parent="LobbyPanel/TopBar/SettingsSection" unique_id=1261898888] +custom_minimum_size = Vector2(15, 0) +layout_mode = 2 + +[node name="GameModeOption" type="OptionButton" parent="LobbyPanel/TopBar/SettingsSection" unique_id=448678888] +custom_minimum_size = Vector2(105, 28) +layout_mode = 2 +theme_override_fonts/font = ExtResource("5_pc087") +theme_override_font_sizes/font_size = 11 +selected = 0 +item_count = 2 +popup/item_0/text = "Freemode" +popup/item_0/id = 0 +popup/item_1/text = "Stop n Go" +popup/item_1/id = 1 + +[node name="GameModeTextLabel" type="Label" parent="LobbyPanel/TopBar/SettingsSection" unique_id=191738888] +visible = false +layout_mode = 2 +theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1) +theme_override_font_sizes/font_size = 11 +text = "Freemode" + [node name="HostBanner" type="PanelContainer" parent="LobbyPanel" unique_id=1670701936] layout_mode = 1 anchors_preset = 5 diff --git a/scenes/main.gd b/scenes/main.gd index f8ccb28..47b155c 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -10,6 +10,7 @@ var goals_cycle_manager var screen_shake_manager var touch_controls var camera_context_manager +var stop_n_go_manager # Minimal local state var _connection_check_timer: float = 0.0 @@ -87,6 +88,13 @@ func _init_managers(): add_child(goals_cycle_manager) goals_cycle_manager.initialize(self) + # Stop n Go manager for phase-based gameplay + if LobbyManager.game_mode == "Stop n Go": + stop_n_go_manager = load("res://scripts/managers/stop_n_go_manager.gd").new() + stop_n_go_manager.name = "StopNGoManager" + add_child(stop_n_go_manager) + # No direct initialize() yet, but we'll call start_game_mode later + # Screen shake manager for impact feedback screen_shake_manager = load("res://scripts/managers/screen_shake.gd").new() screen_shake_manager.name = "ScreenShakeManager" @@ -516,7 +524,10 @@ func _start_game(): rpc("set_current_turn", next_player) # Start the global match timer (this also starts the first cycle) - if goals_cycle_manager: + if LobbyManager.game_mode == "Stop n Go": + if stop_n_go_manager: + stop_n_go_manager.start_game_mode() + elif goals_cycle_manager: var match_duration = LobbyManager.get_match_duration() goals_cycle_manager.start_match(float(match_duration)) @@ -711,7 +722,6 @@ func _create_tekton(pos: Vector2i, tekton_id: int, is_static: bool = false): # If Static, swap controller if is_static: # tekton.is_static_turret = true # Already set above - var old_controller = tekton.get_node_or_null("TektonController") if old_controller: old_controller.queue_free() @@ -724,7 +734,6 @@ func _create_tekton(pos: Vector2i, tekton_id: int, is_static: bool = false): print("[Main] Spawned Tekton at %s (ID: %d)" % [pos, tekton_id]) - func _precalculate_static_positions(): """Calculate and reserve Static Tekton positions early.""" if not multiplayer.is_server(): return @@ -803,12 +812,12 @@ func _create_static_setup(pos: Vector2i, tekton_id: int, shape_idx: int): # Position Stand if enhanced_gridmap: # Convert grid to world - var world_pos = Vector3(pos.x + 0.5, 0, pos.y + 0.5) + var world_pos = Vector3(pos.x + 0.5, 0, pos.y + 0.5) if "cell_size" in enhanced_gridmap: world_pos = Vector3( - pos.x * enhanced_gridmap.cell_size.x + enhanced_gridmap.cell_size.x/2, - 0, - pos.y * enhanced_gridmap.cell_size.z + enhanced_gridmap.cell_size.z/2 + pos.x * enhanced_gridmap.cell_size.x + enhanced_gridmap.cell_size.x / 2, + 0, + pos.y * enhanced_gridmap.cell_size.z + enhanced_gridmap.cell_size.z / 2 ) stand.global_position = world_pos @@ -843,7 +852,6 @@ func _create_static_setup(pos: Vector2i, tekton_id: int, shape_idx: int): _create_tekton(pos, tekton_id, true) - # ============================================================================= # Player Management # ============================================================================= @@ -980,6 +988,13 @@ func sync_game_start(player_list: Array, is_turn_based: bool): TurnManager.turn_based_mode = is_turn_based GameStateManager.start_game() + if LobbyManager.game_mode == "Stop n Go": + if not stop_n_go_manager: + stop_n_go_manager = load("res://scripts/managers/stop_n_go_manager.gd").new() + stop_n_go_manager.name = "StopNGoManager" + add_child(stop_n_go_manager) + stop_n_go_manager.activate_client_side() + # Initialize leaderboard for all peers (after a delay to ensure players loaded) call_deferred("_deferred_init_leaderboard") @@ -1354,6 +1369,9 @@ func sync_grid_item(x: int, y: int, z: int, item: int): # For simplicity, we trust the grid syncs via normal mechanisms or initial state. func randomize_game_grid(): + if LobbyManager.game_mode == "Stop n Go": + return # Stop n Go manages its own arena setup + var enhanced_gridmap = $EnhancedGridMap if enhanced_gridmap: # Randomize Floor 1 using ScarcityController @@ -1364,6 +1382,19 @@ func randomize_game_grid(): # For now, let's assume server authority + sync on connect handles it, or add sync loop if critical. pass +@rpc("authority", "call_local", "reliable") +func sync_full_grid_data_stop_n_go(floor0: PackedInt32Array, floor1: PackedInt32Array, cols: int, rows: int): + print("[Main] Stop n Go Sync: Changing grid size to %dx%d" % [cols, rows]) + var gridmap = $EnhancedGridMap + if gridmap: + gridmap.columns = cols + gridmap.rows = rows + gridmap.clear() + gridmap.set_floor_data(0, floor0) + gridmap.set_floor_data(1, floor1) + gridmap.update_grid_data() + gridmap.initialize_astar() + @rpc("any_peer") func request_full_grid_sync(): if multiplayer.is_server(): @@ -1382,7 +1413,7 @@ func request_full_grid_sync(): func sync_full_grid_data(data: PackedInt32Array): print("[Main] sync_full_grid_data received. Items: %d" % (data.size() / 3)) var enhanced_gridmap = $EnhancedGridMap - if not enhanced_gridmap: + if not enhanced_gridmap: print("[Main] Error: EnhancedGridMap not found!") return @@ -1459,6 +1490,23 @@ func _on_global_timer_updated(time_remaining: float): var minutes = int(time_remaining) / 60 var seconds = int(time_remaining) % 60 timer_label.text = "%d:%02d" % [minutes, seconds] +@rpc("any_peer", "call_local", "reliable") +func sync_game_end_stop_n_go(winner_id: int): + print("[STOP n GO] Game ended! Winner: ", winner_id) + var winner_name = "Player " + str(winner_id) + var player_node = get_node_or_null(str(winner_id)) + if player_node: + winner_name = player_node.display_name + + # Broadcast win + add_message_to_bar("WINNER", winner_name + " Reached the Finish Line!", MessageType.GOAL) + + # Stop logic + if stop_n_go_manager: + stop_n_go_manager.is_active = false + + # Trigger match end + _on_match_ended() func _on_match_ended(): """Called when the global match timer ends - show game over screen.""" @@ -1555,7 +1603,7 @@ func _show_game_over_panel(): Color(0.5, 0.5, 0.5), # 5th Color(0.5, 0.5, 0.5), # 6th Color(0.5, 0.5, 0.5), # 7th - Color(0.5, 0.5, 0.5) # 8th + Color(0.5, 0.5, 0.5) # 8th ] var rank_emojis = ["🥇", "🥈", "🥉", "4th", "5th", "6th", "7th", "8th"] @@ -1723,7 +1771,6 @@ func _render_leaderboard_entries(sorted_player_data: Array): # If local player is outside top 3 (index > 2), show them in 4th slot # But if they are exactly 4th (index 3), it's the same as showing 4th place. # If they are 5th (index 4) or worse, we replace 4th place with them. - if my_index > 3: # Show local player items_to_display.append({"data": sorted_player_data[my_index], "rank": my_index + 1}) diff --git a/scenes/player.gd b/scenes/player.gd index 0d55bad..00dd57b 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -832,6 +832,67 @@ func _find_valid_drop_position() -> Vector2i: return Vector2i(-1, -1) +@rpc("any_peer", "call_local") +func on_stop_phase_violation(): + """Moving during STOP phase makes you lose and scatter tiles.""" + if not multiplayer.is_server(): return + + print("[STOP n GO] Violation by player %s! Scattering tiles." % name) + + # Stun effect + apply_stagger(2.0) + + # Scatter items + var items_to_scatter = [] + for i in range(playerboard.size()): + if playerboard[i] != -1: + items_to_scatter.append(playerboard[i]) + playerboard[i] = -1 + + if items_to_scatter.is_empty(): + return + + rpc("sync_playerboard", playerboard) + + # Find multiple valid drop positions around the player + var drop_positions = _find_multiple_drop_positions(items_to_scatter.size()) + + for i in range(items_to_scatter.size()): + var item_id = items_to_scatter[i] + var pos = drop_positions[i] if i < drop_positions.size() else _find_valid_drop_position() + + if pos != Vector2i(-1, -1): + var cell = Vector3i(pos.x, 1, pos.y) + rpc("sync_grid_item", cell.x, cell.y, cell.z, item_id) + + NotificationManager.send_message(self, "STOP VIOLATION! Tiles scattered!", NotificationManager.MessageType.WARNING) + +func _find_multiple_drop_positions(count: int) -> Array: + var positions = [] + var neighbors = enhanced_gridmap.get_neighbors(current_position, 0) + neighbors.shuffle() + + # Also consider neighbors of neighbors (radius 2) for more scattering + var candidates = [] + for n in neighbors: + candidates.append(n.position) + var n2 = enhanced_gridmap.get_neighbors(n.position, 0) + for nn in n2: + if not nn.position in candidates: + candidates.append(nn.position) + + candidates.shuffle() + + for pos in candidates: + if positions.size() >= count: break + + var item_cell = Vector3i(pos.x, 1, pos.y) + if enhanced_gridmap.get_cell_item(item_cell) == -1: + if not is_position_occupied(pos): + positions.append(pos) + + return positions + # ============================================================================= # Targeting Action # ============================================================================= @@ -1200,6 +1261,15 @@ func start_movement_along_path(path: Array, clear_visual: bool = true): if is_carrying_tekton and is_instance_valid(carried_tekton): carried_tekton.current_position = current_position + # Stop n Go Win Check + if LobbyManager.game_mode == "Stop n Go": + var sng_main = get_tree().root.get_node_or_null("Main") + if sng_main: + var sng_manager = sng_main.get_node_or_null("StopNGoManager") + if sng_manager and sng_manager.has_method("check_win_condition"): + if sng_manager.check_win_condition(name.to_int(), current_position): + sng_main.rpc("sync_game_end_stop_n_go", name.to_int()) + # FORCE SNAP: Update target visual position to the perfect grid center # This ensures that when interpolation resumes (in _process), it pulls to the correct spot target_visual_position = grid_to_world(current_position) diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index 0eee065..9ec8094 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -41,9 +41,14 @@ signal scarcity_mode_changed(mode: String) # Character and area selection var available_characters: Array[String] = ["Copper", "Dabro", "Gatot", "Pip", "Random"] var available_areas: Array[String] = ["Desert", "Forest", "City", "Factory"] +var available_game_modes: Array[String] = ["Freemode", "Stop n Go"] var selected_area: String = "Desert" # Host-controlled +var game_mode: String = "Freemode" # Host-controlled var local_character_index: int = 0 # Local player's character index +# Signals +signal game_mode_changed(mode: String) + # Ready to start game check var _all_ready: bool = false @@ -345,6 +350,29 @@ func sync_area(area_name: String) -> void: selected_area = area_name emit_signal("area_changed", area_name) +# ============================================================================= +# Game Mode Selection (Host Only) +# ============================================================================= + +func set_game_mode(mode: String) -> void: + """Host sets the game mode. Syncs to all clients.""" + if not is_host: + push_warning("Only host can change game mode") + return + + if mode not in available_game_modes: + push_error("Invalid game mode: " + mode) + return + + game_mode = mode + rpc("sync_game_mode", mode) + +@rpc("authority", "call_local", "reliable") +func sync_game_mode(mode: String) -> void: + """Sync game mode selection from host to clients.""" + game_mode = mode + emit_signal("game_mode_changed", mode) + func start_game() -> void: """Host triggers game start (transitions all players to main.tscn).""" if not is_host: @@ -361,6 +389,8 @@ func start_game() -> void: rpc("sync_enable_cycle_timer", enable_cycle_timer) # Sync scarcity mode rpc("sync_scarcity_mode", scarcity_mode) + # Sync game mode + rpc("sync_game_mode", game_mode) # Notify all clients to start rpc("_on_game_starting") @@ -484,3 +514,4 @@ func reset() -> void: selected_area = "Desert" local_character_index = 0 # Default to "Copper" enable_cycle_timer = false + game_mode = "Freemode" diff --git a/scripts/managers/player_movement_manager.gd b/scripts/managers/player_movement_manager.gd index 44da21a..9925319 100644 --- a/scripts/managers/player_movement_manager.gd +++ b/scripts/managers/player_movement_manager.gd @@ -54,6 +54,14 @@ func simple_move_to(grid_position: Vector2i) -> bool: if player.get("is_frozen"): return false + # Stop n Go Mode Violation Check + var main = player.get_tree().root.get_node_or_null("Main") + if main: + var manager = main.get_node_or_null("StopNGoManager") + if manager and manager.has_method("check_movement_violation"): + if manager.check_movement_violation(player.name.to_int(), player.current_position, grid_position): + return false + var distance: int if use_diagonal_movement: distance = max(abs(grid_position.x - player.current_position.x), abs(grid_position.y - player.current_position.y)) @@ -100,15 +108,17 @@ func simple_move_to(grid_position: Vector2i) -> bool: rotate_towards_target(grid_position) - if player.is_multiplayer_authority() and player.has_method("sync_walk_animation"): - player.rpc("sync_walk_animation") + if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: + if player.has_method("sync_walk_animation"): + player.rpc("sync_walk_animation") var path = [Vector2(player.current_position.x, player.current_position.y), Vector2(grid_position.x, grid_position.y)] path.pop_front() current_move_direction = grid_position - player.current_position - player.rpc("start_movement_along_path", path, not (player.is_bot or player.is_in_group("Bots"))) + if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: + player.rpc("start_movement_along_path", path, not (player.is_bot or player.is_in_group("Bots"))) return true @@ -419,8 +429,8 @@ func _is_position_blocked_by_physics(target_pos: Vector2i) -> bool: var space_state = player.get_world_3d().direct_space_state var center_x = target_pos.x + 0.5 var center_z = target_pos.y + 0.5 - var from = Vector3(center_x, 1.0, center_z) - var to = Vector3(center_x, 0.1, center_z) + var from = Vector3(center_x, 1.0, center_z) + var to = Vector3(center_x, 0.1, center_z) var query = PhysicsRayQueryParameters3D.create(from, to) query.collide_with_areas = false diff --git a/scripts/managers/playerboard_manager.gd b/scripts/managers/playerboard_manager.gd index e1f02aa..ae6451d 100644 --- a/scripts/managers/playerboard_manager.gd +++ b/scripts/managers/playerboard_manager.gd @@ -90,6 +90,13 @@ func grab_item(grid_position: Vector2i) -> bool: else: # Normal Tile: Add to playerboard player.playerboard[target_slot] = item + + # Stop n Go Mission Progress + var sng_main = player.get_tree().root.get_node_or_null("Main") + if sng_main: + var sng_manager = sng_main.get_node_or_null("StopNGoManager") + if sng_manager and sng_manager.has_method("update_mission_progress"): + sng_manager.update_mission_progress(player.name.to_int(), item) # Update UI immediately for responsiveness @@ -174,6 +181,11 @@ func _execute_grab(grid_pos: Vector2i, cell: Vector3i, item_id: int): # Do not add to playerboard else: player.playerboard[target_slot] = item_id + + # Stop n Go Mission Progress + var sng_manager = main.get_node_or_null("StopNGoManager") + if sng_manager and sng_manager.has_method("update_mission_progress"): + sng_manager.update_mission_progress(player.name.to_int(), item_id) # 3c. Broadcast the new playerboard state to all clients var peer_id = player.name.to_int() diff --git a/scripts/managers/special_tiles_manager.gd b/scripts/managers/special_tiles_manager.gd index 5099d21..2526870 100644 --- a/scripts/managers/special_tiles_manager.gd +++ b/scripts/managers/special_tiles_manager.gd @@ -165,7 +165,7 @@ func add_powerup_from_item(item_id: int): emit_signal("powerup_unlocked", effect, powerup_levels[effect]) print("Player %s leveled up %s to Lvl %d" % [player.name, SpecialEffect.keys()[effect], powerup_levels[effect]]) - if player.is_multiplayer_authority(): + if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: rpc("sync_inventory_add", effect, powerup_levels[effect]) @rpc("any_peer", "call_local", "reliable") @@ -254,7 +254,7 @@ func activate_effect(effect: int, target_player: Node3D = null): _execute_invisible_mode(player) # Play generic cast animation or sound? - if player.is_multiplayer_authority(): + if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: player.rpc("trigger_screen_shake", "light") # Sync cooldown to others not strictly needed unless UI shows it? # Probably local UI only. @@ -308,7 +308,8 @@ func _execute_area_freeze(center_pos: Vector2i = Vector2i.ZERO): # If inside square radius if dx <= radius and dy <= radius: - p.rpc("apply_slow_effect", FREEZE_SLOW_DURATION) + if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: + p.rpc("apply_slow_effect", FREEZE_SLOW_DURATION) NotificationManager.send_message(p, "Caught in Freeze Zone!", NotificationManager.MessageType.WARNING) if p != player: # Don't score for freezing self (unless desired?) - Assuming enemies hit_count += 1 @@ -336,7 +337,8 @@ func _execute_area_freeze(center_pos: Vector2i = Vector2i.ZERO): var current_item = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y)) if current_item != 4: # 4 is Wall Block # Use Item 12 (Blue Freeze Tile) on Layer 0 (Floor) - main.rpc("sync_grid_item", pos.x, 0, pos.y, 12) + if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: + main.rpc("sync_grid_item", pos.x, 0, pos.y, 12) # Cleanup visual timer (managed locally by author) get_tree().create_timer(FREEZE_SLOW_DURATION).timeout.connect(func(): @@ -404,7 +406,7 @@ func _execute_block_floor(target_pos: Vector2i): if player.is_multiplayer_authority(): var main = player.get_tree().get_root().get_node_or_null("Main") - if main: + if main and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: main.rpc("sync_grid_item", block_pos.x, block_pos.y, block_pos.z, 4) # Record for restoration @@ -462,7 +464,7 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true): if player.is_multiplayer_authority(): var main = player.get_tree().get_root().get_node_or_null("Main") - if main: + if main and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: main.rpc("sync_grid_item", cell.x, cell.y, cell.z, item_id) diff --git a/scripts/managers/stop_n_go_manager.gd b/scripts/managers/stop_n_go_manager.gd new file mode 100644 index 0000000..5f89cd1 --- /dev/null +++ b/scripts/managers/stop_n_go_manager.gd @@ -0,0 +1,265 @@ +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 -> {tile_id: count, current: count} +var finish_line_x: int = 21 # Right side of the map for win condition + +# Tile IDs (Assumed based on analysis) +const TILE_WALKABLE = 0 +const TILE_SAFE = 2 # Green Safe Zone +const TILE_OBSTACLE = 4 # Black Obstacle + +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 Hearts" + 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] + mission_label.text = "Tiles: %d / %d" % [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(): + activate_client_side() + if multiplayer.is_server(): + _setup_arena() + _assign_missions() + _start_phase(Phase.GO) + +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: + 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): + 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 Accurate Horizontal Arena...") + # Set Size for Stop n Go + gridmap.columns = 22 + gridmap.rows = 10 + + # Clear existing items on all layers + gridmap.clear() + + # Create bands based on X (Horizontal Progress) + for x in range(gridmap.columns): + var tile_id = TILE_WALKABLE + # Green Safe Zones at X=7,8 and X=14,15 + if x == 7 or x == 8 or x == 14 or x == 15: + tile_id = TILE_SAFE + + for z in range(gridmap.rows): + gridmap.set_cell_item(Vector3i(x, 0, z), tile_id) + + # Place Specific Obstacles (Black Bars) to match user images + # Lane 1 (X=2..6) - Vertical bar at X=4 + for z in range(0, 4): + gridmap.set_cell_item(Vector3i(4, 0, z), TILE_OBSTACLE) + for z in range(6, 10): + gridmap.set_cell_item(Vector3i(4, 0, z), TILE_OBSTACLE) + + # Lane 2 (X=9..13) - Vertical bar at X=11 + for z in range(3, 7): + gridmap.set_cell_item(Vector3i(11, 0, z), TILE_OBSTACLE) + + # Lane 3 (X=16..20) - L-shape at top + for z in range(0, 3): + gridmap.set_cell_item(Vector3i(18, 0, z), TILE_OBSTACLE) + gridmap.set_cell_item(Vector3i(19, 0, 2), TILE_OBSTACLE) + gridmap.set_cell_item(Vector3i(20, 0, 2), TILE_OBSTACLE) + + # Another bar at bottom of Lane 3 + for z in range(6, 9): + gridmap.set_cell_item(Vector3i(18, 0, z), TILE_OBSTACLE) + + gridmap.update_grid_data() + gridmap.initialize_astar() + + # Spawn tiles for mission (Heart = 7) + 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") + var count = 0 + while count < 40: # Spawn plenty of hearts + var x = randi() % gridmap.columns + var z = randi() % gridmap.rows + # Only spawn in walkable areas, not safe zones or start/finish + if gridmap.get_cell_item(Vector3i(x, 0, z)) == TILE_WALKABLE: + if gridmap.get_cell_item(Vector3i(x, 1, z)) == -1: + gridmap.set_cell_item(Vector3i(x, 1, z), ScarcityModel.TILE_HEART) + count += 1 + +func _assign_missions(): + # Each player needs to collect 3 specific tiles (e.g. Heart) + var players = GameStateManager.players + for p_id in players: + player_missions[p_id] = { + "target_tile": ScarcityModel.TILE_HEART, + "required": 3, + "current": 0 + } + if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: + 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: + 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: + if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: + player_node.rpc("on_stop_phase_violation") + 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) + + if multiplayer.is_server() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: + 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 diff --git a/scripts/managers/stop_n_go_manager.gd.uid b/scripts/managers/stop_n_go_manager.gd.uid new file mode 100644 index 0000000..78f790a --- /dev/null +++ b/scripts/managers/stop_n_go_manager.gd.uid @@ -0,0 +1 @@ +uid://kiv6fpqym1c0 diff --git a/scripts/nakama_manager.gd b/scripts/nakama_manager.gd index 158bbcb..f8e501c 100644 --- a/scripts/nakama_manager.gd +++ b/scripts/nakama_manager.gd @@ -51,15 +51,16 @@ func connect_to_nakama_async(email: String = "", password: String = "") -> bool: # 1. Authenticate if email == "": var device_id = OS.get_unique_id() - # For testing, append a random number to device ID to simulate unique users on one machine - device_id = device_id + str(randi()) + # Use a more stable ID for testing instead of randi() every call + # If you need multiple clients on one machine, consider a command line arg or config session = await client.authenticate_device_async(device_id) else: session = await client.authenticate_email_async(email, password) if session.is_exception(): - printerr("Auth Error: ", session.get_exception().message) - emit_signal("connection_failed", session.get_exception().message) + var err = session.get_exception() + printerr("[NakamaManager] Auth Error: %s (Code: %s)" % [err.message, err.status_code]) + emit_signal("connection_failed", err.message) return false # 2. Connect Socket @@ -67,8 +68,9 @@ func connect_to_nakama_async(email: String = "", password: String = "") -> bool: var socket_result = await socket.connect_async(session) if socket_result.is_exception(): - printerr("Socket Error: ", socket_result.get_exception().message) - emit_signal("connection_failed", socket_result.get_exception().message) + var err = socket_result.get_exception() + printerr("[NakamaManager] Socket Error: %s (Code: %s)" % [err.message, err.status_code]) + emit_signal("connection_failed", err.message) return false # 3. Initialize Multiplayer Bridge