feat: Introduce modular player system with dedicated managers for movement, race, input, playerboard, actions, special tiles, and powerups, along with a new main scene and documentation.

This commit is contained in:
2025-12-16 02:37:26 +08:00
parent 96f5754f99
commit e41ffcfb67
9 changed files with 494 additions and 146 deletions
+11
View File
@@ -55,8 +55,19 @@ func start_cycle():
emit_signal("cycle_started")
if multiplayer.is_server():
# Initialize scores for all connected players
_initialize_player_scores()
rpc("sync_cycle_start")
func _initialize_player_scores():
"""Initialize scores for all connected players to 0."""
var all_players = get_tree().get_nodes_in_group("Players")
for player in all_players:
var peer_id = player.get_multiplayer_authority()
if not player_scores.has(peer_id):
player_scores[peer_id] = 0
_update_leaderboard()
@rpc("authority", "call_local", "reliable")
func sync_cycle_start():
current_cycle_timer = CYCLE_DURATION
@@ -0,0 +1 @@
uid://c6ip5g2kja65e
+9 -1
View File
@@ -34,6 +34,12 @@ func get_ordinal_string(number: int) -> String:
# Goal Pattern Matching (Core Functionality)
# =============================================================================
func _normalize_tile(tile: int) -> int:
"""Convert holo tiles (11-14) to normal tiles (7-10) for goal comparison."""
if tile >= 11 and tile <= 14:
return tile - 4
return tile
func check_3x3_section(board: Array, goals_pattern: Array, start_row: int, start_col: int) -> bool:
for i in range(3):
for j in range(3):
@@ -45,7 +51,9 @@ func check_3x3_section(board: Array, goals_pattern: Array, start_row: int, start
for i in range(3):
for j in range(3):
if goals_pattern[i][j] != -1:
if board[start_row + i][start_col + j] != goals_pattern[i][j]:
# Normalize board value to treat holo tiles (11-14) same as normal (7-10)
var normalized_board = _normalize_tile(board[start_row + i][start_col + j])
if normalized_board != goals_pattern[i][j]:
return false
return true
+16 -3
View File
@@ -14,6 +14,12 @@ func initialize(p_player: Node3D, p_gridmap: Node):
player = p_player
enhanced_gridmap = p_gridmap
func _normalize_tile(tile: int) -> int:
"""Convert holo tiles (11-14) to normal tiles (7-10) for goal comparison."""
if tile >= 11 and tile <= 14:
return tile - 4
return tile
# =============================================================================
# GRAB Operations
# =============================================================================
@@ -230,8 +236,10 @@ func auto_put_item() -> bool:
if current_item == -1:
continue
# Normalize item for goal comparison (holo tiles 11-14 = normal tiles 7-10)
var normalized_item = _normalize_tile(current_item)
# Item is not in goals at all → definitely junk, put this first
if not current_item in player.goals:
if not normalized_item in player.goals:
put_slot = i
break
@@ -253,8 +261,10 @@ func auto_put_item() -> bool:
var goal_row = row - 1
var goal_col = col - 1
var expected_goal = player.goals[goal_row * 3 + goal_col]
# Normalize for comparison (holo tiles 11-14 = normal tiles 7-10)
var normalized_board = _normalize_tile(board_item)
# If the item doesn't match what should be in this goal position, it's misplaced
if board_item != expected_goal and expected_goal != -1:
if normalized_board != expected_goal and expected_goal != -1:
put_slot = i
break
@@ -390,6 +400,9 @@ func find_best_goal_slot_for_item(item: int) -> int:
if item == -1:
return -1
# Normalize item - treat holo tiles (11-14) the same as normal tiles (7-10)
var normalized_item = _normalize_tile(item)
# Convert goals to 2D (3x3)
var goals_2d = []
for i in range(3):
@@ -401,7 +414,7 @@ func find_best_goal_slot_for_item(item: int) -> int:
# Search for where this item should go in the central 3x3 (mapped to 5x5 board)
for i in range(3):
for j in range(3):
if goals_2d[i][j] == item:
if goals_2d[i][j] == normalized_item:
var board_row = i + 1 # offset to center in 5x5
var board_col = j + 1
var slot_index = board_row * 5 + board_col
+1
View File
@@ -0,0 +1 @@
uid://dhm40qesad4cm
+93 -124
View File
@@ -137,60 +137,52 @@ func update_button_states():
func set_local_player(player):
local_player_character = player
# Connect to powerup signals
# Connect to powerup signals with deferred call (manager needs time to initialize)
_connect_powerup_manager_deferred(player)
func _connect_powerup_manager_deferred(player):
"""Wait for PowerUpManager to be initialized before connecting."""
await player.get_tree().create_timer(0.3).timeout
var powerup_manager = player.get_node_or_null("PowerUpManager")
if powerup_manager:
powerup_manager.points_changed.connect(_on_powerup_points_changed)
if not powerup_manager.points_changed.is_connected(_on_powerup_points_changed):
powerup_manager.points_changed.connect(_on_powerup_points_changed)
# Initialize bar with current values
update_powerup_bar(powerup_manager.get_points(), powerup_manager.get_max_points())
# =============================================================================
# Power-Up Bar UI (Battery Style)
# =============================================================================
var powerup_bar: HBoxContainer
var powerup_bar: PanelContainer
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
"""Get reference to existing PowerUpBar in scene and cache segment references."""
powerup_bar = main_node.get_node_or_null("PowerUpBar")
if not powerup_bar:
push_warning("PowerUpBar node not found in scene")
return
# 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
# Get segment references from scene
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)
var hbox = powerup_bar.get_node_or_null("HBox")
if hbox:
for i in range(4):
var segment = hbox.get_node_or_null("Segment" + str(i))
if segment:
# Apply initial empty style
var style = StyleBoxFlat.new()
style.bg_color = Color(0.15, 0.15, 0.15, 1.0)
style.border_color = Color(0.3, 0.7, 0.3, 1.0)
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_segments.append(segment)
func update_powerup_bar(current_points: int, max_points: int):
"""Update battery segments based on current power-up points."""
@@ -219,75 +211,22 @@ func _on_powerup_points_changed(current: int, max_points: int):
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)
"""Get reference to existing LeaderboardPanel in scene."""
leaderboard_panel = main_node.get_node_or_null("LeaderboardPanel")
if not leaderboard_panel:
push_warning("LeaderboardPanel node not found in scene")
return
# Position on right side
leaderboard_panel.set_anchors_preset(Control.PRESET_TOP_RIGHT)
leaderboard_panel.position = Vector2(-200, 100)
# Style the panel
# Apply styling to 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.bg_color = Color(0.08, 0.08, 0.12, 0.92)
style.border_color = Color(0.85, 0.75, 0.25, 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:
@@ -302,29 +241,59 @@ func _get_rank_text(rank: int) -> String:
# =============================================================================
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:
"""Apply styling to standalone GoalsTimer in scene."""
var goals_timer = main_node.get_node_or_null("GoalsTimer")
if not goals_timer:
push_warning("GoalsTimer node not found in scene")
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"):
# Apply dark background style
var style = StyleBoxFlat.new()
style.bg_color = Color(0.1, 0.1, 0.15, 0.9)
style.border_color = Color(1.0, 0.85, 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
goals_timer.add_theme_stylebox_override("panel", style)
# Style the timer label
var timer_label = goals_timer.get_node_or_null("VBox/TimerLabel")
if timer_label:
timer_label.add_theme_color_override("font_color", Color(1.0, 0.85, 0.2))
var suffix_label = goals_timer.get_node_or_null("VBox/SuffixLabel")
if suffix_label:
suffix_label.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
# Method to update leaderboard with all players in match
func initialize_leaderboard_with_players(players: Array):
"""Initialize leaderboard showing all players with score 0."""
if not leaderboard_panel:
return
var vbox = leaderboard_panel.get_node_or_null("MarginContainer/VBox")
if not vbox:
vbox = leaderboard_panel.get_node_or_null("VBox")
if not vbox:
return
for i in range(4):
var entry = vbox.get_node_or_null("Entry" + str(i + 1))
if not entry:
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)
if i < players.size():
var player = players[i]
var name_label = entry.get_node_or_null("NameLabel")
var score_label = entry.get_node_or_null("ScoreLabel")
if name_label:
name_label.text = str(player.name) if player else "Player " + str(i + 1)
if score_label:
score_label.text = str(player.score) if player and player.get("score") else "0"
entry.visible = true
else:
panel.add_child(timer_label)
entry.visible = false