465 lines
17 KiB
GDScript
465 lines
17 KiB
GDScript
extends Node
|
|
|
|
# SpecialTilesManager - Handles special effects triggered by holo tile pickups
|
|
|
|
# Holo tile indices (11-14) trigger special effects
|
|
const HOLO_TILES = [11, 12, 13, 14]
|
|
|
|
enum SpecialEffect {
|
|
BURN_TILES, # Remove 3x3 pattern tiles on random opponent
|
|
SPAWN_TILES, # Spawn 3x3 pattern tiles around activating player
|
|
FREEZE_PLAYER, # Freeze random opponent for 3 seconds
|
|
BLOCK_FLOOR, # Make nearby tile non-walkable for 9 seconds
|
|
INVISIBLE_MODE # Speed boost + auto-grab + shield for 6 seconds
|
|
}
|
|
|
|
# Random shape patterns for 3x3 area (relative offsets from center)
|
|
const PATTERNS = {
|
|
"T": [Vector2i(0, -1), Vector2i(-1, 0), Vector2i(0, 0), Vector2i(1, 0)],
|
|
"L": [Vector2i(0, -1), Vector2i(0, 0), Vector2i(0, 1), Vector2i(1, 1)],
|
|
"I_H": [Vector2i(-1, 0), Vector2i(0, 0), Vector2i(1, 0)],
|
|
"I_V": [Vector2i(0, -1), Vector2i(0, 0), Vector2i(0, 1)],
|
|
"PLUS": [Vector2i(0, -1), Vector2i(-1, 0), Vector2i(0, 0), Vector2i(1, 0), Vector2i(0, 1)],
|
|
"CORNER": [Vector2i(-1, -1), Vector2i(0, -1), Vector2i(-1, 0), Vector2i(0, 0)],
|
|
"FULL": [Vector2i(-1, -1), Vector2i(0, -1), Vector2i(1, -1), Vector2i(-1, 0), Vector2i(0, 0), Vector2i(1, 0), Vector2i(-1, 1), Vector2i(0, 1)],
|
|
"DOT3": [Vector2i(-1, 0), Vector2i(0, 0), Vector2i(1, 0)]
|
|
}
|
|
|
|
var player: Node3D
|
|
var enhanced_gridmap: Node
|
|
var rng: RandomNumberGenerator
|
|
|
|
# Effect durations
|
|
const FREEZE_ZONE_DURATION = 15.0
|
|
const FREEZE_SLOW_MULTIPLIER = 0.2 # Super slow down
|
|
const BLOCK_DURATION = 9.0
|
|
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 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"
|
|
var inventory = {
|
|
SpecialEffect.BURN_TILES: false, # Coin
|
|
SpecialEffect.SPAWN_TILES: false, # (Merged with Coin or deprecated? User said "coin : random between two")
|
|
# Let's map Items 7-10 to Effects
|
|
# 7=Heart=Block, 8=Diamond=Freeze, 9=Star=Invisible, 10=Coin=Burn/Spawn
|
|
SpecialEffect.BLOCK_FLOOR: false,
|
|
SpecialEffect.FREEZE_PLAYER: false,
|
|
SpecialEffect.INVISIBLE_MODE: false
|
|
}
|
|
|
|
# Signal for UI
|
|
signal inventory_updated(inventory_data: Dictionary)
|
|
|
|
func initialize(p_player: Node3D, p_gridmap: Node):
|
|
player = p_player
|
|
enhanced_gridmap = p_gridmap
|
|
rng = RandomNumberGenerator.new()
|
|
rng.randomize()
|
|
|
|
# =============================================================================
|
|
# Helper: Item ID to Effect Enum
|
|
# =============================================================================
|
|
func get_effect_from_item(item_id: int) -> int:
|
|
match item_id:
|
|
7: return SpecialEffect.BLOCK_FLOOR # Heart
|
|
8: return SpecialEffect.FREEZE_PLAYER # Diamond
|
|
9: return SpecialEffect.INVISIBLE_MODE # Star
|
|
10: return SpecialEffect.BURN_TILES # Coin (Handles Burn or Spawn)
|
|
_: return -1
|
|
|
|
func add_powerup_from_item(item_id: int):
|
|
var effect = get_effect_from_item(item_id)
|
|
if effect != -1:
|
|
inventory[effect] = true
|
|
emit_signal("inventory_updated", inventory)
|
|
print("Player %s picked up powerup for effect %s" % [player.name, SpecialEffect.keys()[effect]])
|
|
|
|
if player.is_multiplayer_authority():
|
|
rpc("sync_inventory_add", effect)
|
|
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func sync_inventory_add(effect: int):
|
|
inventory[effect] = true
|
|
emit_signal("inventory_updated", inventory)
|
|
|
|
func remove_powerup(effect: int):
|
|
inventory[effect] = false
|
|
emit_signal("inventory_updated", inventory)
|
|
if player.is_multiplayer_authority():
|
|
rpc("sync_inventory_remove", effect)
|
|
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func sync_inventory_remove(effect: int):
|
|
inventory[effect] = false
|
|
emit_signal("inventory_updated", inventory)
|
|
|
|
# =============================================================================
|
|
# Activate Effect (Explicit Target)
|
|
# =============================================================================
|
|
|
|
func activate_effect(effect: int, target_player: Node3D = null):
|
|
# Validation
|
|
if not inventory.get(effect, false):
|
|
return # Start/Client mismatch
|
|
|
|
# Consume
|
|
remove_powerup(effect)
|
|
|
|
print("[SpecialTiles] Player %s activated %s on %s" % [player.name, SpecialEffect.keys()[effect], target_player.name if target_player else "Self"])
|
|
|
|
match effect:
|
|
SpecialEffect.BURN_TILES:
|
|
# Coin: Random between Burn or Spawn
|
|
# "coin : random between two... make it not use directly" -> When activated, it does one of them.
|
|
if rng.randf() < 0.5:
|
|
_execute_burn_tiles(target_player)
|
|
else:
|
|
# Spawn tiles around SELF (as per user request "around activating player")
|
|
_execute_spawn_tiles(player)
|
|
|
|
SpecialEffect.BLOCK_FLOOR:
|
|
if target_player:
|
|
_execute_block_floor(target_player)
|
|
|
|
SpecialEffect.FREEZE_PLAYER:
|
|
_execute_freeze_zones() # No target needed anymore
|
|
|
|
SpecialEffect.INVISIBLE_MODE:
|
|
# Always self
|
|
_execute_invisible_mode(player)
|
|
|
|
# Play generic cast animation or sound?
|
|
if player.is_multiplayer_authority():
|
|
player.rpc("trigger_screen_shake", "light")
|
|
|
|
|
|
# =============================================================================
|
|
# Check if item is a holo tile
|
|
# =============================================================================
|
|
|
|
func is_holo_tile(item_id: int) -> bool:
|
|
return item_id in HOLO_TILES
|
|
|
|
# =============================================================================
|
|
# Trigger random special effect
|
|
# =============================================================================
|
|
|
|
func trigger_random_effect():
|
|
var effect = rng.randi() % SpecialEffect.size()
|
|
|
|
print("[SpecialTiles] Player %s triggered effect: %s" % [player.name, SpecialEffect.keys()[effect]])
|
|
|
|
match effect:
|
|
SpecialEffect.BURN_TILES:
|
|
_execute_burn_tiles(_get_random_opponent())
|
|
SpecialEffect.SPAWN_TILES:
|
|
_execute_spawn_tiles(player)
|
|
SpecialEffect.FREEZE_PLAYER:
|
|
_execute_freeze_zones()
|
|
SpecialEffect.BLOCK_FLOOR:
|
|
_execute_block_floor(player)
|
|
SpecialEffect.INVISIBLE_MODE:
|
|
_execute_invisible_mode(player)
|
|
|
|
# Sync effect to all clients
|
|
if player.is_multiplayer_authority():
|
|
rpc("sync_effect_triggered", effect)
|
|
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func sync_effect_triggered(effect: int):
|
|
print("[SpecialTiles] Synced effect %s for player %s" % [SpecialEffect.keys()[effect], player.name])
|
|
|
|
# =============================================================================
|
|
# Effect Implementations
|
|
# =============================================================================
|
|
|
|
func _execute_burn_tiles(target: Node3D):
|
|
if not target: return
|
|
|
|
# Knock tiles from target's board
|
|
var board_indices = []
|
|
for i in range(target.playerboard.size()):
|
|
if target.playerboard[i] != -1:
|
|
board_indices.append(i)
|
|
|
|
if board_indices.is_empty():
|
|
return
|
|
|
|
var burn_count = rng.randi_range(3, 6)
|
|
board_indices.shuffle()
|
|
|
|
# Drop logic similar to burn but we just destroy them or scatter?
|
|
# "BURN_TILES, # Remove 3x3 pattern tiles" -> User request says Remove pattern.
|
|
# Original code did burn.
|
|
# Let's just remove them.
|
|
|
|
for i in range(min(burn_count, board_indices.size())):
|
|
target.playerboard[board_indices[i]] = -1
|
|
|
|
if player.is_multiplayer_authority():
|
|
# Sync the change on the target
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if main:
|
|
main.rpc("sync_playerboard", target.name.to_int(), target.playerboard)
|
|
|
|
NotificationManager.send_message(target, NotificationManager.MESSAGES.BURNED_BY % player.display_name, NotificationManager.MessageType.WARNING)
|
|
|
|
|
|
func _execute_spawn_tiles(target: Node3D):
|
|
# Spawn tiles around TARGET (usually Self for Coin)
|
|
spawn_powerups_around(target.current_position, false) # False = normal tiles? User says "Spawn 3x3 pattern tiles"
|
|
# Okay "SPAWN_TILES" usually means useful numbered tiles.
|
|
# But "spawn / replace your nearby tiles into power up" is for Headbutt.
|
|
# For Coin->Spawn_Tiles: "Spawn 3x3 pattern tiles around activating player ( self )".
|
|
# So random number tiles (7-10 are powerups, 1-6 are normal? No, 7-10 are patterns in this game).
|
|
# "Spawn 3x3 pattern tiles" -> Tiles with ID 7,8,9,10 are the goal tiles.
|
|
|
|
NotificationManager.send_message(target, NotificationManager.MESSAGES.TILES_SPAWNED, NotificationManager.MessageType.POWERUP)
|
|
|
|
func _execute_freeze_zones():
|
|
# Area-based freeze: spawn pattern around activating player
|
|
var center = player.current_position
|
|
var pattern_keys = PATTERNS.keys()
|
|
var pattern_name = pattern_keys[rng.randi() % pattern_keys.size()]
|
|
var pattern = PATTERNS[pattern_name]
|
|
|
|
print("[SpecialTiles] Player %s spawning FREEZE zones in pattern %s" % [player.name, pattern_name])
|
|
|
|
for offset in pattern:
|
|
var pos = center + offset
|
|
if enhanced_gridmap.is_position_valid(pos):
|
|
# Add to active zones
|
|
freeze_zones.append({
|
|
"position": pos,
|
|
"timer": FREEZE_ZONE_DURATION
|
|
})
|
|
|
|
# Sync visual (use item ID 15 for "Icy" floor marker or similar)
|
|
if player.is_multiplayer_authority():
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if main:
|
|
# Layer 2 for effect overlays
|
|
main.rpc("sync_grid_item", pos.x, 2, pos.y, 15)
|
|
|
|
NotificationManager.send_message(player, NotificationManager.MESSAGES.FREEZE_ZONE_READY if NotificationManager.MESSAGES.has("FREEZE_ZONE_READY") else "Freeze zones deployed!", NotificationManager.MessageType.POWERUP)
|
|
|
|
func _execute_block_floor(target: Node3D):
|
|
# Make nearby tile non-walkable for 9 seconds
|
|
# Target the floor UNDER or NEAR the target?
|
|
# "block ( other player ) spawn BLOCK_FLOOR Make nearby tile non-walkable"
|
|
# Let's block the tile they are standing on + neighbors?
|
|
var center = target.current_position
|
|
var neighbors = enhanced_gridmap.get_neighbors(center, 1) # Include diagonals
|
|
neighbors.append({"position": center}) # Add center
|
|
|
|
for n in neighbors:
|
|
var pos = n.position
|
|
var block_pos = Vector3i(pos.x, 0, pos.y)
|
|
|
|
# Block it
|
|
if player.is_multiplayer_authority():
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if main:
|
|
# 4 = Blocked Tile ID usually
|
|
main.rpc("sync_grid_item", block_pos.x, block_pos.y, block_pos.z, 4)
|
|
|
|
var original_item = enhanced_gridmap.get_cell_item(block_pos)
|
|
blocked_tiles.append({
|
|
"position": block_pos,
|
|
"original_item": original_item if original_item != 4 else 0, # Restore to 0 (floor) if confused
|
|
"timer": BLOCK_DURATION
|
|
})
|
|
|
|
NotificationManager.send_message(target, NotificationManager.MESSAGES.FLOOR_BLOCKED, NotificationManager.MessageType.WARNING)
|
|
|
|
func _execute_invisible_mode(target: Node3D):
|
|
target.is_invisible = true
|
|
# Auto-disable after duration handled in Player._process or here?
|
|
# SpecialTilesManager seems to handle effect timers.
|
|
invisible_timer = INVISIBLE_DURATION
|
|
NotificationManager.send_message(target, NotificationManager.MESSAGES.INVISIBLE, NotificationManager.MessageType.POWERUP)
|
|
|
|
|
|
# =============================================================================
|
|
# Helper: Spawn Powerups (For Super Push)
|
|
# =============================================================================
|
|
|
|
func spawn_powerups_around(center: Vector2i, force_powerups: bool = true):
|
|
# "spawn / replace your nearby tiles into power up ( special tiles )"
|
|
# PowerUp Tiles are 7, 8, 9, 10 (Heart, Diamond, Star, Coin)
|
|
var radius = 2
|
|
for x in range(-radius, radius + 1):
|
|
for y in range(-radius, radius + 1):
|
|
var pos = center + Vector2i(x, y)
|
|
if enhanced_gridmap.is_position_valid(pos):
|
|
# Random chance
|
|
if rng.randf() > 0.4: continue
|
|
|
|
var item_id = rng.randi_range(7, 10) # 7-10 are the special tiles
|
|
var cell = Vector3i(pos.x, 1, pos.y)
|
|
|
|
if player.is_multiplayer_authority():
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if main:
|
|
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, item_id)
|
|
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
# 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)
|
|
|
|
func _check_for_icy_floor():
|
|
# Every player checks if they are standing on an icy floor (item 15 on layer 2)
|
|
# This ensures slow-mo works even if zones were cast by another player.
|
|
if not player.is_multiplayer_authority():
|
|
return
|
|
|
|
if not enhanced_gridmap:
|
|
return
|
|
|
|
var current_item = enhanced_gridmap.get_cell_item(Vector3i(player.current_position.x, 2, player.current_position.y))
|
|
if current_item == 15:
|
|
_apply_slow_mo(player)
|
|
elif player.movement_manager and player.movement_manager.speed_multiplier < 1.0:
|
|
# Check if we should restore speed
|
|
# In this case, we'll let _apply_slow_mo's timer handle it,
|
|
# OR we can explicitly reset here if NOT on item 15.
|
|
pass
|
|
|
|
func _process(delta):
|
|
_update_blocked_tiles(delta)
|
|
_update_invisible_timer(delta)
|
|
_update_freeze_zones(delta)
|
|
_check_for_icy_floor()
|
|
|
|
func _apply_slow_mo(target_player: Node3D):
|
|
if target_player.has_method("apply_stagger") and target_player.is_frozen:
|
|
return # Already fully frozen/staggered
|
|
|
|
if target_player.movement_manager:
|
|
target_player.movement_manager.set_speed_multiplier(FREEZE_SLOW_MULTIPLIER)
|
|
# Visual tint
|
|
if target_player.has_method("sync_modulate"):
|
|
target_player.rpc("sync_modulate", Color(0.6, 0.8, 1.0)) # Icy blue
|
|
|
|
# Reset speed after a short delay if they leave
|
|
_create_restore_speed_timer(target_player, 0.2)
|
|
|
|
func _create_restore_speed_timer(target_player: Node3D, duration: float):
|
|
# We use a short timer to reset speed. If they are still in the zone,
|
|
# _process will re-apply it next frame.
|
|
await player.get_tree().create_timer(duration).timeout
|
|
if is_instance_valid(target_player) and target_player.movement_manager:
|
|
# Check if they are still on an icy floor
|
|
var still_in_zone = false
|
|
if enhanced_gridmap:
|
|
var item = enhanced_gridmap.get_cell_item(Vector3i(target_player.current_position.x, 2, target_player.current_position.y))
|
|
if item == 15:
|
|
still_in_zone = true
|
|
|
|
if not still_in_zone:
|
|
target_player.movement_manager.set_speed_multiplier(1.0)
|
|
if target_player.has_method("sync_modulate"):
|
|
target_player.rpc("sync_modulate", Color.WHITE)
|
|
|
|
func _update_invisible_timer(delta: float):
|
|
if invisible_timer > 0:
|
|
invisible_timer -= delta
|
|
if invisible_timer <= 0:
|
|
invisible_timer = 0
|
|
if is_instance_valid(player):
|
|
player.is_invisible = false
|
|
NotificationManager.send_message(player, NotificationManager.MESSAGES.INVISIBILITY_ENDED, NotificationManager.MessageType.NORMAL)
|
|
|
|
|
|
# =============================================================================
|
|
# Helper Functions
|
|
# =============================================================================
|
|
|
|
func _get_random_opponent() -> Node3D:
|
|
var all_players = player.get_tree().get_nodes_in_group("Players")
|
|
var opponents = all_players.filter(func(p): return p != player)
|
|
|
|
if opponents.is_empty():
|
|
return null
|
|
|
|
return opponents[rng.randi() % opponents.size()]
|
|
|
|
func _get_empty_neighbors_recursive(center: Vector2i, radius: int) -> Array[Vector2i]:
|
|
var result: Array[Vector2i] = []
|
|
for x in range(-radius, radius + 1):
|
|
for y in range(-radius, radius + 1):
|
|
var pos = center + Vector2i(x, y)
|
|
if enhanced_gridmap.is_position_valid(pos):
|
|
if enhanced_gridmap.get_cell_item(Vector3i(pos.x, 1, pos.y)) == -1:
|
|
result.append(pos)
|
|
return result
|
|
|
|
func _update_blocked_tiles(delta: float):
|
|
var tiles_to_restore: Array[int] = []
|
|
|
|
for i in range(blocked_tiles.size()):
|
|
blocked_tiles[i].timer -= delta
|
|
if blocked_tiles[i].timer <= 0:
|
|
tiles_to_restore.append(i)
|
|
|
|
# Restore tiles in reverse order to maintain indices
|
|
tiles_to_restore.reverse()
|
|
for idx in tiles_to_restore:
|
|
var tile_data = blocked_tiles[idx]
|
|
if player.is_multiplayer_authority():
|
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
|
if main:
|
|
main.rpc("sync_grid_item", tile_data.position.x, tile_data.position.y, tile_data.position.z, tile_data.original_item)
|
|
blocked_tiles.remove_at(idx)
|
|
|
|
if tiles_to_restore.size() > 0:
|
|
enhanced_gridmap.initialize_astar()
|
|
|
|
# =============================================================================
|
|
# Shield Check (for Invisible Mode)
|
|
# =============================================================================
|
|
|
|
func check_shield_and_cancel_effect() -> bool:
|
|
"""Returns true if player has shield (invisible mode) and cancels the incoming effect."""
|
|
if player.get("is_invisible"):
|
|
player.set("is_invisible", false)
|
|
invisible_timer = 0 # Cancel timer
|
|
if player.get("original_movement_range"):
|
|
player.movement_range = player.original_movement_range
|
|
NotificationManager.send_message(player, NotificationManager.MESSAGES.SHIELD_BLOCKED, NotificationManager.MessageType.POWERUP)
|
|
return true
|
|
return false
|
|
|
|
func _create_unfreeze_timer(target_player: Node3D, duration: float):
|
|
await player.get_tree().create_timer(duration).timeout
|
|
if is_instance_valid(target_player):
|
|
target_player.set("is_frozen", false)
|
|
# Reset visuals
|
|
if target_player.has_method("sync_modulate"):
|
|
target_player.rpc("sync_modulate", Color.WHITE)
|
|
NotificationManager.send_message(target_player, NotificationManager.MESSAGES.UNFROZEN, NotificationManager.MessageType.NORMAL)
|