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 { FASTER_SPEED, # ID 11 AREA_FREEZE, # ID 12 BLOCK_FLOOR, # ID 13 INVISIBLE_MODE # ID 14 } # Levels & Cooldowns var powerup_levels: Dictionary = {} # EffectEnum -> int (1 to 8) var powerup_cooldowns: Dictionary = {} # EffectEnum -> float (Time Remaining) var active_buffs: Dictionary = {} # EffectEnum -> float (Duration Running) # Cooldown Constants (Level 1 / Level 8) const COOLDOWN_L1 = 15.0 const COOLDOWN_L8 = 5.0 const GLOBAL_COOLDOWN_MAX = 5.0 const FASTER_DURATION = 5.0 const FREEZE_SLOW_DURATION = 3.0 signal cooldown_updated(effect: int, time_left: float, max_time: float) signal global_cooldown_updated(time_left: float, max_time: float) signal powerup_unlocked(effect: int, level: int) # New Helper functions for Targeting and Preview func get_skill_affected_area(effect: int, center_pos: Vector2i) -> Array[Vector2i]: var area: Array[Vector2i] = [] if effect == SpecialEffect.AREA_FREEZE: var radius = 2 # 5x5 area for freeze for x in range(-radius, radius + 1): for z in range(-radius, radius + 1): area.append(center_pos + Vector2i(x, z)) elif effect == SpecialEffect.BLOCK_FLOOR: # Wall logic: project full line based on player orientation var last_dir = player.movement_manager.last_move_direction if (player and player.movement_manager) else Vector2i(0, 1) if last_dir.x != 0: # Vertical Wall for z in range(enhanced_gridmap.rows): area.append(Vector2i(center_pos.x, z)) else: # Horizontal Wall for x in range(enhanced_gridmap.columns): area.append(Vector2i(x, center_pos.y)) return area func execute_targeted_effect(effect: int, target_pos: Vector2i): # Apply Cooldown NOW var level = powerup_levels.get(effect, 1) var cooldown_time = COOLDOWN_L1 + ((level - 1) * (COOLDOWN_L8 - COOLDOWN_L1) / 7.0) powerup_cooldowns[effect] = cooldown_time emit_signal("cooldown_updated", effect, cooldown_time, cooldown_time) print("[SpecialTiles] Executing Targeted Effect %s at %s" % [SpecialEffect.keys()[effect], target_pos]) match effect: SpecialEffect.AREA_FREEZE: _execute_area_freeze(target_pos) SpecialEffect.BLOCK_FLOOR: _execute_block_floor(target_pos) # Animation / Shake if player.is_multiplayer_authority(): player.rpc("trigger_screen_shake", "light") # Also reset action loop? (ONLY for human players) # 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 active_freeze_zones: Array = [] # Array of {center, radius, timer} var invisible_timer: float = 0.0 var global_cooldown_timer: float = 0.0 # Targeting Mode var is_targeting_mode: bool = false var targeting_effect: int = -1 var target_indicator_pos: Vector2i = Vector2i.ZERO # 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.FASTER_SPEED: false, SpecialEffect.AREA_FREEZE: false, SpecialEffect.BLOCK_FLOOR: 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() # 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 # ============================================================================= func get_effect_from_item(item_id: int) -> int: var mode = LobbyManager.get_game_mode() var is_restricted = GameMode.is_restricted(mode) match item_id: 11: return SpecialEffect.FASTER_SPEED 12: return SpecialEffect.AREA_FREEZE 13: return SpecialEffect.BLOCK_FLOOR 14: return SpecialEffect.INVISIBLE_MODE _: return -1 func add_powerup_from_item(item_id: int): var effect = get_effect_from_item(item_id) if effect == -1: return # 1-PowerUp Rule: If this is a DIFFERENT power-up, clear the old one var is_different = not inventory.get(effect, false) var already_has_any = false for e in inventory: if inventory[e]: already_has_any = true break if is_different and already_has_any: print("Player %s replacing existing powerup with %s" % [player.name, SpecialEffect.keys()[effect]]) for e in inventory: inventory[e] = false powerup_levels[e] = 1 # Reset levels of discarded powerups _exit_targeting_mode() # 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: rpc("sync_inventory_add", effect, 8) @rpc("any_peer", "call_local", "reliable") 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 _exit_targeting_mode() inventory[effect] = true powerup_levels[effect] = level emit_signal("inventory_updated", inventory) emit_signal("powerup_unlocked", effect, level) func remove_powerup(effect: int): # User Request: "If the power up already use it will remain empty" if inventory.get(effect, false): inventory[effect] = false powerup_levels[effect] = 1 emit_signal("inventory_updated", inventory) print("[SpecialTiles] Player %s consumed powerup %s" % [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: rpc("sync_inventory_remove", effect) @rpc("any_peer", "call_local", "reliable") func sync_inventory_remove(effect: int): inventory[effect] = false powerup_levels[effect] = 1 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): print("PowerUp %s not found in inventory or false. Inventory: %s" % [effect, inventory]) return # Check Cooldown if global_cooldown_timer > 0: NotificationManager.send_message(player, "Skill in Cooldown! (%.1fs)" % global_cooldown_timer, NotificationManager.MessageType.WARNING) return # Check Attack Mode Restriction if player.get("is_attack_mode") and effect == SpecialEffect.INVISIBLE_MODE: NotificationManager.send_message(player, "Cannot enter Ghost mode while in Attack Mode!", NotificationManager.MessageType.WARNING) return # Check Carrying Restriction if player.get("is_carrying_tekton") and effect != SpecialEffect.FASTER_SPEED: NotificationManager.send_message(player, "Cannot use this power while carrying a Tekton!", NotificationManager.MessageType.WARNING) return var level = powerup_levels.get(effect, 1) print("[SpecialTiles] Player %s activated %s (Lvl %d)." % [player.name, SpecialEffect.keys()[effect], level]) match effect: SpecialEffect.FASTER_SPEED: _execute_faster_speed() SpecialEffect.AREA_FREEZE, SpecialEffect.BLOCK_FLOOR: # TWO-CLICK LOGIC: First click starts Targeting Mode (Projection), second click executes. if is_targeting_mode and targeting_effect == effect: # SECOND CLICK: Execute at current indicator position _execute_targeted_effect_v2(effect, target_indicator_pos) _exit_targeting_mode() else: # FIRST CLICK: Start targeting _enter_targeting_mode(effect) return # Don't apply cooldown or generic effects yet SpecialEffect.INVISIBLE_MODE: _execute_invisible_mode(player) # Apply 5s cooldown globally global_cooldown_timer = 5.0 emit_signal("global_cooldown_updated", global_cooldown_timer, GLOBAL_COOLDOWN_MAX) # 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: player.rpc("trigger_screen_shake", "light") # Consumable: Remove after use (User Request) print("[SpecialTiles] Consuming %s after successful activation." % SpecialEffect.keys()[effect]) remove_powerup(effect) # ============================================================================= # Targeting Mode Helpers # ============================================================================= func _enter_targeting_mode(effect: int): is_targeting_mode = true targeting_effect = effect print("[SpecialTiles] Entered Targeting Mode for %s" % SpecialEffect.keys()[effect]) # Visual feedback if needed func _exit_targeting_mode(): if is_targeting_mode: is_targeting_mode = false targeting_effect = -1 if player.action_manager: player.action_manager.clear_highlights() print("[SpecialTiles] Exited Targeting Mode.") func _update_targeting_preview(): if not player.is_multiplayer_authority(): return # Logic for projection: # Freeze: 4 tiles ahead # Wall: Always behind player var distance = 4 if targeting_effect == SpecialEffect.BLOCK_FLOOR: distance = -1 # 1 tile behind var move_dir = Vector2i.ZERO if player.movement_manager: move_dir = player.movement_manager.current_move_direction if move_dir == Vector2i.ZERO: move_dir = player.movement_manager.last_move_direction if move_dir == Vector2i.ZERO: move_dir = Vector2i(0, 1) # Fallback var center_pos = player.current_position + (move_dir * distance) target_indicator_pos = center_pos # Show highlights via ActionManager var area = get_skill_affected_area(targeting_effect, center_pos) # User Request: Use yellow/orange like hover (ID 1 in EnhancedGridMap) var indicator_id = 1 if player.action_manager: player.action_manager.highlight_cells_if_authorized(area, indicator_id) func _execute_targeted_effect_v2(effect: int, target_pos: Vector2i): match effect: SpecialEffect.AREA_FREEZE: _execute_area_freeze(target_pos) SpecialEffect.BLOCK_FLOOR: _execute_block_floor(target_pos) # ============================================================================= # Check if item is a holo tile # ============================================================================= func is_holo_tile(item_id: int) -> bool: return item_id in HOLO_TILES # ============================================================================= # Effect Implementations (New) # ============================================================================= func _execute_faster_speed(): # VFX: show speed initiator on all peers if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc(): player.rpc("play_skill_vfx", "skill_speed") elif player.has_method("play_skill_vfx"): player.play_skill_vfx("skill_speed") if player.movement_manager: player.movement_manager.set_speed_multiplier(1.5) # 50% faster active_buffs[SpecialEffect.FASTER_SPEED] = FASTER_DURATION SfxManager.rpc("play_rpc", "speed") NotificationManager.send_message(player, "Speed Boost! (5s)", NotificationManager.MessageType.POWERUP) func _execute_area_freeze(target_pos: Vector2i = Vector2i(-9999, -9999)): # VFX: show freeze initiator on all peers if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc(): player.rpc("play_skill_vfx", "skill_freeze") elif player.has_method("play_skill_vfx"): player.play_skill_vfx("skill_freeze") # Determine Position: if passed Sentinel, fallback to projection ahead var center_pos = target_pos if center_pos.x == -9999: var last_dir = player.movement_manager.last_move_direction if (player and player.movement_manager) else Vector2i(0, 1) center_pos = player.current_position + (last_dir * 4) # Determine Level/Radius var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1) var radius = 2 # Always 5x5 (radius 2) print("Player %s executing Area Freeze at %s (Lvl %d, Rad %d)" % [player.name, center_pos, current_lvl, radius]) # Register Zone for persistence active_freeze_zones.append({ "center": center_pos, "radius": radius, "timer": FREEZE_SLOW_DURATION }) # Initial Check (Instant Feedback) var all_players = player.get_tree().get_nodes_in_group("Players") var hit_count = 0 for p in all_players: # Check distance (Chebyshev distance for square area) var dx = abs(p.current_position.x - center_pos.x) var dy = abs(p.current_position.y - center_pos.y) # If inside square radius if dx <= radius and dy <= radius: if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: p.rpc("apply_slow_effect", 0.5) NotificationManager.send_message(p, "Caught in Freeze Zone!", NotificationManager.MessageType.WARNING) if p != player: # Don't score for freezing self (unless desired?) - Assuming enemies hit_count += 1 SfxManager.rpc("play_rpc", "freeze") if hit_count > 0 and player.is_multiplayer_authority(): var is_sng = LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO) if not is_sng: var points = hit_count * 50 var main = player.get_tree().get_root().get_node_or_null("Main") if main: var gcm = main.get_node_or_null("GoalsCycleManager") if gcm: gcm.rpc("request_add_score", points) NotificationManager.send_message(player, "Hit %d Players! +%d Pts" % [hit_count, points], NotificationManager.MessageType.GOAL) else: NotificationManager.send_message(player, "Hit %d Players!" % hit_count, NotificationManager.MessageType.GOAL) # Visual Feedback (Layer 0 - Ground Level, matching Wall logic) if player.is_multiplayer_authority(): var main_node = get_node_or_null("/root/Main") if main_node and main_node.has_method("sync_grid_items_batch"): var batch_data = [] var restoration_data = [] # Stores {pos, item} to restore later for rx in range(-radius, radius + 1): for rz in range(-radius, radius + 1): var cx = center_pos.x + rx var cz = center_pos.y + rz var pos = Vector3i(cx, 0, cz) # Get original ground item to restore later var original_ground = enhanced_gridmap.get_cell_item(pos) # Ignore if it is an immutable tile (Safe Zone 2, Wall 4, etc) if original_ground in [1, 2, 3, 4, 15, 16]: continue restoration_data.append({"pos": pos, "item": original_ground}) batch_data.append({"x": cx, "y": 0, "z": cz, "item": 5}) if not batch_data.is_empty(): main_node.rpc("sync_grid_items_batch", batch_data) # Removal timer with accurate restoration get_tree().create_timer(FREEZE_SLOW_DURATION).timeout.connect(func(): var cl_node = get_node_or_null("/root/Main") if not cl_node: return var clear_batch = [] for entry in restoration_data: var p = entry.pos # Only restore if it is STILL our Freeze tile if enhanced_gridmap.get_cell_item(p) == 5: clear_batch.append({"x": p.x, "y": 0, "z": p.z, "item": entry.item}) if not clear_batch.is_empty(): cl_node.rpc("sync_grid_items_batch", clear_batch) ) func _execute_block_floor(target_pos: Vector2i = Vector2i(-999, -999)): # VFX: show wall initiator on all peers if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc(): player.rpc("play_skill_vfx", "skill_wall") elif player.has_method("play_skill_vfx"): player.play_skill_vfx("skill_wall") # "Wall Block" - Spawn line behind player var behind_pos = target_pos var last_dir = player.movement_manager.last_move_direction if player.movement_manager else Vector2i(0, 1) if behind_pos == Vector2i(-999, -999): behind_pos = player.current_position - last_dir if not enhanced_gridmap.is_position_valid(behind_pos): return # Rollback: Should be vertical or horizontal based on direction var neighbors = [] if last_dir.x != 0: # Moving on X-axis (Columns) -> Vertical Wall (Fixed X, all Z) for z in range(enhanced_gridmap.rows): neighbors.append({"position": Vector2i(behind_pos.x, z)}) else: # Moving on Z-axis (Rows) -> Horizontal Wall (Fixed Z, all X) for x in range(enhanced_gridmap.columns): neighbors.append({"position": Vector2i(x, behind_pos.y)}) if player.is_multiplayer_authority(): var main = get_node_or_null("/root/Main") if main and main.has_method("sync_grid_items_batch"): var batch_data = [] for n in neighbors: var pos = n.position if _is_position_blocked_by_stand(pos): continue var block_pos = Vector3i(pos.x, 0, pos.y) var original_item = enhanced_gridmap.get_cell_item(block_pos) # PROTECTED FLOOR CHECK: avoid overwriting Start (1), Safe (2), Finish (3), or Wall (4) var is_immutable = false if "immutable_items" in enhanced_gridmap: if original_item in enhanced_gridmap.immutable_items: is_immutable = true if original_item in [1, 2, 3, 4, 15, 16] or is_immutable: continue batch_data.append({"x": block_pos.x, "y": 0, "z": block_pos.z, "item": 4}) # Record for restoration blocked_tiles.append({ "position": block_pos, "original_item": original_item, "timer": BLOCK_DURATION }) if not batch_data.is_empty(): main.rpc("sync_grid_items_batch", batch_data) # Notify SfxManager.rpc("play_rpc", "wall") NotificationManager.send_message(player, "Defensive Wall Deployed!", NotificationManager.MessageType.POWERUP) func _execute_invisible_mode(target: Node3D): # VFX: show ghost initiator on all peers (on the caster/player, not the target) if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc(): player.rpc("play_skill_vfx", "skill_ghost") elif player.has_method("play_skill_vfx"): player.play_skill_vfx("skill_ghost") 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 SfxManager.rpc("play_rpc", "ghost") NotificationManager.send_message(target, "Invisible Mode!", NotificationManager.MessageType.POWERUP) # ============================================================================= # Helper: Spawn Powerups (For Super Push) # ============================================================================= func spawn_powerups_around(center: Vector2i, force_powerups: bool = true, only_common: bool = false, full_density: bool = false): # "spawn / replace your nearby tiles into power up ( special tiles )" # New PowerUp Tiles are 11, 12, 13, 14 SfxManager.rpc("play_rpc", "generate_tile") var radius = 2 for x in range(-radius, radius + 1): for y in range(-radius, radius + 1): var pos = center + Vector2i(x, y) if _is_position_blocked_by_stand(pos): continue if enhanced_gridmap.is_position_valid(pos): # Random chance to spawn ANYTHING at this spot (keep density reasonable) if not full_density and rng.randf() > 0.5: continue # 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 f1 = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 1, pos.y)) # Stop n Go: Don't overwrite static powerup spawns (ID 15 floor) if LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO) and f0 == 15: continue if f0 in [4, -1] or f1 in [4, 16]: continue var item_id: int var mode = LobbyManager.get_game_mode() if only_common or LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO): # Spawn ONLY common tiles (7-10) in Stop n Go mode (User Request) item_id = rng.randi_range(7, 10) elif LobbyManager.is_game_mode(GameMode.Mode.GAUNTLET): # Gauntlet mode: No power-up tile spawns from world. # Only common tiles (7-10) spawn; Smack/Cleanser are handled separately. item_id = rng.randi_range(7, 10) else: # Other modes: 80% Chance for Common Tile (7-10), 20% for PowerUp if rng.randf() < 0.8: item_id = rng.randi_range(7, 10) else: # 20% Chance for PowerUp if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS): # Restrict to Speed (11) and Ghost (14) for Tekton Doors item_id = [11, 14].pick_random() else: item_id = rng.randi_range(11, 14) var cell = Vector3i(pos.x, 1, pos.y) # PREVENT SPAWNING ON FROZEN FLOORS (Visual/Lag Fix) var is_frozen = false if enhanced_gridmap: # Check Layer 2 for Freeze Overlay (ID 5) if enhanced_gridmap.get_cell_item(Vector3i(pos.x, 2, pos.y)) == 5: is_frozen = true if is_frozen: continue if player.is_multiplayer_authority(): var main = player.get_tree().get_root().get_node_or_null("Main") if main and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: 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 active_freeze_zones.is_empty(): var zones_to_remove = [] for i in range(active_freeze_zones.size()): var zone = active_freeze_zones[i] zone.timer -= delta # 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 via RPC only IF not already slowed to prevent network flood if "movement_manager" in p and p.movement_manager and p.movement_manager.speed_multiplier >= 1.0: 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) # This ensures slow-mo works even if zones were cast by another player. if not multiplayer.has_multiplayer_peer(): return if not player.is_multiplayer_authority(): return # Invisible Immunity (Passive) if player.is_invisible: 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 == 5: # Freeze Floor _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 Global Cooldown if global_cooldown_timer > 0: global_cooldown_timer -= delta if global_cooldown_timer <= 0: global_cooldown_timer = 0 emit_signal("global_cooldown_updated", global_cooldown_timer, GLOBAL_COOLDOWN_MAX) # Update Active Buffs (Speed) if active_buffs.has(SpecialEffect.FASTER_SPEED): active_buffs[SpecialEffect.FASTER_SPEED] -= delta if active_buffs[SpecialEffect.FASTER_SPEED] <= 0: active_buffs.erase(SpecialEffect.FASTER_SPEED) if player.movement_manager: player.movement_manager.set_speed_multiplier(1.0) # Reset NotificationManager.send_message(player, "Speed Boost Ended", NotificationManager.MessageType.NORMAL) _update_blocked_tiles(delta) _update_invisible_timer(delta) _update_freeze_zones(delta) _check_for_icy_floor() if is_targeting_mode: _update_targeting_preview() 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 == 5: # Freeze Floor 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 # Reset Visuals if player.has_method("sync_modulate"): player.rpc("sync_modulate", Color.WHITE) NotificationManager.send_message(player, "Invisibility Ended", NotificationManager.MessageType.NORMAL) # ============================================================================= # Helper Functions # ============================================================================= 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, "You are no longer frozen!", NotificationManager.MessageType.NORMAL) func _is_position_blocked_by_stand(target_pos: Vector2i) -> bool: if not player: return false # Raycast check for Static Tekton Stand # Check CENTER of tile (x+0.5) at LOW height (0.3) var from = Vector3(target_pos.x + 0.5, 0.3, target_pos.y + 0.5) + Vector3.UP * 2.0 var to = Vector3(target_pos.x + 0.5, 0.3, target_pos.y + 0.5) + Vector3.DOWN * 2.0 var query = PhysicsRayQueryParameters3D.create(from, to) query.collide_with_areas = false query.collide_with_bodies = true var space_state = player.get_world_3d().direct_space_state var result = space_state.intersect_ray(query) if result: if result.collider != player: return true return false