feat: Implement powerup inventory UI and manager, introducing ghost and area freeze special tiles.

This commit is contained in:
Yogi Wiguna
2026-02-03 17:04:35 +08:00
parent 02d13d9ff5
commit 757051aca8
12 changed files with 297 additions and 69 deletions
+21 -4
View File
@@ -70,7 +70,11 @@ func simple_move_to(grid_position: Vector2i) -> bool:
return false
var cell_item = enhanced_gridmap.get_cell_item(Vector3i(grid_position.x, 0, grid_position.y))
if cell_item == -1 or cell_item in enhanced_gridmap.non_walkable_items:
# Allow passing through Walls (Item 4) if Invisible
var is_wall_passable = player.get("is_invisible") and cell_item == 4
if (cell_item == -1 or cell_item in enhanced_gridmap.non_walkable_items) and not is_wall_passable:
return false
if player.is_position_occupied(grid_position):
@@ -234,7 +238,14 @@ func highlight_movement_range():
# Check basic walkability
var cell_item = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z))
if cell_item == -1 or cell_item in enhanced_gridmap.non_walkable_items or player.is_position_occupied(test_pos):
# Allow passing through Walls (Item 4) if Invisible
var is_wall_passable = player.get("is_invisible") and cell_item == 4
if (cell_item == -1 or cell_item in enhanced_gridmap.non_walkable_items) and not is_wall_passable:
continue
if player.is_position_occupied(test_pos):
continue
# Check if there's a valid path to this cell
@@ -284,8 +295,14 @@ func can_reach_cell(target_pos: Vector2i, blocked_cells: Array) -> bool:
# Skip if already visited, blocked, or not valid
if visited.has(next_pos) or next_pos in blocked_cells:
continue
if not enhanced_gridmap.is_position_valid(next_pos) or not enhanced_gridmap.is_cell_walkable(next_pos, 0):
# Custom Walkable Check incorporating invisibility
var cell_item = enhanced_gridmap.get_cell_item(Vector3i(next_pos.x, 0, next_pos.y))
var is_wall_passable = player.get("is_invisible") and cell_item == 4
if (not enhanced_gridmap.is_position_valid(next_pos) or \
(cell_item in enhanced_gridmap.non_walkable_items and not is_wall_passable) or \
cell_item == -1):
continue
if player.is_position_occupied(next_pos) and next_pos != target_pos:
+133 -47
View File
@@ -52,8 +52,10 @@ const INVISIBLE_DURATION = 6.0
# Active effect tracking
var blocked_tiles: Array[Dictionary] = [] # {position: Vector3i, original_item: int, timer: float}
var freeze_zones: Array[Dictionary] = [] # {position: Vector2i, timer: float}
var active_freeze_zones: Array = [] # Array of {center, radius, timer}
var invisible_timer: float = 0.0
# INVENTORY SYSTEM
# Stores count of each power-up type. Max 1 per type as per user request?
# "player can store 1 of each different power up"
@@ -189,47 +191,107 @@ func _execute_faster_speed():
NotificationManager.send_message(player, "Speed Boost! (5s)", NotificationManager.MessageType.POWERUP)
func _execute_area_freeze():
# "Area Freeze... slow their speed movement for 3s"
# Reuse freeze logic but simpler duration
var center = player.current_position
# 3x3 around player
var radius = 1
print("Player %s executing Area Freeze" % player.name)
# "area with blue like wall but with far away from the player who use it"
# "Make it like 4 floor first (offset 4) and the continue to bigger when the level... is close to max"
# Get enemies in radius
# 1. Calculate Forward Direction based on Rotation
# Rotation 0 = South (+Z), PI = North (-Z)
var rot = player.rotation.y
var forward_x = round(sin(rot))
var forward_z = round(cos(rot))
var forward_vec = Vector2i(forward_x, forward_z)
# If rotation is diagonal or imprecise, normalize to cardinal
if abs(forward_x) > abs(forward_z):
forward_vec = Vector2i(sign(forward_x), 0)
else:
forward_vec = Vector2i(0, sign(forward_z))
# 2. Offset Center (4 tiles away)
var offset_dist = 4
var center = player.current_position + (forward_vec * offset_dist)
# 3. Determine Radius based on Level
# Level 1-4: Radius 1 (3x3 area)
# Level 5-8: Radius 2 (5x5 area)
var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1)
var radius = 1
if current_lvl >= 5:
radius = 2 # Bigger area at high levels
print("Player %s executing Area Freeze at %s (Offset %s, Lvl %d, Rad %d)" % [player.name, center, forward_vec, current_lvl, radius])
# Register Zone for persistence
active_freeze_zones.append({
"center": center,
"radius": radius,
"timer": FREEZE_SLOW_DURATION # Same duration as the visual
})
# Initial Check (Instant Feedback)
var all_players = player.get_tree().get_nodes_in_group("Players")
for p in all_players:
if p == player: continue
var dist = Vector2(p.current_position.x - center.x, p.current_position.y - center.y).length()
if dist <= 1.5: # Adjacent or on top
# Check distance (Chebyshev distance for square area)
var dx = abs(p.current_position.x - center.x)
var dy = abs(p.current_position.y - center.y)
# If inside square radius
if dx <= radius and dy <= radius:
p.rpc("apply_slow_effect", FREEZE_SLOW_DURATION)
NotificationManager.send_message(p, "Caught in Freeze Zone!", NotificationManager.MessageType.WARNING)
# Visual Feedback (Icy Floor for 3s?)
# Visual Feedback (Turn Floor Blue - Item 12 on Layer 0)
if player.is_multiplayer_authority():
for x in range(-1, 2):
for y in range(-1, 2):
# Sync Icy Floor (Layer 0)
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
var pos = center + Vector2i(x, y)
var main = player.get_tree().get_root().get_node_or_null("Main")
if main: main.rpc("sync_grid_item", pos.x, 2, pos.y, 15) # Icy decal
if enhanced_gridmap.is_position_valid(pos):
var main = player.get_tree().get_root().get_node_or_null("Main")
# Use Item 12 (Blue Freeze Tile) on Layer 0 (Floor)
if main: main.rpc("sync_grid_item", pos.x, 0, pos.y, 12)
# Cleanup visual timer (managed locally by author)
get_tree().create_timer(FREEZE_SLOW_DURATION).timeout.connect(func():
for x in range(-1, 2):
for y in range(-1, 2):
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
var pos = center + Vector2i(x, y)
var main = player.get_tree().get_root().get_node_or_null("Main")
if main: main.rpc("sync_grid_item", pos.x, 2, pos.y, -1)
if enhanced_gridmap.is_position_valid(pos):
var main = player.get_tree().get_root().get_node_or_null("Main")
# Restore to Item 0 (Standard Floor)
if main: main.rpc("sync_grid_item", pos.x, 0, pos.y, 0)
)
func _execute_block_floor(target: Node3D):
# Existing logic for blocking, reused
# "Wall Block" usually means block WHERE YOU ARE or FRONT?
# Original code blocked target's floor.
# If target is self, blocks self's floor? Maybe defensive.
# "Have the same cooldown as Faster" -> Just logic reuse.
# User Request: "choose one between horizontal or vertical all the way to the colus or rows"
# We interpret "Choose one" as random 50/50 since there's no UI for sub-selection.
# Check for Immunity (Invisible Mode)
if target.get("is_invisible"):
NotificationManager.send_message(target, "blocked!", NotificationManager.MessageType.POWERUP)
# We should probably notify the attacker too?
return
var center = target.current_position
var neighbors = enhanced_gridmap.get_neighbors(center, 1)
neighbors.append({"position": center})
var is_horizontal = rng.randf() < 0.5
var neighbors = []
if is_horizontal:
# Block entire Row (Fixed Z, iterate all X)
# Assuming 'center.y' corresponds to Grid Z-row
var row_z = center.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 = center.x
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])
for n in neighbors:
var pos = n.position
@@ -240,8 +302,9 @@ func _execute_block_floor(target: Node3D):
if main:
main.rpc("sync_grid_item", block_pos.x, block_pos.y, block_pos.z, 4)
var original_item = 0 # Assume floor
# If we have logic to save original, fine, but for now just 0
# We don't save original item here properly in this loop if we overwrite something important,
# but for Floor 0 it's usually just ground (0) or obstacles.
# If we overwrite another Wall, it's fine.
blocked_tiles.append({
"position": block_pos,
"original_item": 0,
@@ -250,9 +313,13 @@ func _execute_block_floor(target: Node3D):
NotificationManager.send_message(target, "Wall Block Created!", NotificationManager.MessageType.POWERUP)
func _execute_invisible_mode(target: Node3D):
# Existing logic kept as ID 14 placeholder
target.is_invisible = true
invisible_timer = INVISIBLE_DURATION
# Visual Feedback: Ghost Mode (Low Alpha)
if target.has_method("sync_modulate"):
target.rpc("sync_modulate", Color(1.0, 1.0, 1.0, 0.4)) # 40% Opacity
NotificationManager.send_message(target, "Invisible Mode!", NotificationManager.MessageType.POWERUP)
@@ -273,12 +340,12 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true):
if rng.randf() > 0.5: continue
var item_id: int
# 70% Chance for PowerUp (11-14)
# 70% Chance for Normal Tile (7-10)
if rng.randf() < 0.7:
item_id = rng.randi_range(11, 14)
else:
# 30% Chance for Normal Tile (7-10)
item_id = rng.randi_range(7, 10)
else:
# 30% Chance for PowerUp (11-14)
item_id = rng.randi_range(11, 14)
var cell = Vector3i(pos.x, 1, pos.y)
@@ -290,23 +357,35 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true):
func _update_freeze_zones(delta: float):
# Only the authority of this manager (the caster) handles the timers and cleanup
if not player.is_multiplayer_authority():
return
if not active_freeze_zones.is_empty():
var zones_to_remove = []
var zones_to_remove = []
for i in range(freeze_zones.size()):
freeze_zones[i].timer -= delta
if freeze_zones[i].timer <= 0:
zones_to_remove.append(i)
for i in range(active_freeze_zones.size()):
var zone = active_freeze_zones[i]
zone.timer -= delta
# Cleanup expired zones
zones_to_remove.reverse()
for idx in zones_to_remove:
var zone = freeze_zones[idx]
var main = player.get_tree().get_root().get_node_or_null("Main")
if main:
main.rpc("sync_grid_item", zone.position.x, 2, zone.position.y, -1)
freeze_zones.remove_at(idx)
# Check for players inside this zone (Trap Logic)
var all_players = player.get_tree().get_nodes_in_group("Players")
for p in all_players:
# Invisible Immunity (Passive)
if p.get("is_invisible"): continue
var dx = abs(p.current_position.x - zone.center.x)
var dy = abs(p.current_position.y - zone.center.y)
# If inside zone
if dx <= zone.radius and dy <= zone.radius:
# Apply slow effect repeatedly
# We use a short duration so it expires quickly if they leave
p.rpc("apply_slow_effect", 0.5)
if zone.timer <= 0:
zones_to_remove.append(i)
# Cleanup expired zones
zones_to_remove.reverse()
for idx in zones_to_remove:
active_freeze_zones.remove_at(idx)
func _check_for_icy_floor():
# Every player checks if they are standing on an icy floor (item 15 on layer 2)
@@ -314,6 +393,10 @@ func _check_for_icy_floor():
if not player.is_multiplayer_authority():
return
# Invisible Immunity (Passive)
if player.is_invisible:
return
if not enhanced_gridmap:
return
@@ -339,7 +422,6 @@ func _process(delta):
if powerup_cooldowns[effect] <= 0:
powerup_cooldowns[effect] = 0
emit_signal("cooldown_updated", effect, 0, 0)
print("Cooldown finished for %s" % SpecialEffect.keys()[effect])
# Update Active Buffs (Speed)
if active_buffs.has(SpecialEffect.FASTER_SPEED):
@@ -392,7 +474,11 @@ func _update_invisible_timer(delta: float):
invisible_timer = 0
if is_instance_valid(player):
player.is_invisible = false
NotificationManager.send_message(player, NotificationManager.MESSAGES.INVISIBILITY_ENDED, NotificationManager.MessageType.NORMAL)
# Reset Visuals
if player.has_method("sync_modulate"):
player.rpc("sync_modulate", Color.WHITE)
NotificationManager.send_message(player, "Invisibility Ended", NotificationManager.MessageType.NORMAL)
# =============================================================================
+1 -1
View File
@@ -160,7 +160,7 @@ func _on_cooldown_updated(effect: int, time_left: float, max_time: float):
var cd_lbl = btn.get_node_or_null("CooldownLabel")
if cd_lbl:
if time_left > 0:
cd_lbl.text = "%.1f" % time_left
cd_lbl.text = "%d" % int(time_left)
btn.disabled = true
btn.modulate = Color(0.7, 0.7, 0.7, 0.8)
else: