feat: Establish initial game structure including lobby, UI, and core gameplay managers.

This commit is contained in:
Yogi Wiguna
2026-02-02 15:01:30 +08:00
parent 9201c99d42
commit 66d34d0d29
21 changed files with 1688 additions and 1475 deletions
@@ -0,0 +1 @@
uid://c2n4v6brcjtwn
+7 -4
View File
@@ -133,12 +133,15 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
# Wall/Blocked -> Stagger in place
other_player.rpc("apply_stagger", 1.5)
# 4. Consume Boost
# 4. Consume Boost (Full) - One hit per charge
if player.powerup_manager:
player.powerup_manager.reset_boost()
# Consume all available boost to force a full recharge cycle
player.powerup_manager.consume_boost(100.0)
# 5. Reset Attack Mode
player.is_attack_mode = false
# 5. Attack Mode Persistence
# logic moved to consume_boost: checks if <= 0 then disables.
# So we do NOT force disable here.
# player.is_attack_mode = false
return true
+1 -1
View File
@@ -683,7 +683,7 @@ func _check_goal_completion():
if player.race_manager.check_pattern_match():
print("[PlayerboardManager] Goal completed for player %s!" % player.name)
# Award power-up bar for goal completion
# Level up boost difficulty on goal completion
var powerup_manager = player.get_node_or_null("PowerUpManager")
if powerup_manager:
powerup_manager.add_goal_completion_reward()
+63 -13
View File
@@ -3,13 +3,17 @@ extends Node
# PowerUpManager - Handles Boost Meter (Time-based logic)
# Note: Inventory logic is now in SpecialTilesManager + PlayerboardManager
const MAX_BOOST: float = 100.0
const BASE_FILL_RATE: float = 4.0 # 4 points per second baseline (25s to full)
const MAX_BOOST: float = 100.0 # Fills to 100. Activation requires FULL bar (>= 100).
const BASE_FILL_RATE: float = 10.0 # 10 points per second (10s to full) - user requested faster/simpler
var player: Node3D
var enhanced_gridmap: Node
var goal_manager: Node
# Progressive Difficulty Settings
const FILL_TIMES: Array = [2.0, 3.0, 4.0, 5.0, 6.0, 7.5, 8.5, 10.0] # Seconds to full for Lvl 1-8 (2s to 10s)
var current_level: int = 1
# Boost State
var current_boost: float = 0.0
@@ -49,7 +53,13 @@ func _process(delta):
# Use authority ID for lookup
multiplier = goal_manager.get_boost_multiplier(player.get_multiplayer_authority())
current_boost += BASE_FILL_RATE * multiplier * delta
# Calculate Dynamic Fill Rate based on Level
# Level 1 (Index 0) -> Level 8 (Index 7)
var level_idx = clamp(current_level - 1, 0, FILL_TIMES.size() - 1)
var fill_time = FILL_TIMES[level_idx]
var current_rate = MAX_BOOST / fill_time # e.g. 100/10 = 10/s, 100/60 = 1.66/s
current_boost += current_rate * multiplier * delta
current_boost = min(current_boost, MAX_BOOST)
# Update UI (Cast to int for compatibility with existing UI slider/bar)
@@ -59,7 +69,7 @@ func _process(delta):
_on_boost_full()
func _on_boost_full():
player.is_attack_mode = true
# player.is_attack_mode = true # Removed auto-activate
emit_signal("bar_filled")
NotificationManager.send_message(player, NotificationManager.MESSAGES.ATTACK_MODE_READY, NotificationManager.MessageType.POWERUP)
print("[PowerUp] Player %s Boost Full! Entering Attack Mode." % player.name)
@@ -69,7 +79,7 @@ func _on_boost_full():
func reset_boost():
current_boost = 0.0
player.is_attack_mode = false
# player.is_attack_mode = false # Do not auto-disable here, managed by usage
emit_signal("points_changed", 0, int(MAX_BOOST))
emit_signal("boost_reset")
@@ -104,15 +114,51 @@ func get_fill_percentage() -> float:
return current_boost / MAX_BOOST
func can_use_special() -> bool:
return current_boost >= MAX_BOOST
# Use small epsilon for float comparison to avoid "99.999" issues
return current_boost >= (MAX_BOOST - 0.1)
func enable_attack_mode():
"""Enable Attack Mode without consuming boost."""
player.is_attack_mode = true
NotificationManager.send_message(player, NotificationManager.MESSAGES.ATTACK_MODE_READY, NotificationManager.MessageType.POWERUP)
print("[PowerUp] Attack Mode Enabled (Free).")
func use_special_effect() -> bool:
if not can_use_special():
return false
# Consume boost
reset_boost()
# Enable Attack Mode explicitly
player.is_attack_mode = true
# Do NOT consume boost here. Boost acts as "fuel" for the attacks.
# Notification check handled by caller or signal
return true
func consume_boost(amount: float):
"""Consume a specific amount of boost."""
current_boost -= amount
if current_boost <= 0:
current_boost = 0.0
player.is_attack_mode = false # Out of fuel
emit_signal("boost_reset") # UI update for empty
print("[PowerUp] Boost depleted. Attack Mode OFF.")
emit_signal("points_changed", int(current_boost), int(MAX_BOOST))
if player.is_multiplayer_authority():
rpc("sync_boost", current_boost)
func spawn_boost_reward() -> bool:
"""Alternative Boost Usage: Spawn a PowerUp Tile properly."""
if not can_use_special():
return false
if player.special_tiles_manager and player.special_tiles_manager.has_method("spawn_powerups_around"):
player.special_tiles_manager.spawn_powerups_around(player.current_position)
reset_boost() # Consumes full bar
print("[PowerUp] %s used Boost to SPAWN ITEM." % player.name)
return true
return false
func acquire_smash_bonus():
current_boost += 25.0 # Add 25% boost
@@ -122,8 +168,12 @@ func acquire_smash_bonus():
_on_boost_full()
func add_goal_completion_reward():
current_boost += 50.0 # Reward for completing goal
current_boost = min(current_boost, MAX_BOOST)
emit_signal("points_changed", int(current_boost), int(MAX_BOOST))
if current_boost >= MAX_BOOST:
_on_boost_full()
"""Called when player completes a goal board. Increases difficulty level."""
current_level += 1
if current_level > 8:
current_level = 8
var level_idx = clamp(current_level - 1, 0, FILL_TIMES.size() - 1)
print("[PowerUp] Player %s Completed Goal. Boost Level Up! Now: %d (Fill Time: %.1fs)" % [player.name, current_level, FILL_TIMES[level_idx]])
# Optional: Notify user of difficulty increase?
+49 -4
View File
@@ -11,6 +11,7 @@ var virtual_joystick: Control
var grab_button: Button
var put_button: Button
var special_button: Button
var spawn_boost_button: Button
var settings_button: Button
# Settings - persisted to config file
@@ -23,7 +24,8 @@ var joystick_position: Vector2 = Vector2(120, -120) # Relative to bottom-left
var button_positions: Dictionary = {
"grab": Vector2(-200, -240), # Relative to bottom-right
"put": Vector2(-120, -160),
"special": Vector2(-200, -80)
"special": Vector2(-200, -80),
"spawn_boost": Vector2(-120, -80)
}
var button_scale: float = 1.0
@@ -93,6 +95,7 @@ func _create_touch_ui():
grab_button = _find_or_create_action_button(container, "Grab", "👋", button_positions.grab)
put_button = _find_or_create_action_button(container, "Put", "📦", button_positions.put)
special_button = _find_or_create_action_button(container, "Special", "", button_positions.special)
spawn_boost_button = _find_or_create_action_button(container, "SpawnBoost", "🚀", button_positions.spawn_boost)
# Create settings button (top-right corner)
settings_button = container.get_node_or_null("SettingsBtn")
@@ -203,6 +206,7 @@ func _on_button_pressed(button_name: String):
"Grab": btn = grab_button
"Put": btn = put_button
"Special": btn = special_button
"SpawnBoost": btn = spawn_boost_button
if btn:
var tween = create_tween()
@@ -221,8 +225,32 @@ func _on_button_pressed(button_name: String):
"Special":
emit_signal("special_pressed")
var powerup_mgr = local_player.get_node_or_null("PowerUpManager")
if powerup_mgr and powerup_mgr.has_method("use_special_effect"):
powerup_mgr.use_special_effect()
if powerup_mgr:
# Require Full Boost to Activate (User Request: "Connect to boost bar")
var can_use = powerup_mgr.can_use_special()
var boost_val = powerup_mgr.current_boost
print("[TouchControls] Special Pressed. Boost: %s, CanUse: %s" % [boost_val, can_use])
if can_use:
powerup_mgr.use_special_effect() # Sets is_attack_mode=true, Does NOT consume boost yet
else:
# Optional feedback for not ready?
pass
else:
print("[TouchControls] PowerUpManager missing on player")
"SpawnBoost":
var powerup_mgr = local_player.get_node_or_null("PowerUpManager")
if powerup_mgr:
var can_use = powerup_mgr.can_use_special()
var boost_val = powerup_mgr.current_boost
print("[TouchControls] SpawnBoost Pressed. Boost: %s, CanUse: %s" % [boost_val, can_use])
if can_use: # Check if boost is full
if local_player.special_tiles_manager and local_player.special_tiles_manager.has_method("spawn_powerups_around"):
local_player.special_tiles_manager.spawn_powerups_around(local_player.current_position)
powerup_mgr.reset_boost() # Consume the boost manually since we bypassed use_special_effect
else:
print("[TouchControls] PowerUpManager missing on player")
func _on_button_released(button_name: String):
var btn: Button
@@ -230,6 +258,7 @@ func _on_button_released(button_name: String):
"Grab": btn = grab_button
"Put": btn = put_button
"Special": btn = special_button
"SpawnBoost": btn = spawn_boost_button
if btn:
var tween = create_tween()
@@ -268,7 +297,8 @@ func _load_settings():
var grab_pos = config.get_value("touch_controls", "grab_position", Vector2(-200, -240))
var put_pos = config.get_value("touch_controls", "put_position", Vector2(-120, -160))
var special_pos = config.get_value("touch_controls", "special_position", Vector2(-200, -80))
button_positions = {"grab": grab_pos, "put": put_pos, "special": special_pos}
var spawn_boost_pos = config.get_value("touch_controls", "spawn_boost_position", Vector2(-120, -80))
button_positions = {"grab": grab_pos, "put": put_pos, "special": special_pos, "spawn_boost": spawn_boost_pos}
# Apply loaded settings
_apply_settings()
@@ -286,6 +316,7 @@ func _save_settings():
config.set_value("touch_controls", "grab_position", button_positions.grab)
config.set_value("touch_controls", "put_position", button_positions.put)
config.set_value("touch_controls", "special_position", button_positions.special)
config.set_value("touch_controls", "spawn_boost_position", button_positions.spawn_boost)
var err = config.save(CONFIG_PATH)
if err != OK:
@@ -331,6 +362,14 @@ func _apply_settings():
special_button.offset_right = button_positions.special.x + button_size
special_button.offset_bottom = button_positions.special.y + button_size
if spawn_boost_button:
spawn_boost_button.visible = buttons_visible
spawn_boost_button.scale = Vector2(button_scale, button_scale)
spawn_boost_button.offset_left = button_positions.spawn_boost.x
spawn_boost_button.offset_top = button_positions.spawn_boost.y
spawn_boost_button.offset_right = button_positions.spawn_boost.x + button_size
spawn_boost_button.offset_bottom = button_positions.spawn_boost.y + button_size
# Force layer update
visible = true
@@ -375,6 +414,12 @@ func set_button_position(button_name: String, new_position: Vector2):
special_button.offset_top = new_position.y
special_button.offset_right = new_position.x + button_size
special_button.offset_bottom = new_position.y + button_size
"spawn_boost":
if spawn_boost_button:
spawn_boost_button.offset_left = new_position.x
spawn_boost_button.offset_top = new_position.y
spawn_boost_button.offset_right = new_position.x + button_size
spawn_boost_button.offset_bottom = new_position.y + button_size
func get_button_positions() -> Dictionary:
"""Get current button positions for settings UI."""
+6 -2
View File
@@ -271,7 +271,10 @@ func setup_powerup_bar_ui(main_node):
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
# 4 Segments total. Max Boost is 100. So each segment represents 25 points.
# Was previously dividing by 4, causing it to fill at 16 points!
var points_per_segment = _max_points / 4.0
var bars_filled = int(current_points / points_per_segment)
for i in range(powerup_segments.size()):
var segment = powerup_segments[i]
@@ -289,7 +292,8 @@ func update_powerup_bar(current_points: int, _max_points: int):
var _previous_bars: int = 0
func _on_powerup_points_changed(current: int, max_points: int):
var new_bars = current / 4
# Calculate based on max points (100) / 4 segments = 25 points per segment
var new_bars = int(current / 25.0)
# Detect if a new bar was filled
if new_bars > _previous_bars and powerup_bar: