From e90cbfe246b2debac41ca620693eb59a57e53869 Mon Sep 17 00:00:00 2001 From: Yogi Wiguna Date: Thu, 19 Feb 2026 17:29:14 +0800 Subject: [PATCH] feat: Implement Tekton roaming NPC with movement, combat, carry, throw, and knock mechanics. --- .../enhanced_gridmap/meshlibrary/default.tres | 2 +- scenes/lobby.tscn | 1 - scenes/main.gd | 32 +++- scenes/player.gd | 62 +++++-- scripts/managers/goals_cycle_manager.gd | 3 +- scripts/managers/player_action_manager.gd | 2 +- scripts/managers/player_movement_manager.gd | 28 ++- scripts/managers/powerup_manager.gd | 8 +- scripts/managers/stop_n_go_manager.gd | 166 +++++++++++++----- scripts/static_tekton_controller.gd | 8 +- scripts/tekton.gd | 7 +- 11 files changed, 235 insertions(+), 84 deletions(-) diff --git a/addons/enhanced_gridmap/meshlibrary/default.tres b/addons/enhanced_gridmap/meshlibrary/default.tres index dfdd080..404ca64 100644 --- a/addons/enhanced_gridmap/meshlibrary/default.tres +++ b/addons/enhanced_gridmap/meshlibrary/default.tres @@ -94,7 +94,7 @@ item/1/mesh_cast_shadow = 1 item/1/shapes = [] item/1/navigation_mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) item/1/navigation_layers = 1 -item/2/name = "start" +item/2/name = "safe_zone" item/2/mesh = ExtResource("3_8v5xv") item/2/mesh_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) item/2/mesh_cast_shadow = 1 diff --git a/scenes/lobby.tscn b/scenes/lobby.tscn index a0ff867..4140237 100644 --- a/scenes/lobby.tscn +++ b/scenes/lobby.tscn @@ -221,7 +221,6 @@ layout_mode = 2 text = "PROFILE" [node name="LobbyPanel" type="Control" parent="." unique_id=1745714811] -visible = false layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 diff --git a/scenes/main.gd b/scenes/main.gd index 47b155c..cf62fa3 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -486,11 +486,20 @@ func _setup_client_game(): # Wait shorter time for host to be ready, then request full sync to correct positions/state await get_tree().create_timer(1.0).timeout - rpc_id(1, "request_full_player_sync", my_id) - # Only request grid if we remain connected if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: - rpc_id(1, "request_full_grid_sync") + # Ensure we see the server (Peer 1) + if 1 in multiplayer.get_peers(): + rpc_id(1, "request_full_player_sync", my_id) + rpc_id(1, "request_full_grid_sync") + else: + print("Client: Connected but Peer 1 not found yet. Retrying in 1s...") + await get_tree().create_timer(1.0).timeout + if 1 in multiplayer.get_peers(): + rpc_id(1, "request_full_player_sync", my_id) + rpc_id(1, "request_full_grid_sync") + + func _auto_start_from_lobby(): """Called when main.tscn is loaded from lobby - game is already connected.""" @@ -516,6 +525,9 @@ func _auto_start_from_lobby(): func _start_game(): if multiplayer.is_server(): + # Allow socket/peer to stabilize before blasting RPCs + await get_tree().create_timer(2.0).timeout + GameStateManager.start_game() rpc("sync_game_start", GameStateManager.players, TurnManager.turn_based_mode) if TurnManager.turn_based_mode: @@ -525,6 +537,7 @@ func _start_game(): # Start the global match timer (this also starts the first cycle) if LobbyManager.game_mode == "Stop n Go": + # Only Server starts the mode logic (arena setup, missions, etc) if stop_n_go_manager: stop_n_go_manager.start_game_mode() elif goals_cycle_manager: @@ -693,7 +706,10 @@ func spawn_tekton_npc(): # Generate a consistent ID/Name for sync (add index to ensure uniqueness) var tekton_id = Time.get_ticks_msec() + spawned_count _create_tekton(valid_pos, tekton_id) - rpc("sync_spawn_tekton", valid_pos, tekton_id) + + if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: + if multiplayer.get_peers().size() > 0: + rpc("sync_spawn_tekton", valid_pos, tekton_id) spawned_count += 1 print("[Main] Spawned Tekton %d at %s" % [spawned_count, valid_pos]) @@ -752,6 +768,11 @@ func _precalculate_static_positions(): func spawn_static_tektons(): """Spawn fixed static tektons using StaticTektonManager.""" if not multiplayer.is_server(): return + + # Disable for Stop n Go mode + if LobbyManager.game_mode == "Stop n Go": + return + var enhanced_gridmap = $EnhancedGridMap if not enhanced_gridmap: return @@ -1679,7 +1700,8 @@ func _deferred_init_leaderboard(): # Request leaderboard sync from server for accurate data if not multiplayer.is_server(): - rpc_id(1, "request_leaderboard_sync") + if 1 in multiplayer.get_peers(): + rpc_id(1, "request_leaderboard_sync") else: # Server can update directly _update_leaderboard_display() diff --git a/scenes/player.gd b/scenes/player.gd index 00dd57b..4f436a4 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -22,12 +22,16 @@ var display_name: String: if name_label: name_label.text = _display_name - # Sync to other peers if we are authority - if is_multiplayer_authority() and is_inside_tree(): + # Sync to other peers if we are authority and connected + if is_multiplayer_authority() and is_inside_tree() and can_rpc(): rpc("sync_display_name", _display_name) get: return _display_name +# Helper to check network status +func can_rpc() -> bool: + return multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED + # Special effect states var is_frozen: bool = false var is_invisible: bool = false @@ -55,7 +59,7 @@ var is_attack_mode: bool = false: _apply_tint_recursive(self , Color.WHITE) # Sync to others if we are the authority - if is_multiplayer_authority(): + if is_multiplayer_authority() and can_rpc(): rpc("sync_attack_mode", is_attack_mode) @rpc("any_peer", "call_local", "reliable") @@ -199,7 +203,7 @@ func _ready(): pointer.visible = is_multiplayer_authority() # Sync name to other peers if this is our local player or a bot we own - if is_multiplayer_authority(): + if is_multiplayer_authority() and can_rpc(): rpc("sync_display_name", display_name) # Wait briefly to ensure proper scene setup and server recognition @@ -239,25 +243,21 @@ func _ready(): current_position = _find_random_spawn_position() update_player_position(current_position) spawn_point_selected = true - rpc("set_spawn_position", current_position) - rpc("notify_spawn_selected", current_position) + if can_rpc(): + rpc("set_spawn_position", current_position) + rpc("notify_spawn_selected", current_position) # Assign bot character (deterministic based on ID to match lobby preview) - # Bot IDs start from 2 (host is 1) - # Lobby slots are 0-indexed in UI loop, but bots fill empty slots. - # Use name.to_int() because all bots have authority 1 (Server) var bot_id_val = name.to_int() var bot_characters = ["Bob", "Gatot", "Masbro", "Oldpop"] - # Map bot ID to character index. Bot 2 -> Index 1. Bot 3 -> Index 2. - # Formula: (bot_id - 1) % size var char_index = (bot_id_val - 1) % bot_characters.size() var bot_char_name = bot_characters[char_index] set_character(bot_char_name) - if is_multiplayer_authority(): + if is_multiplayer_authority() and can_rpc(): rpc("sync_character", bot_char_name) # Sync bot status to network - if is_multiplayer_authority(): + if is_multiplayer_authority() and can_rpc(): rpc("sync_bot_status", true) # Continue to manager initialization... @@ -282,7 +282,7 @@ func _ready(): current_position.y * 1 + 1 * 0.5 ) target_visual_position = global_position - if is_multiplayer_authority(): + if is_multiplayer_authority() and can_rpc(): rpc("sync_position", current_position) else: target_visual_position = global_position @@ -407,7 +407,7 @@ func _setup_character() -> void: set_character(character_name) # If this is our local player, also sync to other clients for late joiners - if is_multiplayer_authority(): + if is_multiplayer_authority() and can_rpc(): rpc("sync_character", character_name) # ============================================================================= @@ -987,7 +987,8 @@ func _process(delta): _verify_timer += delta if _verify_timer >= 3.0: _verify_timer = 0.0 - rpc("ping_existence") + if can_rpc(): + rpc("ping_existence") else: # Client-side visual smoothing # Only interpolate if NOT running a movement tween, OR if the drift is large (teleport/snap) @@ -1024,7 +1025,8 @@ var last_sent_position: Vector3 func _physics_process(delta): if is_multiplayer_authority(): if global_position.distance_squared_to(last_sent_position) > 0.001: - rpc("remote_set_position", global_position) + if can_rpc(): + rpc("remote_set_position", global_position) last_sent_position = global_position # NOTE: Finish line checking removed - game uses cycle-based goals system now @@ -1158,6 +1160,30 @@ func _find_random_spawn_position() -> Vector2i: print("Warning: No gridmap for random spawn") return Vector2i.ZERO + # Special handling for Stop n Go mode (22x10 Grid) + if LobbyManager.game_mode == "Stop n Go": + var available_positions = [] + # Scan the 22x10 grid + for x in range(22): + for z in range(10): + var pos = Vector2i(x, z) + # Check if position is walkable (not a wall/obstacle) + # We check Floor 0 item. Assuming walls are identifiable. + # In setup_arena, walls are TILE_OBSTACLE (4). + # We should check if it is NOT TILE_OBSTACLE. + var item = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z)) + # Assuming 4 is obstacle, and -1 is void. 0 is walkable, 2 is safe zone. + if item != -1 and item != 4: + if not is_position_occupied(pos): + available_positions.append(pos) + + if available_positions.size() > 0: + var rng = RandomNumberGenerator.new() + rng.randomize() + return available_positions[rng.randi() % available_positions.size()] + return Vector2i(10, 5) # Fallback center + + var available_positions = [] # Scan the grid for valid walkable floor tiles that are not occupied @@ -1893,7 +1919,7 @@ func grab_tekton(): # Find nearby Tekton var tekton = _find_nearby_tekton() if tekton: - if is_multiplayer_authority(): + if is_multiplayer_authority() and can_rpc(): rpc("sync_grab_tekton", tekton.get_path()) @rpc("any_peer", "call_local", "reliable") diff --git a/scripts/managers/goals_cycle_manager.gd b/scripts/managers/goals_cycle_manager.gd index 581d615..d47269b 100644 --- a/scripts/managers/goals_cycle_manager.gd +++ b/scripts/managers/goals_cycle_manager.gd @@ -288,7 +288,8 @@ func add_score(peer_id: int, amount: int): player_scores[peer_id] += amount # Sync - rpc("sync_player_score", peer_id, player_scores[peer_id]) + if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: + rpc("sync_player_score", peer_id, player_scores[peer_id]) print("[GoalsCycle] Added %d points to Player %d. Total: %d" % [amount, peer_id, player_scores[peer_id]]) diff --git a/scripts/managers/player_action_manager.gd b/scripts/managers/player_action_manager.gd index 5c7c1ab..7b3ba77 100644 --- a/scripts/managers/player_action_manager.gd +++ b/scripts/managers/player_action_manager.gd @@ -60,7 +60,7 @@ func after_action_completed(): main.update_all_players_boards() # Sync playerboard (Bots DO need to sync their board logic, just not update local UI) - if player.is_multiplayer_authority(): + if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc(): var main = player.get_tree().get_root().get_node_or_null("Main") if main: main.rpc("sync_playerboard", player.name.to_int(), player.playerboard) diff --git a/scripts/managers/player_movement_manager.gd b/scripts/managers/player_movement_manager.gd index 9925319..a275f37 100644 --- a/scripts/managers/player_movement_manager.gd +++ b/scripts/managers/player_movement_manager.gd @@ -38,7 +38,7 @@ func rotate_towards_target(target_pos: Vector2i): if direction != Vector3.ZERO: target_rotation = atan2(direction.x, direction.z) # Sync rotation to other clients - if player.is_multiplayer_authority(): + if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc(): player.rpc("sync_rotation", target_rotation) func simple_move_to(grid_position: Vector2i) -> bool: @@ -108,7 +108,9 @@ func simple_move_to(grid_position: Vector2i) -> bool: rotate_towards_target(grid_position) - if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: + rotate_towards_target(grid_position) + + if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc(): if player.has_method("sync_walk_animation"): player.rpc("sync_walk_animation") @@ -117,7 +119,7 @@ func simple_move_to(grid_position: Vector2i) -> bool: current_move_direction = grid_position - player.current_position - if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: + if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc(): player.rpc("start_movement_along_path", path, not (player.is_bot or player.is_in_group("Bots"))) return true @@ -139,9 +141,18 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool: # === SUPER PUSH (Attack Mode) === print("Player %s SUPER PUSHING %s!" % [player.name, other_player.name]) + # SAFE ZONE PROTECTION + # Columns 6, 7, 8 and 14, 15, 16 are Safe Zones + var safe_columns = [6, 7, 8, 14, 15, 16] + if target_pos.x in safe_columns: + print(" - Attack BLOCKED by Safe Zone!") + NotificationManager.send_message(player, "Cannot Attack in Safe Zone!", NotificationManager.MessageType.WARNING) + return false + # 1. Drop Victim's Tiles if other_player.has_method("drop_all_tiles"): - other_player.rpc("drop_all_tiles") # Sync drop + if player.has_method("can_rpc") and player.can_rpc(): + other_player.rpc("drop_all_tiles") # Sync drop # 2. Spawn PowerUps around Victim # We delegate this to the attacker's SpecialTilesManager to handle the spawning authority @@ -158,15 +169,18 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool: not _is_position_blocked_by_physics(pushed_to_pos): # Valid push var push_path = [Vector2(pushed_to_pos.x, pushed_to_pos.y)] - other_player.rpc("start_movement_along_path", push_path, false) + if player.has_method("can_rpc") and player.can_rpc(): + other_player.rpc("start_movement_along_path", push_path, false) other_player.target_position = pushed_to_pos # Logical update # Apply stun/freeze effect as requested (same as wall stagger) - other_player.rpc("apply_stagger", 1.5) + if player.has_method("can_rpc") and player.can_rpc(): + other_player.rpc("apply_stagger", 1.5) else: # Wall/Blocked -> Stagger in place - other_player.rpc("apply_stagger", 1.5) + if player.has_method("can_rpc") and player.can_rpc(): + other_player.rpc("apply_stagger", 1.5) # 4. Consume Boost (Full) - One hit per charge if player.powerup_manager: diff --git a/scripts/managers/powerup_manager.gd b/scripts/managers/powerup_manager.gd index d6196b7..689541a 100644 --- a/scripts/managers/powerup_manager.gd +++ b/scripts/managers/powerup_manager.gd @@ -74,7 +74,7 @@ func _on_boost_full(): NotificationManager.send_message(player, NotificationManager.MESSAGES.ATTACK_MODE_READY, NotificationManager.MessageType.POWERUP) print("[PowerUp] Player %s Boost Full! Ready for Attack Mode." % player.name) - if player.is_multiplayer_authority(): + if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc(): rpc("sync_boost", current_boost) func reset_boost(): @@ -83,7 +83,7 @@ func reset_boost(): emit_signal("points_changed", 0, int(MAX_BOOST)) emit_signal("boost_reset") - if player.is_multiplayer_authority(): + if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc(): rpc("sync_boost", 0.0) # ============================================================================= @@ -173,7 +173,7 @@ func consume_boost(amount: float): emit_signal("points_changed", int(current_boost), int(MAX_BOOST)) - if player.is_multiplayer_authority(): + if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc(): rpc("sync_boost", current_boost) func spawn_boost_reward() -> bool: @@ -207,6 +207,6 @@ func add_goal_completion_reward(): # Optional: Notify user of difficulty increase? - if multiplayer.is_server(): + if multiplayer.is_server() and player.has_method("can_rpc") and player.can_rpc(): rpc("sync_boost_level", current_level) diff --git a/scripts/managers/stop_n_go_manager.gd b/scripts/managers/stop_n_go_manager.gd index 5f89cd1..95bf7db 100644 --- a/scripts/managers/stop_n_go_manager.gd +++ b/scripts/managers/stop_n_go_manager.gd @@ -16,13 +16,13 @@ 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 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 (Assumed based on analysis) +# Tile IDs const TILE_WALKABLE = 0 const TILE_SAFE = 2 # Green Safe Zone -const TILE_OBSTACLE = 4 # Black Obstacle +const TILE_OBSTACLE = 4 # Wall var hud_layer: CanvasLayer var phase_label: Label @@ -66,7 +66,7 @@ func _setup_hud(): inner_vbox.add_child(phase_label) mission_label = Label.new() - mission_label.text = "MISSION: Collect 3 Hearts" + mission_label.text = "MISSION: Collect 3 Items" mission_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT inner_vbox.add_child(mission_label) @@ -94,7 +94,16 @@ func _update_hud_visuals(): 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"]] + # 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) @@ -106,11 +115,18 @@ func activate_client_side(): set_process(true) func start_game_mode(): - activate_client_side() + # 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 @@ -118,11 +134,14 @@ func _start_phase(phase: Phase): 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) + 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 @@ -130,7 +149,26 @@ func _setup_arena(): var gridmap = get_node("/root/Main/EnhancedGridMap") if not gridmap: return - print("[StopNGo] Setting up 22x10 Accurate Horizontal Arena...") + 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 @@ -138,41 +176,57 @@ func _setup_arena(): # 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 - # Green Safe Zones at X=7,8 and X=14,15 - if x == 7 or x == 8 or x == 14 or x == 15: + 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) - - # 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) + + # --- SPECIFIC OBSTACLES (Black Walls) --- - # Lane 2 (X=9..13) - Vertical bar at X=11 - for z in range(3, 7): + # 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) - - # Lane 3 (X=16..20) - L-shape at top - for z in range(0, 3): + + # 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) - # Another bar at bottom of Lane 3 - for z in range(6, 9): + # 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 mission (Heart = 7) + # Spawn tiles for missions if multiplayer.is_server(): _spawn_mission_tiles() @@ -186,27 +240,41 @@ func _setup_arena(): 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 + + # 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(): - # Each player needs to collect 3 specific tiles (e.g. Heart) + # 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": ScarcityModel.TILE_HEART, + "target_tile": target, "required": 3, "current": 0 } + idx += 1 + if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: - rpc("sync_missions", player_missions) + if multiplayer.get_peers().size() > 0: + rpc("sync_missions", player_missions) @rpc("authority", "call_local", "reliable") func sync_missions(missions: Dictionary): @@ -218,7 +286,11 @@ func check_movement_violation(player_id: int, from: Vector2i, to: Vector2i) -> b 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 @@ -232,8 +304,13 @@ func _penalize_player(player_id: int): 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("on_stop_phase_violation") + 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): @@ -245,9 +322,18 @@ func update_mission_progress(player_id: int, tile_id: int): 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: - rpc("sync_mission_progress", player_id, mission["current"]) + 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): diff --git a/scripts/static_tekton_controller.gd b/scripts/static_tekton_controller.gd index 102ccf1..9a5ff3b 100644 --- a/scripts/static_tekton_controller.gd +++ b/scripts/static_tekton_controller.gd @@ -68,25 +68,25 @@ func _attempt_throw(): tekton.look_at(Vector3(target_world_pos.x, tekton.global_position.y, target_world_pos.z), Vector3.UP) # 2. Play Animation - if tekton.has_method("play_animation_rpc"): + if tekton.has_method("play_animation_rpc") and tekton.has_method("can_rpc") and tekton.can_rpc(): tekton.rpc("play_animation_rpc", "tekton_throw_tile") # 3. Create Projectile Visual (Synced) - if tekton.has_method("spawn_projectile_rpc"): + if tekton.has_method("spawn_projectile_rpc") and tekton.has_method("can_rpc") and tekton.can_rpc(): tekton.rpc("spawn_projectile_rpc", target_world_pos, 0.5) # 4. Impact / Spawn await get_tree().create_timer(0.5).timeout var main = tekton.get_tree().get_root().get_node_or_null("Main") - if main: + if main and tekton.has_method("can_rpc") and tekton.can_rpc(): # Spawn Item (Random ID 7-10) var item_id = randi_range(7, 10) main.rpc("sync_grid_item", target.x, 1, target.y, item_id) # 5. Resume Idle await get_tree().create_timer(0.5).timeout # Small delay after throw - if tekton.has_method("play_animation_rpc"): + if tekton.has_method("play_animation_rpc") and tekton.has_method("can_rpc") and tekton.can_rpc(): tekton.rpc("play_animation_rpc", "tekton_idle") _start_timer() diff --git a/scripts/tekton.gd b/scripts/tekton.gd index e4c99e0..e4d0a72 100644 --- a/scripts/tekton.gd +++ b/scripts/tekton.gd @@ -106,9 +106,12 @@ func move_to(target_pos: Vector2i): emit_signal("movement_finished") ) - if is_multiplayer_authority(): + if is_multiplayer_authority() and can_rpc(): rpc("sync_movement", target_pos) +func can_rpc() -> bool: + return multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED + @rpc("any_peer", "call_remote", "reliable") func sync_movement(target_pos: Vector2i): move_to(target_pos) @@ -388,7 +391,7 @@ func spawn_tiles_around(count: int = 4): if item_id != -1: var main = get_tree().get_root().get_node_or_null("Main") - if main: + if main and can_rpc(): main.rpc("sync_grid_item", pos.x, 1, pos.y, item_id) spawned += 1