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)