feat: Add core player input, movement, and special ability management systems with new UI components.

This commit is contained in:
Yogi Wiguna
2026-03-11 17:40:45 +08:00
parent 18c3dbecd6
commit 650d241a72
6 changed files with 104 additions and 90 deletions
+3 -3
View File
@@ -1640,8 +1640,8 @@ func sync_grid_item(x: int, y: int, z: int, item: int):
var f1 = enhanced_gridmap.get_cell_item(Vector3i(x, 1, z)) var f1 = enhanced_gridmap.get_cell_item(Vector3i(x, 1, z))
# Block if Layer 0 is Wall (4) or Void (-1) # Block if Layer 0 is Wall (4) or Void (-1)
# OR Layer 1 is already a wall (4 or 13) # OR Layer 1 is already a wall (4)
if f0 in [4, -1] or f1 == 4 or f1 == 13: if f0 in [4, -1] or f1 == 4:
return return
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item) enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
@@ -1666,7 +1666,7 @@ func sync_grid_items_batch(data: Array):
if y == 1 and item >= 7 and item <= 20: if y == 1 and item >= 7 and item <= 20:
var f0 = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z)) var f0 = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
var f1 = enhanced_gridmap.get_cell_item(Vector3i(x, 1, z)) var f1 = enhanced_gridmap.get_cell_item(Vector3i(x, 1, z))
if f0 in [4, -1] or f1 == 4 or f1 == 13: if f0 in [4, -1] or f1 == 4:
continue continue
enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item) enhanced_gridmap.set_cell_item(Vector3i(x, y, z), item)
+21 -14
View File
@@ -114,22 +114,29 @@ func handle_unhandled_input(event):
# Unified check for PowerUp keys # Unified check for PowerUp keys
if ek == key_p1: if ek == key_p1:
player.activate_powerup(0) # FASTER_SPEED # Single Slot Logic: Find whatever powerup is currently set to 'true' and activate it
elif ek == key_p2: var active_effect = -1
if is_sng: if player.special_tiles_manager:
player.activate_powerup(3) # Ghost for SNG P2 for effect_key in player.special_tiles_manager.inventory:
else: if player.special_tiles_manager.inventory[effect_key]:
player.activate_powerup(2) # Wall for Normal P2 active_effect = effect_key
elif ek == key_p3 and not is_sng: break
player.activate_powerup(1) # AREA_FREEZE
elif ek == key_p4 and not is_sng:
player.activate_powerup(3) # INVISIBLE_MODE
# Non-Remappable variants (Numpad) - Optional fallback if active_effect != -1:
player.activate_powerup(active_effect)
else:
print("No powerup in slot 1 to activate.")
# KP Fallback
elif ek == KEY_KP_1: elif ek == KEY_KP_1:
player.activate_powerup(0) var active_effect = -1
elif ek == KEY_KP_2: if player.special_tiles_manager:
player.activate_powerup(3 if is_sng else 2) for effect_key in player.special_tiles_manager.inventory:
if player.special_tiles_manager.inventory[effect_key]:
active_effect = effect_key
break
if active_effect != -1:
player.activate_powerup(active_effect)
# Action Buttons (Remappable) # Action Buttons (Remappable)
elif ek == SettingsManager.get_control_keycode("attack_mode"): elif ek == SettingsManager.get_control_keycode("attack_mode"):
+1 -1
View File
@@ -96,7 +96,7 @@ func simple_move_to(grid_position: Vector2i) -> bool:
return false return false
# Check Floor 1 (Obstacles/Walls) # Check Floor 1 (Obstacles/Walls)
if (cell_item != -1 and cell_item in [4, 13, 16]) and not is_wall_passable: if (cell_item != -1 and cell_item in [4, 16]) and not is_wall_passable:
print("[Move] Failed: Blocked by Item %d on Floor 1" % cell_item) print("[Move] Failed: Blocked by Item %d on Floor 1" % cell_item)
return false return false
+52 -32
View File
@@ -119,6 +119,12 @@ func initialize(p_player: Node3D, p_gridmap: Node):
rng = RandomNumberGenerator.new() rng = RandomNumberGenerator.new()
rng.randomize() rng.randomize()
# Ensure Powerup Tiles (11-14) are NOT in the non-walkable list
if enhanced_gridmap and "non_walkable_items" in enhanced_gridmap:
for id in HOLO_TILES:
if id in enhanced_gridmap.non_walkable_items:
enhanced_gridmap.non_walkable_items.erase(id)
# ============================================================================= # =============================================================================
# Helper: Item ID to Effect Enum # Helper: Item ID to Effect Enum
# ============================================================================= # =============================================================================
@@ -137,45 +143,58 @@ func add_powerup_from_item(item_id: int):
var effect = get_effect_from_item(item_id) var effect = get_effect_from_item(item_id)
if effect == -1: return if effect == -1: return
# Unlock or Level Up # 1-PowerUp Rule: If this is a DIFFERENT power-up, clear the old one
if not inventory.get(effect, false): var is_different = not inventory.get(effect, false)
# New Unlock var already_has_any = false
inventory[effect] = true for e in inventory:
powerup_levels[effect] = 1 if inventory[e]:
emit_signal("inventory_updated", inventory) already_has_any = true
emit_signal("powerup_unlocked", effect, 1) break
print("Player %s unlocked powerup %s (Lvl 1)" % [player.name, SpecialEffect.keys()[effect]])
else: if is_different and already_has_any:
# Level Up print("Player %s replacing existing powerup with %s" % [player.name, SpecialEffect.keys()[effect]])
var current_lvl = powerup_levels.get(effect, 1) for e in inventory:
if current_lvl < 8: inventory[e] = false
powerup_levels[effect] = current_lvl + 1 powerup_levels[e] = 1 # Reset levels of discarded powerups
emit_signal("powerup_unlocked", effect, powerup_levels[effect])
print("Player %s leveled up %s to Lvl %d" % [player.name, SpecialEffect.keys()[effect], powerup_levels[effect]]) # Instant Level 8 on pickup (User Request)
inventory[effect] = true
powerup_levels[effect] = 8
emit_signal("inventory_updated", inventory)
emit_signal("powerup_unlocked", effect, 8)
print("[SpecialTiles] SUCCESS: Player %s picked up %s. Force Level 8 applied." % [player.name, SpecialEffect.keys()[effect]])
if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
rpc("sync_inventory_add", effect, powerup_levels[effect]) rpc("sync_inventory_add", effect, 8)
@rpc("any_peer", "call_local", "reliable") @rpc("any_peer", "call_local", "reliable")
func sync_inventory_add(effect: int, level: int): func sync_inventory_add(effect: int, level: int):
# Clear others on sync too to maintain 1-powerup rule
for e in inventory:
inventory[e] = false
powerup_levels[e] = 1 # Reset levels of discarded powerups
inventory[effect] = true inventory[effect] = true
powerup_levels[effect] = level powerup_levels[effect] = level
emit_signal("inventory_updated", inventory) emit_signal("inventory_updated", inventory)
emit_signal("powerup_unlocked", effect, level) emit_signal("powerup_unlocked", effect, level)
func remove_powerup(effect: int): func remove_powerup(effect: int):
# We DO NOT remove item from inventory on use anymore (for reusable leveling system)? # User Request: "If the power up already use it will remain empty"
# Wait, user request: "cooldown is decarese and max is level 8" if inventory.get(effect, false):
# Does "activate it" consume the item? inventory[effect] = false
# User says "it will cooldown agian for 15 seconds". powerup_levels[effect] = 1
# Implies PERMANENT UNLOCK once picked up, reusable with cooldown. emit_signal("inventory_updated", inventory)
# So we NEVER set inventory[effect] = false unless reset. print("[SpecialTiles] Player %s consumed powerup %s" % [player.name, SpecialEffect.keys()[effect]])
pass
if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
rpc("sync_inventory_remove", effect)
@rpc("any_peer", "call_local", "reliable") @rpc("any_peer", "call_local", "reliable")
func sync_inventory_remove(effect: int): func sync_inventory_remove(effect: int):
# Deprecated for new system, but kept for compatibility if needed inventory[effect] = false
pass powerup_levels[effect] = 1
emit_signal("inventory_updated", inventory)
# ============================================================================= # =============================================================================
# Activate Effect (Explicit Target) # Activate Effect (Explicit Target)
@@ -228,8 +247,10 @@ func activate_effect(effect: int, target_player: Node3D = null):
# Play generic cast animation or sound? # Play generic cast animation or sound?
if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
player.rpc("trigger_screen_shake", "light") player.rpc("trigger_screen_shake", "light")
# Sync cooldown to others not strictly needed unless UI shows it?
# Probably local UI only. # Consumable: Remove after use (User Request)
print("[SpecialTiles] Consuming %s after successful activation." % SpecialEffect.keys()[effect])
remove_powerup(effect)
# ============================================================================= # =============================================================================
@@ -256,10 +277,9 @@ func _execute_area_freeze(target_pos: Vector2i = Vector2i.ZERO):
var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1) var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1)
if center_pos == Vector2i.ZERO: if center_pos == Vector2i.ZERO:
# Calculate distance ahead based on Level # Updated: Always 3 tiles ahead as per user request
# Gap of 2 floors = 3 tiles ahead var distance = 3
# Gap of 4 floors = 5 tiles ahead print("[SpecialTiles] Area Freeze logic executing with distance: %d" % distance)
var distance = 3 if current_lvl < 5 else 5
var movement = player.movement_manager var movement = player.movement_manager
if movement and movement.current_move_direction != Vector2i.ZERO: if movement and movement.current_move_direction != Vector2i.ZERO:
@@ -441,7 +461,7 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true, only_c
# PROTECTED FLOOR CHECK: Don't spawn on existing walls or void # PROTECTED FLOOR CHECK: Don't spawn on existing walls or void
var f0 = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y)) var f0 = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
var f1 = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 1, pos.y)) var f1 = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 1, pos.y))
if f0 in [4, -1] or f1 in [4, 13, 16]: if f0 in [4, -1] or f1 in [4, 16]:
continue continue
var item_id: int var item_id: int
+2 -2
View File
@@ -20,7 +20,7 @@ var safe_zone_centers: Array[Vector2i] = []
var safe_zone_spawned: bool = false var safe_zone_spawned: bool = false
# Power-Up Tile Spawning # Power-Up Tile Spawning
const POWERUP_TILES = [11, 14] # Speed (11) and Ghost (14) const POWERUP_TILES = [11, 12, 13, 14] # Speed, Freeze, Wall, Ghost
const POWERUP_SPAWN_COUNT: int = 5 # Number of power-up tiles to spawn const POWERUP_SPAWN_COUNT: int = 5 # Number of power-up tiles to spawn
var powerups_spawned: bool = false var powerups_spawned: bool = false
var stop_phase_occurred: bool = false var stop_phase_occurred: bool = false
@@ -362,7 +362,7 @@ func _spawn_mission_tiles():
var current_item = gridmap.get_cell_item(Vector3i(x, 1, z)) var current_item = gridmap.get_cell_item(Vector3i(x, 1, z))
# PROTECTED FLOOR CHECK: Don't spawn on walls or void # PROTECTED FLOOR CHECK: Don't spawn on walls or void
if base_tile in [TILE_OBSTACLE, -1] or current_item == TILE_OBSTACLE or current_item == 13: if base_tile in [TILE_OBSTACLE, -1] or current_item == TILE_OBSTACLE:
continue continue
# Spawn tiles with 60% density # Spawn tiles with 60% density
+24 -37
View File
@@ -50,16 +50,13 @@ func _ready():
_setup_btn(2, wall_btn) _setup_btn(2, wall_btn)
_setup_btn(3, ghost_btn) _setup_btn(3, ghost_btn)
# All skills available with new logic, but restricted in Stop n Go # Remove mode-based restrictions on visibility for now, as ownership controls visibility
if is_restricted: if wall_btn: wall_btn.visible = false
if wall_btn: wall_btn.visible = false if freeze_btn: freeze_btn.visible = false
if freeze_btn: freeze_btn.visible = false if speed_btn: speed_btn.visible = false
# Re-setup shortcut labels for restricted mode if ghost_btn: ghost_btn.visible = false
_update_shortcuts_for_mode(true)
else: _update_shortcuts_for_mode(is_restricted)
if wall_btn: wall_btn.visible = true
if freeze_btn: freeze_btn.visible = true
_update_shortcuts_for_mode(false)
print("[PowerUpUI] UI Initialization Complete. Mapped %d buttons." % icon_containers.size()) print("[PowerUpUI] UI Initialization Complete. Mapped %d buttons." % icon_containers.size())
@@ -140,33 +137,16 @@ func _update_shortcuts_for_mode(is_restricted: bool):
var btn = icon_containers[effect_id] var btn = icon_containers[effect_id]
_update_btn_shortcut(effect_id, btn) _update_btn_shortcut(effect_id, btn)
func _update_btn_shortcut(effect_id: int, btn: Button): func _update_btn_shortcut(_effect_id: int, btn: Button):
var sc_lbl = btn.get_node_or_null("ShortcutLabel") var sc_lbl = btn.get_node_or_null("ShortcutLabel")
if not sc_lbl: return if not sc_lbl: return
var mode = LobbyManager.get_game_mode()
var is_sng = mode == GameMode.Mode.STOP_N_GO
var key_text = ""
# Safety check for SettingsManager (Autoload)
if not SettingsManager: if not SettingsManager:
sc_lbl.text = "" sc_lbl.text = ""
return return
if is_sng: # Show only Shortcut 1 as per single-slot request
match effect_id: sc_lbl.text = "[%s]" % SettingsManager.get_control_text("powerup_1")
0: key_text = SettingsManager.get_control_text("powerup_1")
3: key_text = SettingsManager.get_control_text("powerup_2") # Map P2 to Slot 3 (Ghost) in SNG
_: key_text = ""
else:
match effect_id:
0: key_text = SettingsManager.get_control_text("powerup_1")
2: key_text = SettingsManager.get_control_text("powerup_2")
1: key_text = SettingsManager.get_control_text("powerup_3")
3: key_text = SettingsManager.get_control_text("powerup_4")
sc_lbl.text = key_text
func _on_btn_pressed(effect_id: int): func _on_btn_pressed(effect_id: int):
print("[PowerUpUI] Clicked Button %d" % effect_id) print("[PowerUpUI] Clicked Button %d" % effect_id)
@@ -241,6 +221,10 @@ func _on_powerup_unlocked(effect: int, level: int):
var lvl_lbl = btn.get_node_or_null("LevelLabel") var lvl_lbl = btn.get_node_or_null("LevelLabel")
if lvl_lbl: if lvl_lbl:
lvl_lbl.text = "Lvl %d" % level lvl_lbl.text = "Lvl %d" % level
# Enforce 1-slot rule by hiding others
if special_manager_ref:
_on_inventory_updated(special_manager_ref.inventory)
else: else:
print("[PowerUpUI] ERROR: Unlocked Effect %d but no UI button found! Keys: %s" % [effect, icon_containers.keys()]) print("[PowerUpUI] ERROR: Unlocked Effect %d but no UI button found! Keys: %s" % [effect, icon_containers.keys()])
@@ -261,12 +245,15 @@ func _on_cooldown_updated(effect: int, time_left: float, max_time: float):
btn.modulate = Color.WHITE btn.modulate = Color.WHITE
func _on_inventory_updated(inventory: Dictionary): func _on_inventory_updated(inventory: Dictionary):
# Update UI icons (Dimmed vs Lit) and Enablement # Update UI icons (Only show ONE active slot as per user request)
print("[PowerUpUI] Inventory Updated: ", inventory) print("[PowerUpUI] Inventory Updated Signal Received! Data: ", inventory)
for effect in icon_containers: for effect in icon_containers:
if inventory.has(effect): var has_item = inventory.get(effect, false)
var has_item = inventory[effect] var btn = icon_containers[effect]
var btn = icon_containers[effect]
btn.modulate = Color.WHITE if has_item else Color(0.5, 0.5, 0.5, 0.5) # Single slot logic: Only the owned one is visible
btn.disabled = !has_item btn.visible = has_item
if has_item:
btn.disabled = false # Individual cooldown logic might disable it later
btn.modulate = Color.WHITE
_update_btn_shortcut(effect, btn)