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_DURATION = 3.0 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} func initialize(p_player: Node3D, p_gridmap: Node): player = p_player enhanced_gridmap = p_gridmap rng = RandomNumberGenerator.new() rng.randomize() func _process(delta): _update_blocked_tiles(delta) # ============================================================================= # 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() SpecialEffect.SPAWN_TILES: _execute_spawn_tiles() SpecialEffect.FREEZE_PLAYER: _execute_freeze_player() SpecialEffect.BLOCK_FLOOR: _execute_block_floor() SpecialEffect.INVISIBLE_MODE: _execute_invisible_mode() # 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]) # ============================================================================= # Pattern Generation # ============================================================================= func _get_random_pattern() -> Array[Vector2i]: var pattern_keys = PATTERNS.keys() var selected_pattern = pattern_keys[rng.randi() % pattern_keys.size()] var base_pattern = PATTERNS[selected_pattern].duplicate() # Randomly rotate pattern (0, 90, 180, 270 degrees) var rotations = rng.randi() % 4 for i in range(rotations): for j in range(base_pattern.size()): var p = base_pattern[j] base_pattern[j] = Vector2i(-p.y, p.x) # 90 degree rotation # Ensure pattern has 3-8 cells var result: Array[Vector2i] = [] for offset in base_pattern: result.append(offset) return result func _get_valid_pattern_positions(center: Vector2i, pattern: Array[Vector2i]) -> Array[Vector2i]: var valid_positions: Array[Vector2i] = [] for offset in pattern: var pos = center + offset if enhanced_gridmap.is_position_valid(pos): valid_positions.append(pos) return valid_positions # ============================================================================= # Effect Implementations # ============================================================================= func _execute_burn_tiles(): # Find random opponent var opponent = _get_random_opponent() if not opponent: print("[SpecialTiles] No opponent found for BURN_TILES") return # Get pattern around opponent var pattern = _get_random_pattern() var positions = _get_valid_pattern_positions(opponent.current_position, pattern) # Remove tiles at these positions for pos in positions: var cell = Vector3i(pos.x, 1, pos.y) if enhanced_gridmap.get_cell_item(cell) != -1: 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, -1) print("[SpecialTiles] BURN_TILES: Removed %d tiles around %s" % [positions.size(), opponent.name]) player.rpc("display_message", "Burned tiles near opponent!") func _execute_spawn_tiles(): # Get pattern around activating player var pattern = _get_random_pattern() var positions = _get_valid_pattern_positions(player.current_position, pattern) # Spawn random tiles at empty positions var spawned_count = 0 for pos in positions: var cell = Vector3i(pos.x, 1, pos.y) if enhanced_gridmap.get_cell_item(cell) == -1: var new_tile = rng.randi_range(7, 10) # Random normal tile 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, new_tile) spawned_count += 1 print("[SpecialTiles] SPAWN_TILES: Spawned %d tiles around %s" % [spawned_count, player.name]) player.rpc("display_message", "Spawned new tiles!") func _execute_freeze_player(): # Find random opponent var opponent = _get_random_opponent() if not opponent: print("[SpecialTiles] No opponent found for FREEZE_PLAYER") return # Freeze the opponent if opponent.has_method("apply_freeze"): opponent.apply_freeze(FREEZE_DURATION) else: # Fallback: directly set frozen state opponent.set("is_frozen", true) _create_unfreeze_timer(opponent, FREEZE_DURATION) print("[SpecialTiles] FREEZE_PLAYER: Froze %s for %ds" % [opponent.name, FREEZE_DURATION]) player.rpc("display_message", "Froze an opponent!") opponent.rpc("display_message", "You are frozen!") 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) target_player.rpc("display_message", "Unfrozen!") func _execute_block_floor(): # Find valid tile near player to block var neighbors = enhanced_gridmap.get_neighbors(player.current_position, 0) var valid_neighbors = neighbors.filter(func(n): return n.is_walkable) if valid_neighbors.is_empty(): print("[SpecialTiles] No valid tile to block") return var target_neighbor = valid_neighbors[rng.randi() % valid_neighbors.size()] var block_pos = Vector3i(target_neighbor.position.x, 0, target_neighbor.position.y) var original_item = enhanced_gridmap.get_cell_item(block_pos) # Make tile non-walkable (use a blocked item index) var blocked_item = 4 # Using non_walkable_items[0] typically if enhanced_gridmap.non_walkable_items.size() > 0: blocked_item = enhanced_gridmap.non_walkable_items[0] if player.is_multiplayer_authority(): var main = player.get_tree().get_root().get_node_or_null("Main") if main: main.rpc("sync_grid_item", block_pos.x, block_pos.y, block_pos.z, blocked_item) # Track blocked tile for restoration blocked_tiles.append({ "position": block_pos, "original_item": original_item, "timer": BLOCK_DURATION }) # Re-initialize pathfinding enhanced_gridmap.initialize_astar() print("[SpecialTiles] BLOCK_FLOOR: Blocked tile at %s for %ds" % [target_neighbor.position, BLOCK_DURATION]) player.rpc("display_message", "Blocked a floor tile!") func _execute_invisible_mode(): # Set invisible mode on player if player.has_method("apply_invisible_mode"): player.apply_invisible_mode(INVISIBLE_DURATION) else: # Fallback: directly set invisible state player.set("is_invisible", true) player.set("original_movement_range", player.movement_range) player.movement_range = player.movement_range + 2 # Speed boost _create_invisibility_timer(INVISIBLE_DURATION) print("[SpecialTiles] INVISIBLE_MODE: %s is now invisible for %ds" % [player.name, INVISIBLE_DURATION]) player.rpc("display_message", "Invisible mode activated!") func _create_invisibility_timer(duration: float): await player.get_tree().create_timer(duration).timeout if is_instance_valid(player): player.set("is_invisible", false) if player.get("original_movement_range"): player.movement_range = player.original_movement_range player.rpc("display_message", "Invisible mode ended!") # ============================================================================= # 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 _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) if player.get("original_movement_range"): player.movement_range = player.original_movement_range player.rpc("display_message", "Shield blocked an attack!") return true return false