diff --git a/.gitignore b/.gitignore index 59f8b91..aa5948b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .vscode/ .agent/ _daily_basis/ +_daily_changes/ /android/ .tmp diff --git a/assets/graphics/pop_up_window/modal.png b/assets/graphics/pop_up_window/modal.png deleted file mode 100644 index e546187..0000000 Binary files a/assets/graphics/pop_up_window/modal.png and /dev/null differ diff --git a/assets/graphics/pop_up_window/modal.png.import b/assets/graphics/pop_up_window/modal.png.import deleted file mode 100644 index 2ec8ed9..0000000 --- a/assets/graphics/pop_up_window/modal.png.import +++ /dev/null @@ -1,41 +0,0 @@ -[remap] - -importer="texture" -type="CompressedTexture2D" -uid="uid://dq75sm8vwoei0" -path.s3tc="res://.godot/imported/modal.png-43ddf0c61a3419805cffadd2ed5d63ce.s3tc.ctex" -metadata={ -"imported_formats": ["s3tc_bptc"], -"vram_texture": true -} - -[deps] - -source_file="res://assets/graphics/pop_up_window/modal.png" -dest_files=["res://.godot/imported/modal.png-43ddf0c61a3419805cffadd2ed5d63ce.s3tc.ctex"] - -[params] - -compress/mode=2 -compress/high_quality=false -compress/lossy_quality=0.7 -compress/uastc_level=0 -compress/rdo_quality_loss=0.0 -compress/hdr_compression=1 -compress/normal_map=0 -compress/channel_pack=0 -mipmaps/generate=false -mipmaps/limit=-1 -roughness/mode=0 -roughness/src_normal="" -process/channel_remap/red=0 -process/channel_remap/green=1 -process/channel_remap/blue=2 -process/channel_remap/alpha=3 -process/fix_alpha_border=true -process/premult_alpha=false -process/normal_map_invert_y=false -process/hdr_as_srgb=false -process/hdr_clamp_exposure=false -process/size_limit=0 -detect_3d/compress_to=1 diff --git a/assets/main-environment.tres b/assets/main-environment.tres index 9794e52..39c966d 100644 --- a/assets/main-environment.tres +++ b/assets/main-environment.tres @@ -23,7 +23,6 @@ ssao_intensity = 0.5 ssao_power = 100.0 ssao_horizon = 0.1 sdfgi_cascades = 1 -sdfgi_max_distance = 25.6 sdfgi_energy = 0.5 glow_levels/2 = 0.6 glow_levels/3 = 0.6 diff --git a/scenes/boot_screen.tscn b/scenes/boot_screen.tscn index f2609bc..85f3acc 100644 --- a/scenes/boot_screen.tscn +++ b/scenes/boot_screen.tscn @@ -1,7 +1,9 @@ -[gd_scene load_steps=3 format=3 uid="uid://cyfjwldknv8m6"] +[gd_scene load_steps=5 format=3 uid="uid://cyfjwldknv8m6"] [ext_resource type="Script" uid="uid://vgyrq5y5p7jw" path="res://scripts/ui/boot_screen.gd" id="1"] [ext_resource type="Theme" uid="uid://da337sh5qxi0s" path="res://assets/themes/ui_theme.tres" id="2"] +[ext_resource type="Texture2D" uid="uid://40tlo0mda3wr" path="res://assets/graphics/main_menu/result_bg.png" id="3_v46t4"] +[ext_resource type="Texture2D" uid="uid://dvp0as6yyudco" path="res://assets/graphics/main_menu/bg_illust.png" id="4_okh44"] [node name="BootScreen" type="Control"] layout_mode = 3 @@ -13,14 +15,25 @@ grow_vertical = 2 theme = ExtResource("2") script = ExtResource("1") -[node name="Background" type="ColorRect" parent="."] +[node name="Background" type="TextureRect" parent="."] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -color = Color(0.12, 0.1, 0.08, 1) +texture = ExtResource("3_v46t4") +expand_mode = 2 + +[node name="Background2" type="TextureRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("4_okh44") +expand_mode = 3 [node name="CenterContainer" type="CenterContainer" parent="."] layout_mode = 1 diff --git a/scenes/lobby.tscn b/scenes/lobby.tscn index 3033925..a673714 100644 --- a/scenes/lobby.tscn +++ b/scenes/lobby.tscn @@ -1,8 +1,9 @@ -[gd_scene load_steps=4 format=3 uid="uid://b7nxt2hc4kqp8"] +[gd_scene load_steps=5 format=3 uid="uid://b7nxt2hc4kqp8"] [ext_resource type="Script" uid="uid://b5q6yekyk0tld" path="res://scenes/lobby.gd" id="1_lp6xi"] [ext_resource type="Theme" uid="uid://da337sh5qxi0s" path="res://assets/themes/ui_theme.tres" id="2_theme"] [ext_resource type="Texture2D" uid="uid://2d1ks5pmblc7" path="res://assets/graphics/main_menu/bg_back.png" id="3_q60fs"] +[ext_resource type="Texture2D" uid="uid://dvp0as6yyudco" path="res://assets/graphics/main_menu/bg_illust.png" id="4_nqcc7"] [node name="Lobby" type="Control"] layout_mode = 3 @@ -24,6 +25,16 @@ grow_vertical = 2 texture = ExtResource("3_q60fs") expand_mode = 2 +[node name="Background2" type="TextureRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +texture = ExtResource("4_nqcc7") +expand_mode = 2 + [node name="MainMenuPanel" type="PanelContainer" parent="."] layout_mode = 1 anchors_preset = 8 diff --git a/scenes/main.gd b/scenes/main.gd index f3b9ff5..be08bae 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -367,15 +367,17 @@ func _setup_client_game(): add_player_character(p_id) print("Client: Pre-spawned player ", p_id) - # Pre-spawn potential bots (IDs 2 to MaxPlayers) to prevent RPC "Node not found" errors - # Bots use small integer IDs (2, 3, 4...) while clients use large unique IDs + # Pre-spawn potential bots (IDs from count+1 to MaxPlayers) to prevent RPC "Node not found" errors + # Bots use small integer IDs (e.g. 2, 3, 4...) while clients use large unique IDs if GameStateManager.enable_bots: - for i in range(2, GameStateManager.max_players + 1): + # Server spawns bots starting after the last human player index + # So if we have 2 humans, bots start at ID 3. + var start_bot_id = lobby_players.size() + 1 + for i in range(start_bot_id, GameStateManager.max_players + 1): # Only spawn if not already existing (e.g. if a human somehow got this ID, though unlikely) if not has_node(str(i)): - add_player_character(i) - get_node(str(i)).is_bot = true # Assume bot initially - get_node(str(i)).add_to_group("Bots", true) + # Spawning as BOT + add_player_character(i, true) print("Client: Pre-spawned potential bot ", i) # Ensure local player setup (UI, controls) is verified @@ -488,18 +490,28 @@ func create_bot(bot_id: int): var goal_index = bot_id - 1 if goal_index < GoalManager.preset_goals.size(): - # Wait for bot managers to be ready - await get_tree().create_timer(0.2).timeout + # Wait for bot managers to be ready (race_manager is created at T=0.5) + await get_tree().create_timer(0.75).timeout bot_character.goals = GoalManager.preset_goals[goal_index].duplicate() # Use deferred goals sync to avoid timing issues call_deferred("_deferred_set_player_goals", bot_id, bot_character.goals) @rpc("any_peer", "call_local") -func add_player_character(peer_id: int): +func add_player_character(peer_id: int, is_bot: bool = false): if has_node(str(peer_id)): return - var player_character = PlayerManager.add_player_character(peer_id) + var player_character + if is_bot: + player_character = PlayerManager.create_bot(peer_id) + player_character.add_to_group("Bots", true) + else: + player_character = PlayerManager.add_player_character(peer_id) + + # Set properties BEFORE adding to tree (ensure _ready sees correct state) + # create_bot already sets is_bot=true, but we ensure consistency + player_character.is_bot = is_bot + add_child(player_character) player_character.add_to_group("Players", true) @@ -1037,7 +1049,7 @@ func _show_game_over_panel(): for p in get_tree().get_nodes_in_group("Players"): player_scores.append({ "name": p.display_name if not p.display_name.is_empty() else str(p.name), - "score": goals_cycle_manager.get_player_score(p.get_multiplayer_authority()) if goals_cycle_manager else 0 + "score": goals_cycle_manager.get_player_score(p.name.to_int()) if goals_cycle_manager else 0 }) player_scores.sort_custom(func(a, b): return a.score > b.score) diff --git a/scenes/player.gd b/scenes/player.gd index 2a28211..e239b5a 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -173,9 +173,6 @@ func _ready(): # ========================================================================= # BOT-SPECIFIC SETUP - BotController handles bot AI, we just disable input # ========================================================================= - # ========================================================================= - # BOT-SPECIFIC SETUP - BotController handles bot AI - # ========================================================================= if is_bot == true or is_in_group("Bots"): # Disable input processing for bots set_process_input(false) @@ -183,10 +180,11 @@ func _ready(): # Set initial position for bots if enhanced_gridmap: - current_position = _find_random_spawn_position() - update_player_position(current_position) - spawn_point_selected = true if is_multiplayer_authority(): + 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) # Assign bot character (deterministic based on ID to match lobby preview) @@ -603,6 +601,96 @@ func _apply_tint_recursive(node: Node, color: Color): for child in node.get_children(): _apply_tint_recursive(child, color) +var immunity_timer: float = 0.0 + +@rpc("any_peer", "call_local") +func apply_stagger(duration: float = 1.5): + if immunity_timer > 0: + return # Immune! + + if is_frozen: + return # Already staggered + + is_frozen = true + _apply_tint_recursive(self, Color.BLUE) # Visual feedback + + # Set immunity (3 seconds as requested) + immunity_timer = 3.0 + + print("Player %s staggered for %.1f seconds" % [name, duration]) + + if is_multiplayer_authority(): + rpc("display_message", "C R U S H E D !", 4) # MessageType.WARNING + drop_random_item() + + # Grant "Smashed" Bonus (1 bar, max 2) + if powerup_manager: + powerup_manager.acquire_smash_bonus() + + await get_tree().create_timer(duration).timeout + + is_frozen = false + # If still immune, show immunity tint (Green?), otherwise White + if immunity_timer > 0: + _apply_tint_recursive(self, Color(0.5, 1.0, 0.5)) # Light Green for immunity + else: + _apply_tint_recursive(self, Color.WHITE) # Remove tint + +func drop_random_item(): + if playerboard_is_empty(): + return + + # Find occupied slots + var occupied_indices = [] + for i in range(playerboard.size()): + if playerboard[i] != -1: + occupied_indices.append(i) + + if occupied_indices.size() == 0: + return + + # Pick random slot + var slot_index = occupied_indices.pick_random() + var item_id = playerboard[slot_index] + + # Try to find empty spot on grid near player + var drop_pos = _find_valid_drop_position() + + if drop_pos != Vector2i(-1, -1): + # Drop it + playerboard[slot_index] = -1 + rpc("sync_playerboard", playerboard) + + # Sync grid item + var cell = Vector3i(drop_pos.x, 0, drop_pos.y) + rpc("sync_grid_item", cell.x, cell.y, cell.z, item_id) + + rpc("display_message", "Dropped item!", 4) + print("Player %s dropped item %d at %s" % [name, item_id, drop_pos]) + +func playerboard_is_empty() -> bool: + for item in playerboard: + if item != -1: + return false + return true + +func _find_valid_drop_position() -> Vector2i: + # Try random adjacent cells + var neighbors = enhanced_gridmap.get_neighbors(current_position, 0) + neighbors.shuffle() + + for neighbor in neighbors: + var pos = neighbor.position + if enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y)) == -1: # Empty floor? No, 0 is floor. -1 is void? + # Wait, items are on layer 1 usually? + # Check logic: grab_item uses y=1? + var item_cell = Vector3i(pos.x, 1, pos.y) + if enhanced_gridmap.get_cell_item(item_cell) == -1: + if not is_position_occupied(pos): + return pos + + return Vector2i(-1, -1) + func _process(delta): if is_multiplayer_authority(): @@ -629,6 +717,13 @@ func _process(delta): if movement_manager: movement_manager._process(delta) + # Immunity Timer Logic + if immunity_timer > 0: + immunity_timer -= delta + if immunity_timer <= 0: + immunity_timer = 0 + _apply_tint_recursive(self, Color.WHITE) # Remove immunity tint + @rpc("any_peer", "call_local") func ping_existence(): # This just lets other clients know this player exists @@ -876,7 +971,8 @@ func start_movement_along_path(path: Array, clear_visual: bool = true): # Clear visuals for everyone including bots if clear_visual: - enhanced_gridmap.clear_path_visualization() + if enhanced_gridmap: + enhanced_gridmap.clear_path_visualization() # Check for buffered input if movement_manager and movement_manager.has_method("_on_movement_finished"): @@ -1429,8 +1525,10 @@ func sync_position(pos: Vector2i): current_position.y * cell_size.z + cell_size.z * 0.5 ) + cell_offset - global_position = new_pos - target_visual_position = new_pos # Reset smoothing target to prevent fighting + # Only snap visual if not moving (moving players will tween to destination) + if not is_player_moving: + global_position = new_pos + target_visual_position = new_pos # Reset smoothing target to prevent fighting @rpc("any_peer", "call_local", "reliable") func set_spawn_position(pos: Vector2i): diff --git a/scripts/bot_controller.gd b/scripts/bot_controller.gd index 7327d08..e470ee6 100644 --- a/scripts/bot_controller.gd +++ b/scripts/bot_controller.gd @@ -51,8 +51,8 @@ func _ready(): queue_free() return - # Wait for actor to be fully ready - await get_tree().create_timer(1.0).timeout + # Wait for actor to be fully ready (player._ready awaits 0.5s then creates managers) + await get_tree().create_timer(1.5).timeout enhanced_gridmap = actor.enhanced_gridmap if not enhanced_gridmap: @@ -335,10 +335,23 @@ func _try_move() -> bool: _is_processing_action = true _current_action = "moving" - # Wait for movement to finish (signal from movement manager) - await actor.movement_manager.movement_finished + # Wait for movement to finish or timeout (safety) + # Race: Signal vs Timeout + # Since Godot 4 doesn't support 'await' racing easily without helper, + # we'll just wait for the signal but ensure movement manager emits it. + # safer approach: check if is_moving goes false + # Safety timeout to prevent infinite loop + var max_wait_time = 2.0 + var elapsed = 0.0 + while actor.is_player_moving and is_instance_valid(self): + await get_tree().process_frame + elapsed += get_process_delta_time() + if elapsed > max_wait_time: + print("[BotController] Movement timed out!") + break + if not is_instance_valid(self): return true _is_processing_action = false _current_action = "idle" diff --git a/scripts/managers/player_movement_manager.gd b/scripts/managers/player_movement_manager.gd index 46e2ab6..7093634 100644 --- a/scripts/managers/player_movement_manager.gd +++ b/scripts/managers/player_movement_manager.gd @@ -42,10 +42,7 @@ func rotate_towards_target(target_pos: Vector2i): func simple_move_to(grid_position: Vector2i) -> bool: if is_moving: - # Calculate direction for buffering var direction = grid_position - player.current_position - - # FIX: Only buffer if direction is DIFFERENT from current move (prevents overshoot) if direction != current_move_direction: buffer_move_input(direction) return false @@ -53,11 +50,9 @@ func simple_move_to(grid_position: Vector2i) -> bool: if not player.is_multiplayer_authority(): return false - # Check if player is frozen if player.get("is_frozen"): return false - # Check if target is within 1-tile range 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)) @@ -67,32 +62,23 @@ func simple_move_to(grid_position: Vector2i) -> bool: if distance != 1: return false # Only single-step moves allowed - # Check if target position is within grid bounds if not enhanced_gridmap.is_position_valid(grid_position): return false - # Check for finish line logic (delegated back to player or race manager) if player.has_method("can_move_to_finish") and not player.can_move_to_finish(grid_position): return false - # Check walkability and obstacles var cell_item = enhanced_gridmap.get_cell_item(Vector3i(grid_position.x, 0, grid_position.y)) if cell_item == -1 or cell_item in enhanced_gridmap.non_walkable_items: return false - # Check for player collision and try to push if player.is_position_occupied(grid_position): - # Direction for the push var push_dir = grid_position - player.current_position if not try_push(grid_position, push_dir): return false - # If push succeeded, the tile is now technically free (or will be processed as free) - # proceed to move into it - # All checks passed, perform move rotate_towards_target(grid_position) - # Play walk animation (synced across network) if player.is_multiplayer_authority() and player.has_method("sync_walk_animation"): player.rpc("sync_walk_animation") @@ -101,7 +87,6 @@ func simple_move_to(grid_position: Vector2i) -> bool: current_move_direction = grid_position - player.current_position - # Use the existing RPC to move player.rpc("start_movement_along_path", path, not (player.is_bot or player.is_in_group("Bots"))) return true @@ -119,15 +104,21 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool: # Check if pushed destination is valid if not enhanced_gridmap.is_position_valid(pushed_to_pos): + # Blocked by world bounds -> Double Push! + other_player.rpc("apply_stagger", 1.5) return false # Check walkability of pushed destination var cell_item = enhanced_gridmap.get_cell_item(Vector3i(pushed_to_pos.x, 0, pushed_to_pos.y)) if cell_item == -1 or cell_item in enhanced_gridmap.non_walkable_items: + # Blocked by obstacle -> Double Push! + other_player.rpc("apply_stagger", 1.5) return false - # Check if pushed destination is ALREADY occupied (no daisy chaining) + # Check if pushed destination is ALREADY occupied (Double Push / Crush) if player.is_position_occupied(pushed_to_pos): + # Blocked by another player -> Double Push! + other_player.rpc("apply_stagger", 1.5) return false # Check if other player is currently moving (don't push moving players to avoid sync issues) diff --git a/scripts/managers/powerup_manager.gd b/scripts/managers/powerup_manager.gd index 991ddd2..dfb28e1 100644 --- a/scripts/managers/powerup_manager.gd +++ b/scripts/managers/powerup_manager.gd @@ -58,11 +58,22 @@ func _add_bar(): # Type 1 = POWERUP message for special styling player.rpc("display_message", "Power-up bar filled!", 1) print("[PowerUp] Player %s gained 1 bar! Total: %d/%d points" % [player.name, current_points, MAX_POINTS]) + + if player.is_multiplayer_authority(): + player.get_node("PowerUpManager").rpc("sync_points", current_points) # ============================================================================= # Goal Completion Reward # ============================================================================= +func acquire_smash_bonus(): + """Called when player is smashed. Grants 1 bar up to a max of 2 bars.""" + if get_bars() < 2: + _add_bar() + print("[PowerUp] Player %s gained smash bonus bar! Total: %d/%d" % [player.name, current_points, MAX_POINTS]) + else: + print("[PowerUp] Player %s smash bonus capped (already has >= 2 bars)" % player.name) + func add_goal_completion_reward(): """Called when player completes a goal pattern. Awards 1 bar.""" _add_bar() diff --git a/scripts/managers/ui_manager.gd b/scripts/managers/ui_manager.gd index 31c9592..2837ca0 100644 --- a/scripts/managers/ui_manager.gd +++ b/scripts/managers/ui_manager.gd @@ -205,7 +205,8 @@ func set_local_player(player): func _connect_powerup_manager_deferred(player): """Wait for PowerUpManager to be initialized before connecting.""" - await player.get_tree().create_timer(0.3).timeout + # player._ready waits 0.5s before creating managers, so wait longer + await player.get_tree().create_timer(0.8).timeout var powerup_manager = player.get_node_or_null("PowerUpManager") if powerup_manager: @@ -213,6 +214,8 @@ func _connect_powerup_manager_deferred(player): powerup_manager.points_changed.connect(_on_powerup_points_changed) # Initialize bar with current values update_powerup_bar(powerup_manager.get_points(), powerup_manager.get_max_points()) + else: + push_warning("[UIManager] PowerUpManager not found on player after 0.8s wait") # ============================================================================= # Power-Up Bar UI (Battery Style)