feat: Implement initial game structure with core logic, various managers, player scene, and project configuration.
This commit is contained in:
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user