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():
NotificationManager.send_message(self , NotificationManager.MESSAGES.CRUSHED, NotificationManager.MessageType.WARNING)
drop_random_item()
# Grant "Smashed" Bonus (1 bar, max 2)
if powerup_manager:
@@ -1140,15 +1139,16 @@ func handle_grid_click(grid_position: Vector2i):
# Modify is_position_occupied to check for selected spawn points
func is_position_occupied(pos: Vector2i) -> bool:
for player in get_tree().get_nodes_in_group("Players"):
if player == self:
for p in get_tree().get_nodes_in_group("Players"):
if p == self:
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
# 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 false
@@ -1336,12 +1336,16 @@ func simple_move_to(grid_position: Vector2i):
func move_player_to_clicked_position(grid_position: Vector2i):
movement_manager.move_to_clicked_position(grid_position)
@rpc("any_peer", "call_remote", "unreliable")
func start_movement_along_path(path: Array, clear_visual: bool = true):
if is_player_moving:
return # ALREADY MOVING. Guard against redundant RPCs or interruptions.
@rpc("any_peer", "call_remote", "reliable")
func start_movement_along_path(path: Array, clear_visual: bool = true, force: bool = false):
if is_player_moving and not force:
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)
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
@rpc("any_peer", "reliable")
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
if spawn_pos in highlighted_spawn_points:
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:
match event.keycode:
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()
var is_restricted = GameMode.is_restricted(mode)
if is_restricted:
match event.keycode:
KEY_KP_1, KEY_1:
player.activate_powerup(0) # FASTER_SPEED
KEY_KP_2, KEY_2:
player.activate_powerup(3) # INVISIBLE_MODE (Ghost is now 2)
else:
match event.keycode:
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
# Unified Mapping for all modes: 1-Speed, 2-Wall, 3-Freeze, 4-Ghost
match event.keycode:
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:
# player.auto_put_item()
KEY_Q:
+32 -48
View File
@@ -24,6 +24,7 @@ func initialize(p_player: Node3D, p_gridmap: Node):
signal movement_finished
var movement_queue: Array[Vector2i] = [] # Queue of target grid positions
var current_move_direction: Vector2i = Vector2i.ZERO
var last_move_direction: Vector2i = Vector2i(0, 1) # Default forward (towards +Z)
func _process(delta):
if player:
@@ -137,6 +138,8 @@ func simple_move_to(grid_position: Vector2i) -> bool:
path.pop_front()
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():
# 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)
return false
# 1. Drop Victim's Tiles
if other_player.has_method("drop_all_tiles"):
if _can_rpc():
other_player.rpc("drop_all_tiles") # Sync drop
# 1. 3-Floor Knockback towards Starting Line (X=0)
var push_direction = Vector2i(-1, 0) # Backwards
var pushed_to_pos = target_pos
var push_path = []
# 2. Spawn PowerUps around Victim
# We delegate this to the attacker's SpecialTilesManager to handle the spawning authority
if player.special_tiles_manager and player.special_tiles_manager.has_method("spawn_powerups_around"):
player.special_tiles_manager.spawn_powerups_around(other_player.current_position)
# 3. Knockback / Stagger
# Push them away
var pushed_to_pos = target_pos + direction
# 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)]
# Try to push up to 3 tiles back, building the path as we go
for i in range(3):
var next_back = pushed_to_pos + push_direction
if _can_push_to(next_back):
pushed_to_pos = next_back
push_path.append(Vector2(pushed_to_pos.x, pushed_to_pos.y))
else:
break # Blocked by wall or edge
for alt in alts:
if _can_push_to(alt):
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 push_path.size() > 0:
# Valid push movement
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
# Apply stun/freeze effect as requested (same as wall stagger)
if _can_rpc():
other_player.rpc("apply_stagger", 1.5)
# 2. Apply freeze/stun effect (blue tint)
if _can_rpc():
other_player.rpc("apply_stagger", 1.5)
else:
# Wall/Blocked -> Stagger in place (Only if no alternatives found)
if _can_rpc():
other_player.rpc("apply_stagger", 1.5)
# Handle local execution (e.g. offline or host-only logic)
other_player.apply_stagger(1.5)
# 4. Consume Boost (Full) - One hit per charge
if player.powerup_manager:
@@ -246,12 +234,8 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
else:
NotificationManager.send_message(player, "Successful Attack!", NotificationManager.MessageType.GOAL)
# 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
# 5. Block the attacker from moving into the victim's space to prevent overlapping
return false
func set_speed_multiplier(multiplier: float):
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 powerup_unlocked(effect: int, level: int)
var wall_orientation_horizontal: bool = false # False = Vertical, True = Horizontal
# 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:
SpecialEffect.AREA_FREEZE:
var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1)
var radius = 1
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)
# Preview 3 blocks ahead of current hover (if we ever re-enable targeting)
area.append(center_pos)
SpecialEffect.BLOCK_FLOOR:
# Logic: Based on toggled orientation state
var is_horizontal = wall_orientation_horizontal
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))
# Preview just the single block
area.append(center_pos)
return area
@@ -143,12 +126,8 @@ func get_effect_from_item(item_id: int) -> int:
match item_id:
11: return SpecialEffect.FASTER_SPEED
12:
if is_restricted: return -1
return SpecialEffect.AREA_FREEZE
13:
if is_restricted: return -1
return SpecialEffect.BLOCK_FLOOR
12: return SpecialEffect.AREA_FREEZE
13: return SpecialEffect.BLOCK_FLOOR
14: return SpecialEffect.INVISIBLE_MODE
_: return -1
@@ -231,32 +210,11 @@ func activate_effect(effect: int, target_player: Node3D = null):
SpecialEffect.FASTER_SPEED:
_execute_faster_speed()
SpecialEffect.AREA_FREEZE, SpecialEffect.BLOCK_FLOOR:
# Enter Targeting Mode instead of executing immediately (ONLY for human players)
if not (player.is_bot or player.is_in_group("Bots")):
var main = player.get_tree().get_root().get_node_or_null("Main")
if main and main.ui_manager:
# Toggle Logic for Wall Block
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
# Execute immediately based on direction instead of entering Targeting Mode
if effect == SpecialEffect.BLOCK_FLOOR:
_execute_block_floor()
else:
_execute_area_freeze()
SpecialEffect.INVISIBLE_MODE:
_execute_invisible_mode(player)
@@ -284,13 +242,33 @@ func _execute_faster_speed():
active_buffs[SpecialEffect.FASTER_SPEED] = FASTER_DURATION
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:
# Fallback to old behavior if no target provided (or error)
return
# Calculate distance ahead based on Level
# 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
var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1)
var radius = 1
if current_lvl >= 5:
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)
)
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):
# "Wall Block"
var is_horizontal = wall_orientation_horizontal
func _execute_block_floor(target_pos: Vector2i = Vector2i.ZERO):
# "Wall Block" - Spawn line behind player
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 = []
if is_horizontal:
# 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
if last_dir.x != 0:
# Moving on X-axis (Columns) -> Vertical Wall (Fixed X, all Z)
for z in range(enhanced_gridmap.rows):
neighbors.append({"position": Vector2i(col_x, z)})
print("Player %s activated Wall Block: VERTICAL COLUMN (X=%d)" % [player.name, col_x])
neighbors.append({"position": Vector2i(behind_pos.x, z)})
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():
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)
# Notify
var all_players = player.get_tree().get_nodes_in_group("Players")
for p in all_players:
if p.current_position == target_pos:
NotificationManager.send_message(p, "Wall Block Created!", NotificationManager.MessageType.POWERUP)
NotificationManager.send_message(player, "Defensive Wall Deployed!", NotificationManager.MessageType.POWERUP)
func _execute_invisible_mode(target: Node3D):
target.is_invisible = true
+9 -18
View File
@@ -40,9 +40,9 @@ func _ready():
_setup_btn(2, wall_btn)
_setup_btn(3, ghost_btn)
if is_restricted:
if wall_btn: wall_btn.visible = false
if freeze_btn: freeze_btn.visible = false
# All skills available with new logic
if wall_btn: wall_btn.visible = true
if freeze_btn: freeze_btn.visible = true
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
var key_text = ""
var mode = LobbyManager.get_game_mode()
var is_restricted = GameMode.is_restricted(mode)
if is_restricted:
# Restricted Mapping: 1, 2
match effect_id:
0: key_text = "1"
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"
# Consistent mapping: 1, 2, 3, 4
match effect_id:
0: key_text = "1" # Speed
2: key_text = "2" # Wall
1: key_text = "3" # Freeze
3: key_text = "4" # Ghost
sc_lbl.text = key_text
btn.add_child(sc_lbl)