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)