feat: introduce core player logic with manager integration and power-up inventory UI.

This commit is contained in:
Yogi Wiguna
2026-03-04 15:56:03 +08:00
parent a78acd12b8
commit 8f03cc15c5
5 changed files with 123 additions and 174 deletions
+17 -10
View File
@@ -723,7 +723,6 @@ func apply_stagger(duration: float = 1.5):
if is_multiplayer_authority(): if is_multiplayer_authority():
NotificationManager.send_message(self , NotificationManager.MESSAGES.CRUSHED, NotificationManager.MessageType.WARNING) NotificationManager.send_message(self , NotificationManager.MESSAGES.CRUSHED, NotificationManager.MessageType.WARNING)
drop_random_item()
# Grant "Smashed" Bonus (1 bar, max 2) # Grant "Smashed" Bonus (1 bar, max 2)
if powerup_manager: if powerup_manager:
@@ -1140,15 +1139,16 @@ func handle_grid_click(grid_position: Vector2i):
# Modify is_position_occupied to check for selected spawn points # Modify is_position_occupied to check for selected spawn points
func is_position_occupied(pos: Vector2i) -> bool: func is_position_occupied(pos: Vector2i) -> bool:
for player in get_tree().get_nodes_in_group("Players"): for p in get_tree().get_nodes_in_group("Players"):
if player == self: if p == self:
continue continue
if player.spawn_point_selected and player.current_position == pos: # Check if player has selected a spawn point OR is already visible (active in match)
if (p.spawn_point_selected or p.visible) and p.current_position == pos:
return true return true
# Check target position (where they are moving to) # Check target position (where they are moving to)
if player.is_player_moving and player.target_position == pos: if p.is_player_moving and p.target_position == pos:
return true return true
return false return false
@@ -1336,12 +1336,16 @@ func simple_move_to(grid_position: Vector2i):
func move_player_to_clicked_position(grid_position: Vector2i): func move_player_to_clicked_position(grid_position: Vector2i):
movement_manager.move_to_clicked_position(grid_position) movement_manager.move_to_clicked_position(grid_position)
@rpc("any_peer", "call_remote", "unreliable") @rpc("any_peer", "call_remote", "reliable")
func start_movement_along_path(path: Array, clear_visual: bool = true): func start_movement_along_path(path: Array, clear_visual: bool = true, force: bool = false):
if is_player_moving: if is_player_moving and not force:
return # ALREADY MOVING. Guard against redundant RPCs or interruptions. return # ALREADY MOVING. Guard against redundant RPCs or interruptions unless forced.
print("[Player] %s starting move along path: %s" % [name, path]) if force and movement_manager:
movement_manager.movement_queue.clear()
movement_manager.current_move_direction = Vector2i.ZERO
print("[Player] %s starting move along path: %s (Forced: %s)" % [name, path, force])
# SERVER-SIDE VIOLATION CHECK (for Stop n Go) # SERVER-SIDE VIOLATION CHECK (for Stop n Go)
if multiplayer.is_server() and LobbyManager.game_mode == "Stop n Go": if multiplayer.is_server() and LobbyManager.game_mode == "Stop n Go":
@@ -1797,6 +1801,9 @@ func request_server_put(grid_position: Vector2i, slot_index: int, x: int, y: int
# Add new RPC function to notify others about spawn selection # Add new RPC function to notify others about spawn selection
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func notify_spawn_selected(spawn_pos: Vector2i): func notify_spawn_selected(spawn_pos: Vector2i):
# Mark as selected on all peers so occupancy checks work
spawn_point_selected = true
# Update local highlight state for all clients # Update local highlight state for all clients
if spawn_pos in highlighted_spawn_points: if spawn_pos in highlighted_spawn_points:
highlighted_spawn_points.erase(spawn_pos) highlighted_spawn_points.erase(spawn_pos)
+10 -18
View File
@@ -95,24 +95,16 @@ func handle_unhandled_input(event):
if event is InputEventKey and event.pressed and not event.echo: if event is InputEventKey and event.pressed and not event.echo:
match event.keycode: match event.keycode:
KEY_KP_1, KEY_1, KEY_KP_2, KEY_2, KEY_KP_3, KEY_3, KEY_KP_4, KEY_4: KEY_KP_1, KEY_1, KEY_KP_2, KEY_2, KEY_KP_3, KEY_3, KEY_KP_4, KEY_4:
var mode = LobbyManager.get_game_mode() # Unified Mapping for all modes: 1-Speed, 2-Wall, 3-Freeze, 4-Ghost
var is_restricted = GameMode.is_restricted(mode) match event.keycode:
if is_restricted: KEY_KP_1, KEY_1:
match event.keycode: player.activate_powerup(0) # FASTER_SPEED
KEY_KP_1, KEY_1: KEY_KP_2, KEY_2:
player.activate_powerup(0) # FASTER_SPEED player.activate_powerup(2) # BLOCK_FLOOR
KEY_KP_2, KEY_2: KEY_KP_3, KEY_3:
player.activate_powerup(3) # INVISIBLE_MODE (Ghost is now 2) player.activate_powerup(1) # AREA_FREEZE
else: KEY_KP_4, KEY_4:
match event.keycode: player.activate_powerup(3) # INVISIBLE_MODE
KEY_KP_1, KEY_1:
player.activate_powerup(0) # FASTER_SPEED
KEY_KP_2, KEY_2:
player.activate_powerup(2) # BLOCK_FLOOR
KEY_KP_3, KEY_3:
player.activate_powerup(1) # AREA_FREEZE
KEY_KP_4, KEY_4:
player.activate_powerup(3) # INVISIBLE_MODE
# KEY_R: # KEY_R:
# player.auto_put_item() # player.auto_put_item()
KEY_Q: KEY_Q:
+32 -48
View File
@@ -24,6 +24,7 @@ func initialize(p_player: Node3D, p_gridmap: Node):
signal movement_finished signal movement_finished
var movement_queue: Array[Vector2i] = [] # Queue of target grid positions var movement_queue: Array[Vector2i] = [] # Queue of target grid positions
var current_move_direction: Vector2i = Vector2i.ZERO var current_move_direction: Vector2i = Vector2i.ZERO
var last_move_direction: Vector2i = Vector2i(0, 1) # Default forward (towards +Z)
func _process(delta): func _process(delta):
if player: if player:
@@ -137,6 +138,8 @@ func simple_move_to(grid_position: Vector2i) -> bool:
path.pop_front() path.pop_front()
current_move_direction = grid_position - player.current_position current_move_direction = grid_position - player.current_position
if current_move_direction != Vector2i.ZERO:
last_move_direction = current_move_direction
if player.is_multiplayer_authority(): if player.is_multiplayer_authority():
# Authority starts their own tween locally # Authority starts their own tween locally
@@ -174,54 +177,39 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
NotificationManager.send_message(player, "Cannot Attack in Safe Zone!", NotificationManager.MessageType.WARNING) NotificationManager.send_message(player, "Cannot Attack in Safe Zone!", NotificationManager.MessageType.WARNING)
return false return false
# 1. Drop Victim's Tiles # 1. 3-Floor Knockback towards Starting Line (X=0)
if other_player.has_method("drop_all_tiles"): var push_direction = Vector2i(-1, 0) # Backwards
if _can_rpc(): var pushed_to_pos = target_pos
other_player.rpc("drop_all_tiles") # Sync drop var push_path = []
# 2. Spawn PowerUps around Victim # Try to push up to 3 tiles back, building the path as we go
# We delegate this to the attacker's SpecialTilesManager to handle the spawning authority for i in range(3):
if player.special_tiles_manager and player.special_tiles_manager.has_method("spawn_powerups_around"): var next_back = pushed_to_pos + push_direction
player.special_tiles_manager.spawn_powerups_around(other_player.current_position) if _can_push_to(next_back):
pushed_to_pos = next_back
# 3. Knockback / Stagger push_path.append(Vector2(pushed_to_pos.x, pushed_to_pos.y))
# Push them away else:
var pushed_to_pos = target_pos + direction break # Blocked by wall or edge
# IMPROVED: Check if destination is valid and walkable to prevent being stuck on 'blocks'
var is_dest_valid = _can_push_to(pushed_to_pos)
# DEFLECTION LOGIC: If direct path is blocked, try diagonal deflection
if not is_dest_valid:
var alts = []
if direction.x != 0 and direction.y == 0: # Horizontal push -> try diagonal North/South
alts = [pushed_to_pos + Vector2i(0, 1), pushed_to_pos + Vector2i(0, -1)]
elif direction.y != 0 and direction.x == 0: # Vertical push -> try diagonal East/West
alts = [pushed_to_pos + Vector2i(1, 0), pushed_to_pos + Vector2i(-1, 0)]
elif direction.x != 0 and direction.y != 0: # Diagonal push -> try horizontal/vertical components
alts = [pushed_to_pos - Vector2i(direction.x, 0), pushed_to_pos - Vector2i(0, direction.y)]
for alt in alts: if push_path.size() > 0:
if _can_push_to(alt): # Valid push movement
pushed_to_pos = alt
is_dest_valid = true
break
if is_dest_valid:
# Valid push
var push_path = [Vector2(pushed_to_pos.x, pushed_to_pos.y)]
if _can_rpc(): if _can_rpc():
other_player.rpc("start_movement_along_path", push_path, false) # Pass 'true' for 'force' parameter to interrupt active movements
other_player.rpc("start_movement_along_path", push_path, false, true)
# Authority Check: If we are already the authority for the victim (e.g. Host hitting a Bot),
# the 'call_remote' RPC above won't execute locally. We MUST call it manually.
if other_player.is_multiplayer_authority():
other_player.start_movement_along_path(push_path, false, true)
other_player.target_position = pushed_to_pos # Logical update other_player.target_position = pushed_to_pos # Logical update
# Apply stun/freeze effect as requested (same as wall stagger) # 2. Apply freeze/stun effect (blue tint)
if _can_rpc(): if _can_rpc():
other_player.rpc("apply_stagger", 1.5) other_player.rpc("apply_stagger", 1.5)
else: else:
# Wall/Blocked -> Stagger in place (Only if no alternatives found) # Handle local execution (e.g. offline or host-only logic)
if _can_rpc(): other_player.apply_stagger(1.5)
other_player.rpc("apply_stagger", 1.5)
# 4. Consume Boost (Full) - One hit per charge # 4. Consume Boost (Full) - One hit per charge
if player.powerup_manager: if player.powerup_manager:
@@ -246,12 +234,8 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
else: else:
NotificationManager.send_message(player, "Successful Attack!", NotificationManager.MessageType.GOAL) NotificationManager.send_message(player, "Successful Attack!", NotificationManager.MessageType.GOAL)
# 5. Attack Mode Persistence # 5. Block the attacker from moving into the victim's space to prevent overlapping
# logic moved to consume_boost: checks if <= 0 then disables. return false
# So we do NOT force disable here.
# player.is_attack_mode = false
return true
func set_speed_multiplier(multiplier: float): func set_speed_multiplier(multiplier: float):
speed_multiplier = multiplier speed_multiplier = multiplier
+55 -80
View File
@@ -26,7 +26,6 @@ const FREEZE_SLOW_DURATION = 3.0
signal cooldown_updated(effect: int, time_left: float, max_time: float) signal cooldown_updated(effect: int, time_left: float, max_time: float)
signal powerup_unlocked(effect: int, level: int) signal powerup_unlocked(effect: int, level: int)
var wall_orientation_horizontal: bool = false # False = Vertical, True = Horizontal
# New Helper functions for Targeting and Preview # New Helper functions for Targeting and Preview
@@ -35,28 +34,12 @@ func get_skill_affected_area(effect: int, center_pos: Vector2i) -> Array[Vector2
match effect: match effect:
SpecialEffect.AREA_FREEZE: SpecialEffect.AREA_FREEZE:
var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1) # Preview 3 blocks ahead of current hover (if we ever re-enable targeting)
var radius = 1 area.append(center_pos)
if current_lvl >= 5:
radius = 2
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
var pos = center_pos + Vector2i(x, y)
# Validate bounds
if enhanced_gridmap.is_position_valid(pos):
area.append(pos)
SpecialEffect.BLOCK_FLOOR: SpecialEffect.BLOCK_FLOOR:
# Logic: Based on toggled orientation state # Preview just the single block
var is_horizontal = wall_orientation_horizontal area.append(center_pos)
if is_horizontal:
for x in range(enhanced_gridmap.columns):
area.append(Vector2i(x, center_pos.y))
else:
for z in range(enhanced_gridmap.rows):
area.append(Vector2i(center_pos.x, z))
return area return area
@@ -143,12 +126,8 @@ func get_effect_from_item(item_id: int) -> int:
match item_id: match item_id:
11: return SpecialEffect.FASTER_SPEED 11: return SpecialEffect.FASTER_SPEED
12: 12: return SpecialEffect.AREA_FREEZE
if is_restricted: return -1 13: return SpecialEffect.BLOCK_FLOOR
return SpecialEffect.AREA_FREEZE
13:
if is_restricted: return -1
return SpecialEffect.BLOCK_FLOOR
14: return SpecialEffect.INVISIBLE_MODE 14: return SpecialEffect.INVISIBLE_MODE
_: return -1 _: return -1
@@ -231,32 +210,11 @@ func activate_effect(effect: int, target_player: Node3D = null):
SpecialEffect.FASTER_SPEED: SpecialEffect.FASTER_SPEED:
_execute_faster_speed() _execute_faster_speed()
SpecialEffect.AREA_FREEZE, SpecialEffect.BLOCK_FLOOR: SpecialEffect.AREA_FREEZE, SpecialEffect.BLOCK_FLOOR:
# Enter Targeting Mode instead of executing immediately (ONLY for human players) # Execute immediately based on direction instead of entering Targeting Mode
if not (player.is_bot or player.is_in_group("Bots")): if effect == SpecialEffect.BLOCK_FLOOR:
var main = player.get_tree().get_root().get_node_or_null("Main") _execute_block_floor()
if main and main.ui_manager: else:
# Toggle Logic for Wall Block _execute_area_freeze()
if main.ui_manager.current_action_state == main.ui_manager.ActionState.TARGETING and main.ui_manager.pending_skill_id == effect:
if effect == SpecialEffect.BLOCK_FLOOR:
toggle_wall_orientation()
powerup_cooldowns[effect] = 0.0 # Revert cooldown
emit_signal("cooldown_updated", effect, 0.0, 0.0)
return
main.ui_manager.current_action_state = main.ui_manager.ActionState.TARGETING
main.ui_manager.pending_skill_id = effect
var msg = "Select a target area..."
if effect == SpecialEffect.BLOCK_FLOOR:
msg = "Click again to toggle Vertical/Horizontal"
NotificationManager.send_message(player, msg, NotificationManager.MessageType.NORMAL)
# Do NOT set cooldown yet. Cooldown sets on execution.
# Revert the cooldown set above (hacky but handles the split flow)
powerup_cooldowns[effect] = 0.0
emit_signal("cooldown_updated", effect, 0.0, 0.0)
print("[SpecialTiles] Entered Targeting Mode for %s" % SpecialEffect.keys()[effect])
return # Exit, wait for input
SpecialEffect.INVISIBLE_MODE: SpecialEffect.INVISIBLE_MODE:
_execute_invisible_mode(player) _execute_invisible_mode(player)
@@ -284,13 +242,33 @@ func _execute_faster_speed():
active_buffs[SpecialEffect.FASTER_SPEED] = FASTER_DURATION active_buffs[SpecialEffect.FASTER_SPEED] = FASTER_DURATION
NotificationManager.send_message(player, "Speed Boost! (5s)", NotificationManager.MessageType.POWERUP) NotificationManager.send_message(player, "Speed Boost! (5s)", NotificationManager.MessageType.POWERUP)
func _execute_area_freeze(center_pos: Vector2i = Vector2i.ZERO): func _execute_area_freeze(target_pos: Vector2i = Vector2i.ZERO):
var center_pos = target_pos
# Determine Level early for distance calculation
var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1)
if center_pos == Vector2i.ZERO: if center_pos == Vector2i.ZERO:
# Fallback to old behavior if no target provided (or error) # Calculate distance ahead based on Level
return # Gap of 3 floors = 4 tiles ahead
# Gap of 5 floors = 6 tiles ahead
var distance = 4 if current_lvl < 5 else 6
var movement = player.movement_manager
if movement and movement.current_move_direction != Vector2i.ZERO:
center_pos = player.current_position + movement.current_move_direction * distance
else:
# Fallback if standing still
var last_dir = player.movement_manager.last_move_direction if movement else Vector2i(0, 1)
center_pos = player.current_position + last_dir * distance
if not enhanced_gridmap.is_position_valid(center_pos):
# Try a bit closer if out of bounds
center_pos = player.current_position + (center_pos - player.current_position).normalized() * 1.0
if not enhanced_gridmap.is_position_valid(center_pos):
return
# 3. Determine Radius based on Level # 3. Determine Radius based on Level
var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1)
var radius = 1 var radius = 1
if current_lvl >= 5: if current_lvl >= 5:
radius = 2 # Bigger area at high levels radius = 2 # Bigger area at high levels
@@ -366,29 +344,29 @@ func _execute_area_freeze(center_pos: Vector2i = Vector2i.ZERO):
main.rpc("sync_grid_items_batch", restore_batch) main.rpc("sync_grid_items_batch", restore_batch)
) )
func toggle_wall_orientation():
wall_orientation_horizontal = !wall_orientation_horizontal
var mode_str = "HORIZONTAL" if wall_orientation_horizontal else "VERTICAL"
NotificationManager.send_message(player, "Wall Mode: " + mode_str, NotificationManager.MessageType.NORMAL)
func _execute_block_floor(target_pos: Vector2i): func _execute_block_floor(target_pos: Vector2i = Vector2i.ZERO):
# "Wall Block" # "Wall Block" - Spawn line behind player
var is_horizontal = wall_orientation_horizontal var behind_pos = target_pos
var last_dir = player.movement_manager.last_move_direction if player.movement_manager else Vector2i(0, 1)
if behind_pos == Vector2i.ZERO:
behind_pos = player.current_position - last_dir
if not enhanced_gridmap.is_position_valid(behind_pos):
return
print("Player %s activated Wall Block behind at %s" % [player.name, behind_pos])
var neighbors = [] var neighbors = []
if last_dir.x != 0:
if is_horizontal: # Moving on X-axis (Columns) -> Vertical Wall (Fixed X, all Z)
# Block entire Row (Fixed Z, iterate all X)
var row_z = target_pos.y
for x in range(enhanced_gridmap.columns):
neighbors.append({"position": Vector2i(x, row_z)})
print("Player %s activated Wall Block: HORIZONTAL ROW (Z=%d)" % [player.name, row_z])
else:
# Block entire Column (Fixed X, iterate all Z)
var col_x = target_pos.x
for z in range(enhanced_gridmap.rows): for z in range(enhanced_gridmap.rows):
neighbors.append({"position": Vector2i(col_x, z)}) neighbors.append({"position": Vector2i(behind_pos.x, z)})
print("Player %s activated Wall Block: VERTICAL COLUMN (X=%d)" % [player.name, col_x]) else:
# Moving on Z-axis (Rows) -> Horizontal Wall (Fixed Z, all X)
for x in range(enhanced_gridmap.columns):
neighbors.append({"position": Vector2i(x, behind_pos.y)})
if player.is_multiplayer_authority(): if player.is_multiplayer_authority():
var main = player.get_tree().get_root().get_node_or_null("Main") var main = player.get_tree().get_root().get_node_or_null("Main")
@@ -420,10 +398,7 @@ func _execute_block_floor(target_pos: Vector2i):
main.rpc("sync_grid_items_batch", batch_data) main.rpc("sync_grid_items_batch", batch_data)
# Notify # Notify
var all_players = player.get_tree().get_nodes_in_group("Players") NotificationManager.send_message(player, "Defensive Wall Deployed!", NotificationManager.MessageType.POWERUP)
for p in all_players:
if p.current_position == target_pos:
NotificationManager.send_message(p, "Wall Block Created!", NotificationManager.MessageType.POWERUP)
func _execute_invisible_mode(target: Node3D): func _execute_invisible_mode(target: Node3D):
target.is_invisible = true target.is_invisible = true
+9 -18
View File
@@ -40,9 +40,9 @@ func _ready():
_setup_btn(2, wall_btn) _setup_btn(2, wall_btn)
_setup_btn(3, ghost_btn) _setup_btn(3, ghost_btn)
if is_restricted: # All skills available with new logic
if wall_btn: wall_btn.visible = false if wall_btn: wall_btn.visible = true
if freeze_btn: freeze_btn.visible = false if freeze_btn: freeze_btn.visible = true
print("[PowerUpUI] UI Initialization Complete. Mapped %d buttons." % icon_containers.size()) print("[PowerUpUI] UI Initialization Complete. Mapped %d buttons." % icon_containers.size())
@@ -111,21 +111,12 @@ func _setup_btn(effect_id: int, btn: Button):
# Determine Label Text based on Effect ID # Determine Label Text based on Effect ID
var key_text = "" var key_text = ""
var mode = LobbyManager.get_game_mode() # Consistent mapping: 1, 2, 3, 4
var is_restricted = GameMode.is_restricted(mode) match effect_id:
if is_restricted: 0: key_text = "1" # Speed
# Restricted Mapping: 1, 2 2: key_text = "2" # Wall
match effect_id: 1: key_text = "3" # Freeze
0: key_text = "1" 3: key_text = "4" # Ghost
3: key_text = "2" # Ghost is now 2
_: key_text = ""
else:
# Free Mode Mapping: 1, 2, 3, 4 (Original)
match effect_id:
0: key_text = "1"
2: key_text = "2"
1: key_text = "3"
3: key_text = "4"
sc_lbl.text = key_text sc_lbl.text = key_text
btn.add_child(sc_lbl) btn.add_child(sc_lbl)