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
+80 -3
View File
@@ -130,6 +130,13 @@ func _run_ai_tick():
print("[BotController] Action Taken: Put (Priority)")
return
# Priority 0: Attack Mode (Aggressive Chase)
if actor.get("is_attack_mode"):
print("[BotController] Attack Mode Active! Hunting targets...")
if await _try_attack_chase():
print("[BotController] Action Taken: Attack Pursuit")
return
# Priority 1: Use power-up sabotage if conditions are met
if await _try_use_powerup():
print("[BotController] Action Taken: PowerUp")
@@ -175,6 +182,10 @@ func _try_use_powerup() -> bool:
var powerup_manager = actor.get_node_or_null("PowerUpManager")
if not powerup_manager or not powerup_manager.can_use_special():
return false
# PREVENT LOOP: If already in Attack Mode, do not try to use it again
if actor.get("is_attack_mode"):
return false
# Evaluate sabotage opportunity
var eval = strategic_planner.evaluate_sabotage_opportunity()
@@ -185,7 +196,21 @@ func _try_use_powerup() -> bool:
_is_processing_action = true
_current_action = "sabotaging"
var success = powerup_manager.use_special_effect()
# 50/50 Chance: Attack Mode vs Spawn Item
var success = false
if rng.randf() > 0.5:
# Attack Mode
success = powerup_manager.use_special_effect()
print("[BotController] %s chose ATTACK MODE." % actor.name)
else:
# Spawn Item (if supported)
if powerup_manager.has_method("spawn_boost_reward"):
success = powerup_manager.spawn_boost_reward()
print("[BotController] %s chose SPAWN ITEM." % actor.name)
else:
# Fallback to attack mode if method missing
success = powerup_manager.use_special_effect()
if success:
print("[BotController] %s used power-up (reason: %s)" % [actor.name, eval.reason])
NotificationManager.send_message(actor, NotificationManager.MESSAGES.USED_SPECIAL_POWER, NotificationManager.MessageType.POWERUP)
@@ -197,6 +222,58 @@ func _try_use_powerup() -> bool:
_current_action = "idle"
return success
# =============================================================================
# Attack Mode Logic (Aggressive Chase)
# =============================================================================
func _try_attack_chase() -> bool:
"""Find nearest player and move towards them to RAM them."""
var victim = _find_nearest_victim()
if not victim:
# No victim found? Just behave normally (grab tiles etc)
return false
# Pathfind to victim
var path = enhanced_gridmap.find_path(
Vector2(actor.current_position),
Vector2(victim.current_position),
0,
false,
false
)
if path.size() >= 2:
var next_step = Vector2i(path[1].x, path[1].y)
# Move to next step (If occupied by victim, movement_manager will trigger PUSH)
if actor.movement_manager.simple_move_to(next_step):
_is_processing_action = true
_current_action = "attacking"
await _wait_with_variance(action_delay) # Shorter delay for attacks? perhaos
if not is_instance_valid(self): return true
_is_processing_action = false
_current_action = "idle"
return true
return false
func _find_nearest_victim() -> Node3D:
var best_dist = 9999.0
var best_victim = null
var players = get_tree().get_nodes_in_group("Players")
for p in players:
if p == actor or p.is_in_group("Spectators"):
continue
# Dist check
var dist = abs(p.current_position.x - actor.current_position.x) + abs(p.current_position.y - actor.current_position.y)
if dist < best_dist:
best_dist = dist
best_victim = p
return best_victim
# =============================================================================
# Grab Tiles
# =============================================================================
@@ -550,8 +627,8 @@ func _handle_goal_completion():
goals_cycle_manager.on_goal_completed(actor, time_remaining)
var powerup_manager = actor.get_node_or_null("PowerUpManager")
if powerup_manager:
powerup_manager.add_goal_completion_reward()
# if powerup_manager:
# powerup_manager.add_goal_completion_reward()
print("[BotController] %s COMPLETED GOAL!" % actor.name)
@@ -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: