From 96f5754f9913a9facf4d6189514c1d547a452d83 Mon Sep 17 00:00:00 2001 From: adtpdn Date: Fri, 12 Dec 2025 02:16:06 +0800 Subject: [PATCH] feat: Implement initial game structure with core logic, various managers, player scene, and project configuration. --- .../implementation_plan_2025-12-12.md | 235 +++++++++++++++ _daily_basis/task_2025-12-12.md | 35 +++ _daily_basis/walkthrough_2025-12-12.md | 54 ++++ icon.svg | 28 +- project.godot | 5 + scenes/main.gd | 88 ++++++ scenes/player.gd | 20 +- scripts/managers/goals_cycle_manager.gd | 271 ++++++++++++++++++ scripts/managers/player_race_manager.gd | 173 ++++------- scripts/managers/playerboard_manager.gd | 70 ++++- scripts/managers/powerup_manager.gd | 128 +++++++++ scripts/managers/ui_manager.gd | 192 +++++++++++++ 12 files changed, 1161 insertions(+), 138 deletions(-) create mode 100644 _daily_basis/implementation_plan_2025-12-12.md create mode 100644 _daily_basis/task_2025-12-12.md create mode 100644 _daily_basis/walkthrough_2025-12-12.md create mode 100644 scripts/managers/goals_cycle_manager.gd create mode 100644 scripts/managers/powerup_manager.gd diff --git a/_daily_basis/implementation_plan_2025-12-12.md b/_daily_basis/implementation_plan_2025-12-12.md new file mode 100644 index 0000000..68c50f4 --- /dev/null +++ b/_daily_basis/implementation_plan_2025-12-12.md @@ -0,0 +1,235 @@ +# Game Mechanics Refactor Implementation Plan + +This plan transforms the game from a lap-based racing system to a cycle-based goals system with power-ups, scoring, and a live leaderboard. + +## User Review Required + +> [!IMPORTANT] +> **Major Breaking Change**: This removes the entire lap/finish line mechanic. Players will no longer race to a finish line. The game becomes purely score-based with timed goal cycles. + +> [!WARNING] +> **UI Changes**: The AllPlayerGoals panels will be modified to add timers. A new leaderboard panel will be added to the right side of main.tscn. + +--- + +## Proposed Changes + +### GoalsCycleManager (New Manager) + +#### [NEW] [goals_cycle_manager.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scripts/managers/goals_cycle_manager.gd) + +New autoload manager handling: +- 60-second countdown timer for goal cycles +- Broadcasting timer sync across multiplayer +- Goal regeneration on cycle end +- Score calculation (early completion = bonus points) +- Playerboard clear on timer end with match-to-score conversion + +```gdscript +# Key properties: +var cycle_duration: float = 60.0 +var current_cycle_timer: float = 0.0 +var player_scores: Dictionary = {} # peer_id -> score + +# Key methods: +func start_cycle() +func _process_timer(delta) +func on_goal_completed(player, time_remaining: float) # More time = more score +func on_cycle_end() # Clear playerboards, convert matches to score +func regenerate_goals_for_player(player) +``` + +--- + +### PowerUpManager (New Manager) + +#### [NEW] [powerup_manager.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scripts/managers/powerup_manager.gd) + +New manager (attached to player) handling: +- Power-up points tracking (max 12 points = 4 bars) +- Holo tile pickup tracking (4 pickups = 1 bar = 4 points) +- Goal completion awards 1 bar (4 points) +- Using special effect consumes 1 bar (4 points) + +```gdscript +const MAX_POINTS: int = 12 +const POINTS_PER_BAR: int = 4 +const MAX_BARS: int = 4 + +var current_points: int = 0 +var holo_pickup_count: int = 0 + +func add_holo_pickup() # Called when picking holo tile +func add_goal_completion_reward() # Called when goal completed +func can_use_special() -> bool # True if >= 4 points +func use_special_effect() # Consume 4 points, trigger random effect +``` + +--- + +### Player Modifications + +#### [MODIFY] [player.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scenes/player.gd) + +- Add `score: int` property +- Add `powerup_manager` reference +- Remove/deprecate references to `can_finish`, `finish_race`, lap tracking +- Add input handling for `use_powerup` action +- Add `on_goal_completed()` method that triggers tile randomization + +--- + +### Special Tiles Manager Modifications + +#### [MODIFY] [special_tiles_manager.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scripts/managers/special_tiles_manager.gd) + +- Remove auto `trigger_random_effect()` call on holo tile pickup +- Instead, call `player.powerup_manager.add_holo_pickup()` +- Move `trigger_random_effect()` to be called by PowerUpManager + +--- + +### Playerboard Manager Modifications + +#### [MODIFY] [playerboard_manager.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scripts/managers/playerboard_manager.gd) + +- In `grab_item()`: Change holo tile handling to add power-up points instead of triggering effect +- Add `check_goal_completion()` method that detects if current playerboard matches goals +- Add `clear_and_convert_to_score()` method for cycle end + +--- + +### Race Manager Modifications (Heavy Refactor) + +#### [MODIFY] [player_race_manager.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scripts/managers/player_race_manager.gd) + +- Remove: `current_lap`, `lap1_finishers`, `lap2_finishers`, `finish_locations`, `spawn_locations` +- Remove: `finish_race()`, `start_new_lap()`, `get_current_finish_locations()` +- Keep: `goals`, `playerboard`, `check_pattern_match()` (renamed to `check_goal_completion()`) +- Add: `on_goal_completed()` → triggers 9-tile randomization around player + +--- + +### Main Scene Modifications + +#### [MODIFY] [main.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scenes/main.gd) + +- Initialize `GoalsCycleManager` (as autoload or child) +- Add leaderboard UI update logic in `_process()` +- Add timer sync RPCs +- Remove lap/finish related code paths + +#### [MODIFY] [main.tscn](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scenes/main.tscn) + +- Add `TimerLabel` to each panel in `AllPlayerGoals` (shows countdown) +- Add `LeaderboardPanel` (VBoxContainer on right side with 4 player entries) +- Add `PowerUpBar` UI (battery-style visualization) + +--- + +### Project Settings + +#### [MODIFY] [project.godot](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/project.godot) + +Add new input action: +```ini +use_powerup={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"physical_keycode":70,...)] # F key +} +``` + +--- + +### UI Manager Modifications + +#### [MODIFY] [ui_manager.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scripts/managers/ui_manager.gd) + +- Add `update_timer_display(player_idx, time_remaining)` method +- Add `update_leaderboard(scores_dict)` method +- Add `update_powerup_bar(current_points, max_points)` method + +--- + +## Diagram: New Game Flow + +```mermaid +flowchart TD + A[Game Start] --> B[Cycle Begins - 60s Timer] + B --> C{Player Actions} + C --> D[Grab Tile] + D --> E{Holo Tile?} + E -->|Yes| F[Add to Holo Counter] + F --> G{4 Holo Pickups?} + G -->|Yes| H[+1 Bar Power-up] + G -->|No| C + H --> C + E -->|No| I[Place in Playerboard] + I --> J{Goal Complete?} + J -->|Yes| K[+Score based on time left] + K --> L[+1 Bar Power-up] + L --> M[Randomize 9 tiles around player] + M --> N[Regenerate Goals] + N --> C + J -->|No| C + + C --> O[Use Power-up Hotkey] + O --> P{Has 1 Bar?} + P -->|Yes| Q[Trigger Random Special Effect] + P -->|No| C + Q --> C + + B --> R{Timer = 0?} + R -->|Yes| S[Clear Playerboard] + S --> T[Convert Matching Tiles to Score] + T --> U[New Cycle Begins] + U --> B +``` + +--- + +## Verification Plan + +### Manual Testing (User Required) + +Since this is a Godot game with multiplayer networking, automated testing is limited. The following manual tests are required: + +#### Test 1: Goal Cycle Timer +1. Launch the game from `res://scenes/lobby.tscn` +2. Create a lobby and start the game +3. **Verify**: A 60-second timer appears next to each player's goals panel +4. **Verify**: Timer counts down in real-time +5. **Verify**: When timer reaches 0, playerboard is cleared and new goals appear + +#### Test 2: Goal Completion & Scoring +1. Play until you complete a goal pattern (match 3x3 in playerboard to goals) +2. **Verify**: Score increases (formula: base + time_remaining_bonus) +3. **Verify**: 1 bar of power-up is added +4. **Verify**: 9 tiles around your player position are randomized +5. **Verify**: New goals are generated immediately + +#### Test 3: Power-up Points (Holo Tiles) +1. Pick up 4 holo tiles (indices 11-14) +2. **Verify**: No automatic special effect triggers +3. **Verify**: Power-up bar shows +1 bar (4 points accumulated) +4. **Verify**: Power-up bar UI shows battery-style visualization + +#### Test 4: Using Special Effects +1. Accumulate at least 1 bar of power-up (4 points) +2. Press the `F` key (use_powerup action) +3. **Verify**: A random special effect triggers +4. **Verify**: Power-up bar decreases by 1 bar + +#### Test 5: Live Leaderboard +1. Play with multiple players or bots +2. **Verify**: Leaderboard appears on right side of screen +3. Complete goals to gain score +4. **Verify**: Leaderboard reorders dynamically based on score +5. **Verify**: Position shows 1st, 2nd, 3rd, 4th correctly + +#### Test 6: Multiplayer Sync +1. Host a game with 2+ players +2. **Verify**: Timer is synchronized across all clients +3. **Verify**: Score updates appear for all players on all clients +4. **Verify**: Leaderboard shows same order for all clients +5. **Verify**: Power-up usage effects are visible to all players diff --git a/_daily_basis/task_2025-12-12.md b/_daily_basis/task_2025-12-12.md new file mode 100644 index 0000000..379b894 --- /dev/null +++ b/_daily_basis/task_2025-12-12.md @@ -0,0 +1,35 @@ +# Game Mechanics Refactor Task + +## Summary +Refactor game mechanics to replace the lap-based racing system with a cycle-based goals system, add power-up points, live leaderboard, and enhanced special tiles handling. + +## Tasks + +### Phase 1: Core System Changes +- [x] Create `GoalsCycleManager` (new manager for 60-second goal cycles with timer) +- [ ] Modify `player_race_manager.gd` to remove lap/finish logic +- [ ] Remove `can_finish`, `finish_race`, `start_new_lap` logic from player +- [x] Add score tracking system to player + +### Phase 2: Power-up System +- [x] Create `PowerUpManager` (new manager for power-up points) +- [x] Modify `special_tiles_manager.gd` to track holo tile pickups (4 = 1 bar) +- [x] Change holo tiles to give power-up points instead of auto-triggering effects +- [/] Add hotkey for using special effect (project.godot input action) + +### Phase 3: Goal Completion Logic +- [x] Implement goal completion detection → award 1 power-up point +- [x] Implement score calculation (early completion = more points) +- [x] Implement 9-tile randomization around player on goal complete +- [x] Implement timer end logic: clear playerboard, convert matches to score + +### Phase 4: UI Components +- [x] Add timer display to each player's goals panel in AllPlayerGoals +- [x] Create power-up bar UI (battery-style, 4 bars max) +- [x] Create live leaderboard UI on right side of main.tscn +- [x] Wire up leaderboard to update dynamically based on scores + +### Phase 5: Input Mapping & Cleanup +- [x] Add `use_powerup` input action to project.godot +- [x] Remove/deprecate lap-related code paths +- [ ] Test multiplayer synchronization of new systems diff --git a/_daily_basis/walkthrough_2025-12-12.md b/_daily_basis/walkthrough_2025-12-12.md new file mode 100644 index 0000000..e59eb44 --- /dev/null +++ b/_daily_basis/walkthrough_2025-12-12.md @@ -0,0 +1,54 @@ +# Game Mechanics Refactor - Walkthrough + +## Summary + +Successfully refactored the game from a **lap-based racing system** to a **cycle-based goals system** with power-ups, scoring, and a live leaderboard. + +--- + +## Changes Made + +### New Files Created + +| File | Purpose | +|------|---------| +| [goals_cycle_manager.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scripts/managers/goals_cycle_manager.gd) | 60-sec timer, scoring (base+time bonus), goal regeneration, 9-tile randomization | +| [powerup_manager.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scripts/managers/powerup_manager.gd) | Power-up points (4 bars max), holo tile tracking, special effect activation | + +### Modified Files + +| File | Changes | +|------|---------| +| [player.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scenes/player.gd) | Added `powerup_manager`, `score`, use_powerup input, removed finish checks | +| [playerboard_manager.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scripts/managers/playerboard_manager.gd) | Holo tiles → powerup points, goal completion check | +| [player_race_manager.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scripts/managers/player_race_manager.gd) | Deprecated lap system, kept pattern matching | +| [main.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scenes/main.gd) | GoalsCycleManager init, UI setups, signal handlers | +| [ui_manager.gd](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/scripts/managers/ui_manager.gd) | Power-up bar, leaderboard panel, timer labels | +| [project.godot](file:///c:/Users/ADT/Documents/GodotProjects/tekton-enet/project.godot) | Added `use_powerup` input (F key) | + +--- + +## New Game Flow + +```mermaid +flowchart LR + A[60s Cycle] --> B[Grab Tiles] + B --> C{Holo?} + C -->|Yes| D[+Powerup] + C -->|No| E[Check Goal] + E -->|Match| F[+Score +Powerup +Randomize 9 tiles] + F --> G[New Goals] + A --> H{Timer End} + H --> I[Clear Board → Score Matching] +``` + +--- + +## Manual Testing Required + +1. **Timer Test**: Verify 60-sec countdown on goal panels +2. **Goal Completion**: Complete pattern → score + powerup + tile randomization +3. **Holo Tiles**: Pick 4 → 1 powerup bar (no auto effect) +4. **F Key**: Use powerup → triggers random special effect +5. **Leaderboard**: Updates dynamically with score rankings +6. **Multiplayer Sync**: Timer and scores sync across clients diff --git a/icon.svg b/icon.svg index 9d8b7fa..d10f9ee 100644 --- a/icon.svg +++ b/icon.svg @@ -1 +1,27 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/project.godot b/project.godot index bb28aa0..0533cba 100644 --- a/project.godot +++ b/project.godot @@ -106,3 +106,8 @@ action_put={ , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":83,"location":0,"echo":false,"script":null) ] } +use_powerup={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":70,"location":0,"echo":false,"script":null) +] +} diff --git a/scenes/main.gd b/scenes/main.gd index 5c74c1f..70a35dc 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -7,6 +7,7 @@ extends Node3D # Manager references var ui_manager var obstacle_manager +var goals_cycle_manager # Minimal local state var _connection_check_timer: float = 0.0 @@ -25,6 +26,9 @@ func _ready(): # Setup UI ui_manager.setup_action_buttons(_set_action_state_callback) ui_manager.setup_playerboard_ui() + ui_manager.setup_timer_labels(self) + ui_manager.setup_leaderboard_ui(self) + ui_manager.setup_powerup_bar_ui(self) _setup_obstacle_ui() # Auto-start game if coming from lobby (already connected to match) @@ -44,6 +48,17 @@ func _init_managers(): obstacle_manager.name = "ObstacleManager" add_child(obstacle_manager) obstacle_manager.initialize($EnhancedGridMap) + + # Goals cycle manager for 60-second timer and scoring + goals_cycle_manager = load("res://scripts/managers/goals_cycle_manager.gd").new() + goals_cycle_manager.name = "GoalsCycleManager" + add_child(goals_cycle_manager) + goals_cycle_manager.initialize(self) + + # Connect signals for UI updates + goals_cycle_manager.timer_updated.connect(_on_timer_updated) + goals_cycle_manager.score_updated.connect(_on_score_updated) + goals_cycle_manager.leaderboard_updated.connect(_on_leaderboard_updated) # Message Bar Configuration const MAX_MESSAGES := 5 @@ -248,6 +263,10 @@ func _start_game(): TurnManager.reset_turn() var next_player = TurnManager.next_turn(GameStateManager.players) rpc("set_current_turn", next_player) + + # Start the goals cycle timer + if goals_cycle_manager: + goals_cycle_manager.start_cycle() # ============================================================================= # Player Management @@ -706,3 +725,72 @@ func sync_grid_item(x: int, y: int, z: int, item: int): var enhanced_gridmap = $EnhancedGridMap if enhanced_gridmap: enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item) + +# ============================================================================= +# Goals Cycle & Leaderboard UI +# ============================================================================= + +func _on_timer_updated(time_remaining: float): + # Update timer display on all player goal panels + var time_text = "%02d:%02d" % [int(time_remaining) / 60, int(time_remaining) % 60] + + for i in range($AllPlayerGoals.get_child_count()): + var panel = $AllPlayerGoals.get_child(i) + if panel.visible: + var timer_label = panel.get_node_or_null("TimerLabel") + if timer_label: + timer_label.text = time_text + +func _on_score_updated(peer_id: int, new_score: int): + # Update player's score display + var player = get_node_or_null(str(peer_id)) + if player: + player.score = new_score + + # Update leaderboard UI + _update_leaderboard_display() + +func _on_leaderboard_updated(sorted_scores: Array): + # Update the leaderboard panel + _update_leaderboard_display() + +func _update_leaderboard_display(): + var leaderboard_panel = get_node_or_null("LeaderboardPanel") + if not leaderboard_panel: + return + + var sorted_scores = goals_cycle_manager.get_leaderboard() if goals_cycle_manager else [] + + # Get player names and update entries + for i in range(4): # Max 4 entries + var entry = leaderboard_panel.get_node_or_null("Entry" + str(i + 1)) + if not entry: + continue + + if i < sorted_scores.size(): + var score_data = sorted_scores[i] + var player = get_node_or_null(str(score_data.peer_id)) + var player_name = player.name if player else str(score_data.peer_id) + + var rank_label = entry.get_node_or_null("RankLabel") + var name_label = entry.get_node_or_null("NameLabel") + var score_label = entry.get_node_or_null("ScoreLabel") + + if rank_label: + rank_label.text = _get_ordinal(i + 1) + if name_label: + name_label.text = player_name + if score_label: + score_label.text = str(score_data.score) + + entry.visible = true + else: + entry.visible = false + +func _get_ordinal(n: int) -> String: + match n: + 1: return "1st" + 2: return "2nd" + 3: return "3rd" + 4: return "4th" + _: return str(n) + "th" diff --git a/scenes/player.gd b/scenes/player.gd index 4ecdbed..7e4bdb0 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -7,6 +7,10 @@ var input_manager var playerboard_manager var action_manager var special_tiles_manager +var powerup_manager + +# Score tracking +var score: int = 0 # Special effect states var is_frozen: bool = false @@ -223,6 +227,11 @@ func _init_managers(): special_tiles_manager.name = "SpecialTilesManager" add_child(special_tiles_manager) special_tiles_manager.initialize(self, enhanced_gridmap) + + powerup_manager = load("res://scripts/managers/powerup_manager.gd").new() + powerup_manager.name = "PowerUpManager" + add_child(powerup_manager) + powerup_manager.initialize(self, enhanced_gridmap) # Add function to check if position is at finish line func is_at_finish_line() -> bool: @@ -366,16 +375,19 @@ func _physics_process(delta): rpc("remote_set_position", global_position) last_sent_position = global_position - # Add continuous finish line check (uses lap-aware finish locations) - var current_finish = race_manager.get_current_finish_locations() if race_manager else [] - if race_manager and current_position in current_finish and can_finish and not is_player_moving: - finish_race() # This handles lap increment and calls start_new_lap properly + # NOTE: Finish line checking removed - game uses cycle-based goals system now # -------------------------------------------------------------------- # Input # -------------------------------------------------------------------- func _unhandled_input(event): + # Handle power-up usage + if event.is_action_pressed("use_powerup") and is_multiplayer_authority(): + if powerup_manager and powerup_manager.can_use_special(): + powerup_manager.use_special_effect() + return + if input_manager: input_manager.handle_unhandled_input(event) diff --git a/scripts/managers/goals_cycle_manager.gd b/scripts/managers/goals_cycle_manager.gd new file mode 100644 index 0000000..2eca250 --- /dev/null +++ b/scripts/managers/goals_cycle_manager.gd @@ -0,0 +1,271 @@ +extends Node + +# GoalsCycleManager - Handles 60-second goal cycles, scoring, and goal regeneration + +const CYCLE_DURATION: float = 60.0 +const BASE_SCORE: int = 100 +const TIME_BONUS_MULTIPLIER: float = 2.0 + +# Timer state +var current_cycle_timer: float = 0.0 +var is_cycle_active: bool = false + +# Score tracking: peer_id -> score +var player_scores: Dictionary = {} + +# Reference to main scene +var main_scene: Node = null + +signal cycle_started() +signal cycle_ended() +signal timer_updated(time_remaining: float) +signal score_updated(peer_id: int, new_score: int) +signal leaderboard_updated(sorted_scores: Array) + +func _ready(): + set_process(false) + +func initialize(main: Node): + main_scene = main + +func _process(delta): + if not is_cycle_active: + return + + current_cycle_timer -= delta + + if current_cycle_timer <= 0: + current_cycle_timer = 0 + _on_cycle_end() + else: + emit_signal("timer_updated", current_cycle_timer) + + # Server broadcasts timer sync every second + if multiplayer.is_server() and int(current_cycle_timer) != int(current_cycle_timer + delta): + rpc("sync_timer", current_cycle_timer) + +# ============================================================================= +# Cycle Control +# ============================================================================= + +func start_cycle(): + current_cycle_timer = CYCLE_DURATION + is_cycle_active = true + set_process(true) + emit_signal("cycle_started") + + if multiplayer.is_server(): + rpc("sync_cycle_start") + +@rpc("authority", "call_local", "reliable") +func sync_cycle_start(): + current_cycle_timer = CYCLE_DURATION + is_cycle_active = true + set_process(true) + emit_signal("cycle_started") + +@rpc("authority", "call_local", "unreliable") +func sync_timer(time_remaining: float): + current_cycle_timer = time_remaining + emit_signal("timer_updated", current_cycle_timer) + +func _on_cycle_end(): + is_cycle_active = false + set_process(false) + emit_signal("cycle_ended") + + if multiplayer.is_server(): + # Clear all playerboards and convert matches to score + _process_cycle_end_for_all_players() + rpc("sync_cycle_end") + + # Start new cycle after a brief delay + await get_tree().create_timer(2.0).timeout + start_cycle() + +@rpc("authority", "call_local", "reliable") +func sync_cycle_end(): + is_cycle_active = false + set_process(false) + emit_signal("cycle_ended") + +# ============================================================================= +# Goal Completion & Scoring +# ============================================================================= + +func on_goal_completed(player: Node, time_remaining: float): + """Called when a player completes their goal pattern.""" + if not multiplayer.is_server(): + return + + var peer_id = player.get_multiplayer_authority() + + # Calculate score: base + time bonus + var time_bonus = int(time_remaining * TIME_BONUS_MULTIPLIER) + var score_earned = BASE_SCORE + time_bonus + + # Update player score + if not player_scores.has(peer_id): + player_scores[peer_id] = 0 + player_scores[peer_id] += score_earned + + emit_signal("score_updated", peer_id, player_scores[peer_id]) + _update_leaderboard() + + # Sync score to all clients + rpc("sync_player_score", peer_id, player_scores[peer_id]) + + # Regenerate goals for this player + regenerate_goals_for_player(player) + + # Randomize 9 tiles around player + _randomize_tiles_around_player(player) + + print("[GoalsCycle] Player %d completed goal! +%d points (base: %d, time bonus: %d)" % [peer_id, score_earned, BASE_SCORE, time_bonus]) + +@rpc("authority", "call_local", "reliable") +func sync_player_score(peer_id: int, total_score: int): + player_scores[peer_id] = total_score + emit_signal("score_updated", peer_id, total_score) + _update_leaderboard() + +func _update_leaderboard(): + # Sort players by score (descending) + var sorted_scores = [] + for peer_id in player_scores.keys(): + sorted_scores.append({"peer_id": peer_id, "score": player_scores[peer_id]}) + + sorted_scores.sort_custom(func(a, b): return a.score > b.score) + emit_signal("leaderboard_updated", sorted_scores) + +# ============================================================================= +# Cycle End Processing +# ============================================================================= + +func _process_cycle_end_for_all_players(): + """Server-side: Clear playerboards and convert matching tiles to score.""" + var all_players = get_tree().get_nodes_in_group("Players") + + for player in all_players: + var peer_id = player.get_multiplayer_authority() + var match_score = _calculate_match_score(player) + + if match_score > 0: + if not player_scores.has(peer_id): + player_scores[peer_id] = 0 + player_scores[peer_id] += match_score + rpc("sync_player_score", peer_id, player_scores[peer_id]) + + # Clear playerboard + player.playerboard.fill(-1) + player.rpc("sync_playerboard", player.playerboard) + + # Generate new goals + regenerate_goals_for_player(player) + + _update_leaderboard() + +func _calculate_match_score(player: Node) -> int: + """Calculate score from matching tiles in playerboard to goals.""" + var matching_tiles = 0 + var goals = player.goals + var playerboard = player.playerboard + + # Check center 3x3 of playerboard against goals + for i in range(3): + for j in range(3): + var goal_idx = i * 3 + j + var board_idx = (i + 1) * 5 + (j + 1) # Center 3x3 in 5x5 board + + if goal_idx < goals.size() and board_idx < playerboard.size(): + if goals[goal_idx] != -1 and playerboard[board_idx] == goals[goal_idx]: + matching_tiles += 1 + + # 10 points per matching tile + return matching_tiles * 10 + +# ============================================================================= +# Goal Regeneration +# ============================================================================= + +func regenerate_goals_for_player(player: Node): + """Generate new random goals for a player.""" + if not multiplayer.is_server(): + return + + var new_goals = GoalManager.initialize_random_goals(9, 7, 10, 1.0) + var int_goals: Array[int] = [] + for g in new_goals: + int_goals.append(g) + + player.goals = int_goals + player.rpc("sync_goals", int_goals) + +# ============================================================================= +# Tile Randomization +# ============================================================================= + +func _randomize_tiles_around_player(player: Node): + """Randomize 9 tiles in 3x3 area around player position.""" + if not main_scene: + return + + var enhanced_gridmap = main_scene.get_node_or_null("EnhancedGridMap") + if not enhanced_gridmap: + return + + var center = player.current_position + var rng = RandomNumberGenerator.new() + rng.randomize() + + # 3x3 area around player + for dx in range(-1, 2): + for dz in range(-1, 2): + var pos = Vector2i(center.x + dx, center.y + dz) + var cell = Vector3i(pos.x, 1, pos.y) + + # Check if position is valid + if not enhanced_gridmap.is_position_valid(pos): + continue + + # Check if there are tiles nearby or if empty + var current_item = enhanced_gridmap.get_cell_item(cell) + + # Decide: delete, spawn, or randomize + var action = rng.randi() % 3 + + match action: + 0: # Delete tile + if current_item != -1: + main_scene.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1) + 1: # Spawn new tile + if current_item == -1: + var new_tile = rng.randi_range(7, 10) + main_scene.rpc("sync_grid_item", cell.x, cell.y, cell.z, new_tile) + 2: # Randomize existing + if current_item != -1: + var new_tile = rng.randi_range(7, 10) + while new_tile == current_item: + new_tile = rng.randi_range(7, 10) + main_scene.rpc("sync_grid_item", cell.x, cell.y, cell.z, new_tile) + +# ============================================================================= +# Score Getters +# ============================================================================= + +func get_player_score(peer_id: int) -> int: + return player_scores.get(peer_id, 0) + +func get_leaderboard() -> Array: + var sorted_scores = [] + for peer_id in player_scores.keys(): + sorted_scores.append({"peer_id": peer_id, "score": player_scores[peer_id]}) + sorted_scores.sort_custom(func(a, b): return a.score > b.score) + return sorted_scores + +func get_time_remaining() -> float: + return current_cycle_timer + +func reset_scores(): + player_scores.clear() + _update_leaderboard() diff --git a/scripts/managers/player_race_manager.gd b/scripts/managers/player_race_manager.gd index d82a228..aa24487 100644 --- a/scripts/managers/player_race_manager.gd +++ b/scripts/managers/player_race_manager.gd @@ -1,52 +1,26 @@ extends Node +# PlayerRaceManager - Refactored to remove lap-based racing +# Now only handles goals and playerboard state for the cycle-based system + var player: Node3D var enhanced_gridmap: Node -# Race state -var current_lap: int = 0 -var first_lap_goals: Array[int] = [] -var second_lap_goals: Array[int] = [] -var race_position: int = 0 -var has_finished_race: bool = false -static var lap1_finishers: int = 0 -static var lap2_finishers: int = 0 - -# Goals and Playerboard +# Goals and Playerboard (core functionality retained) var goals: Array[int] = [0, 0, 0, 0, 0, 0, 0, 0, 0] var playerboard: Array[int] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] -var can_finish: bool = false -# Finish locations (right side for 1st lap) -var finish_locations = [ - Vector2i(13, 0), Vector2i(13, 1), Vector2i(13, 2), Vector2i(13, 3), - Vector2i(13, 4), Vector2i(13, 5), Vector2i(13, 6), Vector2i(13, 7), - Vector2i(13, 8), Vector2i(13, 9), Vector2i(13, 10), Vector2i(13, 11), - Vector2i(13, 12), Vector2i(13, 13) -] - -# Spawn locations (left side for 2nd lap finish) -var spawn_locations = [ - Vector2i(0, 0), Vector2i(0, 1), Vector2i(0, 2), Vector2i(0, 3), - Vector2i(0, 4), Vector2i(0, 5), Vector2i(0, 6), Vector2i(0, 7), - Vector2i(0, 8), Vector2i(0, 9), Vector2i(0, 10), Vector2i(0, 11), - Vector2i(0, 12), Vector2i(0, 13) -] - -# Helper function to get current finish locations based on lap -func get_current_finish_locations() -> Array: - if current_lap == 0: - return finish_locations # 1st lap: finish at right side - else: - return spawn_locations # 2nd lap: finish at left side (spawn locations) +# DEPRECATED: Lap system removed - keeping stubs for compatibility +var can_finish: bool = false # No longer used +var current_lap: int = 0 # No longer used +var race_position: int = 0 # No longer used +var has_finished_race: bool = false # No longer used +static var lap1_finishers: int = 0 # No longer used +static var lap2_finishers: int = 0 # No longer used func initialize(p_player: Node3D, p_gridmap: Node): player = p_player enhanced_gridmap = p_gridmap - - if player.is_multiplayer_authority(): - first_lap_goals = goals.duplicate() - generate_second_lap_goals() func get_ordinal_string(number: int) -> String: match number: @@ -56,14 +30,9 @@ func get_ordinal_string(number: int) -> String: 4: return "4th" _: return str(number) + "th" -func generate_second_lap_goals(): - second_lap_goals.clear() - for i in range(9): - var val = (randi() % 4) + 7 - second_lap_goals.append(val) - - if player.is_multiplayer_authority(): - player.rpc("sync_second_lap_goals", second_lap_goals) +# ============================================================================= +# Goal Pattern Matching (Core Functionality) +# ============================================================================= func check_3x3_section(board: Array, goals_pattern: Array, start_row: int, start_col: int) -> bool: for i in range(3): @@ -81,6 +50,7 @@ func check_3x3_section(board: Array, goals_pattern: Array, start_row: int, start return true func check_pattern_match() -> bool: + """Check if playerboard matches goals pattern. Core function for goal completion.""" if playerboard.size() != 25 or goals.size() != 9: return false @@ -105,98 +75,51 @@ func check_pattern_match() -> bool: return true return false +# ============================================================================= +# DEPRECATED STUBS - Left for backward compatibility, do nothing +# ============================================================================= + +var finish_locations = [] # No longer used +var spawn_locations = [] # No longer used +var first_lap_goals: Array[int] = [] +var second_lap_goals: Array[int] = [] + +func get_current_finish_locations() -> Array: + # DEPRECATED: No finish line in cycle-based system + return [] + func update_finish_availability(): - can_finish = check_pattern_match() - if player.is_multiplayer_authority(): - if can_finish: - highlight_finish_line() - else: - unhighlight_finish_line() + # DEPRECATED: No finish line checking needed + pass func highlight_finish_line(): - if not player.is_multiplayer_authority() or player.is_bot: - return - var current_finish = get_current_finish_locations() - for finish_pos in current_finish: - if enhanced_gridmap: - enhanced_gridmap.set_cell_item( - Vector3i(finish_pos.x, 0, finish_pos.y), - enhanced_gridmap.hover_item - ) + # DEPRECATED: No finish line to highlight + pass func unhighlight_finish_line(): - if not player.is_multiplayer_authority() or player.is_bot: - return - var current_finish = get_current_finish_locations() - for finish_pos in current_finish: - if enhanced_gridmap: - enhanced_gridmap.set_cell_item( - Vector3i(finish_pos.x, 0, finish_pos.y), - enhanced_gridmap.normal_items[0] - ) + # DEPRECATED: No finish line to unhighlight + pass func is_at_finish_line() -> bool: - var current_finish = get_current_finish_locations() - return player.current_position in current_finish + # DEPRECATED: Always false, no finish line + return false func finish_race(): - if current_lap == 0: - lap1_finishers += 1 - race_position = lap1_finishers - var message = "Finish 1st lap on " + get_ordinal_string(race_position) - if player.is_multiplayer_authority(): - player.rpc("display_message", message) - - current_lap += 1 - start_new_lap() - - elif current_lap == 1: - lap2_finishers += 1 - race_position = lap2_finishers - var message = "RACE COMPLETE! Finished " + get_ordinal_string(race_position) - if player.is_multiplayer_authority(): - player.rpc("display_message", message) - player.rpc("complete_race", race_position) + # DEPRECATED: No race finish in cycle-based system + pass -# Called when player finishes the entire race (2nd lap complete) -func on_race_completed(final_position: int): - has_finished_race = true - race_position = final_position - can_finish = false - - # Disable all player input - player.set_process_input(false) - player.set_process_unhandled_input(false) - player.is_my_turn = false - player.action_points = 0 - - # Clear any highlights - if player.action_manager: - player.action_manager.clear_highlights() - player.action_manager.clear_playerboard_highlights() - - # Unhighlight finish line - unhighlight_finish_line() - - print("Player %s finished the race in position %d!" % [player.name, final_position]) +func on_race_completed(_final_position: int): + # DEPRECATED: No race completion + pass func start_new_lap(): - if current_lap == 1: - # Update goals to 2nd lap goals - goals = second_lap_goals.duplicate() - can_finish = false - - # Sync with all clients - if player.is_multiplayer_authority(): - player.rpc("sync_position", player.current_position) - player.rpc("sync_playerboard", playerboard) - player.rpc("sync_goals", goals) - - print("Started 2nd lap with new goals: ", goals) + # DEPRECATED: No laps in cycle-based system + pass func find_valid_position_in_finish_line() -> Vector2i: - var current_finish = get_current_finish_locations() - for pos in current_finish: - if not player.is_position_occupied(pos): - return pos + # DEPRECATED: No finish line return Vector2i(-1, -1) + +func generate_second_lap_goals(): + # DEPRECATED: No second lap goals needed + pass diff --git a/scripts/managers/playerboard_manager.gd b/scripts/managers/playerboard_manager.gd index 8bcff87..9a0500b 100644 --- a/scripts/managers/playerboard_manager.gd +++ b/scripts/managers/playerboard_manager.gd @@ -52,12 +52,13 @@ func grab_item(grid_position: Vector2i) -> bool: # Apply changes locally first, server will validate/sync enhanced_gridmap.set_cell_item(cell, -1) # Remove item visually immediately - # Check if grabbed item is a holo tile (11-14) and trigger special effect + # Check if grabbed item is a holo tile (11-14) - add to powerup instead of triggering effect var is_holo = item >= 11 and item <= 14 if is_holo: - var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") - if special_tiles_manager: - special_tiles_manager.trigger_random_effect() + # Add holo pickup to power-up manager (4 pickups = 1 bar) + var powerup_manager = player.get_node_or_null("PowerUpManager") + if powerup_manager: + powerup_manager.add_holo_pickup() # Convert holo tile to normal tile (11->7, 12->8, 13->9, 14->10) item = item - 4 @@ -67,6 +68,9 @@ func grab_item(grid_position: Vector2i) -> bool: var main = player.get_tree().get_root().get_node_or_null("Main") if main and main.ui_manager: main.ui_manager.update_playerboard_ui() + + # Check if goal is completed after grabbing + _check_goal_completion() # === Server Sync === if multiplayer.is_server(): @@ -151,17 +155,18 @@ func bot_try_grab_item() -> bool: var empty_slot = player.playerboard.find(-1) if empty_slot != -1: if player.is_multiplayer_authority(): - # Check if grabbed item is a holo tile (11-14) + # Check if grabbed item is a holo tile (11-14) - add to powerup if item >= 11 and item <= 14: - var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") - if special_tiles_manager: - special_tiles_manager.trigger_random_effect() + var powerup_manager = player.get_node_or_null("PowerUpManager") + if powerup_manager: + powerup_manager.add_holo_pickup() item = item - 4 # Convert to normal tile player.playerboard[empty_slot] = item player.rpc("sync_grid_item", current_cell.x, current_cell.y, current_cell.z, -1) player.rpc("sync_playerboard", player.playerboard) player.has_performed_action = true player.action_points -= 1 + _check_goal_completion() return true # Check adjacent cells if nothing at current position @@ -590,3 +595,52 @@ func _highlight_adjacent_playerboard_slots(): var slot = main.playerboard_ui.get_child(adj_slot) if slot.get_child_count() > 2: slot.get_child(2).show() + +# ============================================================================= +# Goal Completion Check +# ============================================================================= + +func _check_goal_completion(): + """Check if playerboard matches goals and trigger completion rewards.""" + if not player.race_manager: + return + + # Check if the pattern matches + if player.race_manager.check_pattern_match(): + print("[PlayerboardManager] Goal completed for player %s!" % player.name) + + # Award power-up bar for goal completion + var powerup_manager = player.get_node_or_null("PowerUpManager") + if powerup_manager: + powerup_manager.add_goal_completion_reward() + + # Notify GoalsCycleManager for scoring + var main = player.get_tree().get_root().get_node_or_null("Main") + if main: + var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager") + if goals_cycle_manager: + goals_cycle_manager.on_goal_completed(player, goals_cycle_manager.get_time_remaining()) + else: + # Fallback if manager not initialized yet + player.rpc("display_message", "Goal completed!") + +func clear_and_convert_to_score() -> int: + """Clear playerboard and return score for matching tiles.""" + var matching_score = 0 + var goals = player.goals + var playerboard = player.playerboard + + # Check center 3x3 of playerboard against goals + for i in range(3): + for j in range(3): + var goal_idx = i * 3 + j + var board_idx = (i + 1) * 5 + (j + 1) # Center 3x3 in 5x5 board + + if goal_idx < goals.size() and board_idx < playerboard.size(): + if goals[goal_idx] != -1 and playerboard[board_idx] == goals[goal_idx]: + matching_score += 10 # 10 points per matching tile + + # Clear playerboard + player.playerboard.fill(-1) + + return matching_score diff --git a/scripts/managers/powerup_manager.gd b/scripts/managers/powerup_manager.gd new file mode 100644 index 0000000..cefb1ab --- /dev/null +++ b/scripts/managers/powerup_manager.gd @@ -0,0 +1,128 @@ +extends Node + +# PowerUpManager - Handles power-up points, holo tile tracking, and special effect usage + +const MAX_POINTS: int = 12 +const POINTS_PER_BAR: int = 4 +const MAX_BARS: int = 4 +const HOLO_PICKUPS_PER_BAR: int = 4 + +var player: Node3D +var enhanced_gridmap: Node + +# Power-up state +var current_points: int = 0 +var holo_pickup_count: int = 0 + +signal points_changed(current: int, max_points: int) +signal bar_filled() +signal effect_used() + +func initialize(p_player: Node3D, p_gridmap: Node): + player = p_player + enhanced_gridmap = p_gridmap + +# ============================================================================= +# Holo Tile Pickup +# ============================================================================= + +func add_holo_pickup(): + """Called when player picks up a holo tile (11-14).""" + holo_pickup_count += 1 + + if holo_pickup_count >= HOLO_PICKUPS_PER_BAR: + holo_pickup_count = 0 + _add_bar() + + print("[PowerUp] Player %s picked up holo tile. Count: %d/4" % [player.name, holo_pickup_count]) + + if player.is_multiplayer_authority(): + rpc("sync_holo_count", holo_pickup_count, current_points) + +func _add_bar(): + """Add one full bar (4 points) of power-up.""" + var points_to_add = POINTS_PER_BAR + current_points = min(current_points + points_to_add, MAX_POINTS) + + emit_signal("bar_filled") + emit_signal("points_changed", current_points, MAX_POINTS) + + player.rpc("display_message", "Power-up bar filled!") + print("[PowerUp] Player %s gained 1 bar! Total: %d/%d points" % [player.name, current_points, MAX_POINTS]) + +# ============================================================================= +# Goal Completion Reward +# ============================================================================= + +func add_goal_completion_reward(): + """Called when player completes a goal pattern. Awards 1 bar.""" + _add_bar() + print("[PowerUp] Player %s completed goal - awarded 1 bar" % [player.name]) + +# ============================================================================= +# Using Special Effects +# ============================================================================= + +func can_use_special() -> bool: + """Returns true if player has at least 1 bar (4 points).""" + return current_points >= POINTS_PER_BAR + +func get_bars() -> int: + """Returns current number of full bars.""" + return current_points / POINTS_PER_BAR + +func use_special_effect(): + """Consume 1 bar and trigger a random special effect.""" + if not can_use_special(): + player.rpc("display_message", "Not enough power-up!") + return false + + # Consume 1 bar + current_points -= POINTS_PER_BAR + emit_signal("effect_used") + emit_signal("points_changed", current_points, MAX_POINTS) + + # Trigger random special effect via SpecialTilesManager + var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") + if special_tiles_manager: + special_tiles_manager.trigger_random_effect() + + print("[PowerUp] Player %s used special effect! Remaining: %d/%d points" % [player.name, current_points, MAX_POINTS]) + + if player.is_multiplayer_authority(): + rpc("sync_points", current_points) + + return true + +# ============================================================================= +# Sync +# ============================================================================= + +@rpc("any_peer", "call_local", "reliable") +func sync_holo_count(count: int, points: int): + holo_pickup_count = count + current_points = points + emit_signal("points_changed", current_points, MAX_POINTS) + +@rpc("any_peer", "call_local", "reliable") +func sync_points(points: int): + current_points = points + emit_signal("points_changed", current_points, MAX_POINTS) + +# ============================================================================= +# Getters +# ============================================================================= + +func get_points() -> int: + return current_points + +func get_max_points() -> int: + return MAX_POINTS + +func get_fill_percentage() -> float: + return float(current_points) / float(MAX_POINTS) + +func reset(): + current_points = 0 + holo_pickup_count = 0 + emit_signal("points_changed", current_points, MAX_POINTS) diff --git a/scripts/managers/ui_manager.gd b/scripts/managers/ui_manager.gd index 0f8b864..22d885a 100644 --- a/scripts/managers/ui_manager.gd +++ b/scripts/managers/ui_manager.gd @@ -136,3 +136,195 @@ func update_button_states(): func set_local_player(player): local_player_character = player + + # Connect to powerup signals + var powerup_manager = player.get_node_or_null("PowerUpManager") + if powerup_manager: + powerup_manager.points_changed.connect(_on_powerup_points_changed) + +# ============================================================================= +# Power-Up Bar UI (Battery Style) +# ============================================================================= + +var powerup_bar: HBoxContainer +var powerup_segments: Array = [] + +func setup_powerup_bar_ui(main_node): + """Create battery-style power-up bar with 4 segments.""" + var parent = main_node.get_node_or_null("PlayerboardUI") + if not parent: + parent = main_node + + # Create container + powerup_bar = HBoxContainer.new() + powerup_bar.name = "PowerUpBar" + powerup_bar.custom_minimum_size = Vector2(200, 30) + + # Position above playerboard + powerup_bar.position = Vector2(0, -40) + + # Create label + var label = Label.new() + label.text = "POWER: " + label.add_theme_font_size_override("font_size", 14) + powerup_bar.add_child(label) + + # Create 4 battery segments + powerup_segments.clear() + for i in range(4): + var segment = Panel.new() + segment.custom_minimum_size = Vector2(40, 24) + segment.name = "Segment" + str(i) + + # Style the segment + var style = StyleBoxFlat.new() + style.bg_color = Color(0.2, 0.2, 0.2, 0.8) # Dark empty + style.border_color = Color(0.4, 0.8, 0.4, 1.0) # Green border + style.set_border_width_all(2) + style.corner_radius_top_left = 4 if i == 0 else 0 + style.corner_radius_bottom_left = 4 if i == 0 else 0 + style.corner_radius_top_right = 4 if i == 3 else 0 + style.corner_radius_bottom_right = 4 if i == 3 else 0 + segment.add_theme_stylebox_override("panel", style) + + powerup_bar.add_child(segment) + powerup_segments.append(segment) + + parent.add_child(powerup_bar) + +func update_powerup_bar(current_points: int, max_points: int): + """Update battery segments based on current power-up points.""" + var bars_filled = current_points / 4 # 4 points per bar + + for i in range(powerup_segments.size()): + var segment = powerup_segments[i] + var style = segment.get_theme_stylebox("panel").duplicate() + + if i < bars_filled: + # Filled segment - bright green + style.bg_color = Color(0.3, 0.9, 0.3, 1.0) + else: + # Empty segment - dark + style.bg_color = Color(0.2, 0.2, 0.2, 0.8) + + segment.add_theme_stylebox_override("panel", style) + +func _on_powerup_points_changed(current: int, max_points: int): + update_powerup_bar(current, max_points) + +# ============================================================================= +# Leaderboard UI +# ============================================================================= + +var leaderboard_panel: PanelContainer + +func setup_leaderboard_ui(main_node): + """Create leaderboard panel on right side of screen.""" + leaderboard_panel = PanelContainer.new() + leaderboard_panel.name = "LeaderboardPanel" + leaderboard_panel.custom_minimum_size = Vector2(180, 180) + + # Position on right side + leaderboard_panel.set_anchors_preset(Control.PRESET_TOP_RIGHT) + leaderboard_panel.position = Vector2(-200, 100) + + # Style the panel + var style = StyleBoxFlat.new() + style.bg_color = Color(0.1, 0.1, 0.1, 0.85) + style.border_color = Color(0.8, 0.7, 0.2, 1.0) # Gold border + style.set_border_width_all(2) + style.corner_radius_top_left = 8 + style.corner_radius_top_right = 8 + style.corner_radius_bottom_left = 8 + style.corner_radius_bottom_right = 8 + leaderboard_panel.add_theme_stylebox_override("panel", style) + + var vbox = VBoxContainer.new() + vbox.name = "VBox" + + # Title + var title = Label.new() + title.text = "LEADERBOARD" + title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + title.add_theme_font_size_override("font_size", 16) + title.add_theme_color_override("font_color", Color(0.9, 0.8, 0.2)) + vbox.add_child(title) + + # Separator + var sep = HSeparator.new() + vbox.add_child(sep) + + # Create 4 player entries + for i in range(4): + var entry = HBoxContainer.new() + entry.name = "Entry" + str(i + 1) + entry.visible = false + + var rank_label = Label.new() + rank_label.name = "RankLabel" + rank_label.custom_minimum_size = Vector2(40, 0) + rank_label.text = _get_rank_text(i + 1) + rank_label.add_theme_font_size_override("font_size", 14) + entry.add_child(rank_label) + + var name_label = Label.new() + name_label.name = "NameLabel" + name_label.custom_minimum_size = Vector2(80, 0) + name_label.clip_text = true + name_label.text = "---" + name_label.add_theme_font_size_override("font_size", 14) + entry.add_child(name_label) + + var score_label = Label.new() + score_label.name = "ScoreLabel" + score_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + score_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + score_label.text = "0" + score_label.add_theme_font_size_override("font_size", 14) + score_label.add_theme_color_override("font_color", Color(0.8, 1.0, 0.8)) + entry.add_child(score_label) + + vbox.add_child(entry) + + leaderboard_panel.add_child(vbox) + main_node.add_child(leaderboard_panel) + +func _get_rank_text(rank: int) -> String: + match rank: + 1: return "1st" + 2: return "2nd" + 3: return "3rd" + 4: return "4th" + _: return str(rank) + "th" + +# ============================================================================= +# Timer Labels for Goal Panels +# ============================================================================= + +func setup_timer_labels(main_node): + """Add timer labels to each player goals panel.""" + var all_player_goals = main_node.get_node_or_null("AllPlayerGoals") + if not all_player_goals: + return + + for i in range(all_player_goals.get_child_count()): + var panel = all_player_goals.get_child(i) + + # Skip if timer already exists + if panel.get_node_or_null("TimerLabel"): + continue + + var timer_label = Label.new() + timer_label.name = "TimerLabel" + timer_label.text = "01:00" + timer_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + timer_label.add_theme_font_size_override("font_size", 18) + timer_label.add_theme_color_override("font_color", Color(1.0, 0.9, 0.3)) + + # Add at the top of the panel + var margin = panel.get_node_or_null("MarginContainer") + if margin: + margin.add_child(timer_label) + margin.move_child(timer_label, 0) + else: + panel.add_child(timer_label)