diff --git a/assets/models/meshes/lobby.tscn b/assets/models/meshes/lobby.tscn index ad2976f..2f9b3f0 100644 --- a/assets/models/meshes/lobby.tscn +++ b/assets/models/meshes/lobby.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=7 format=3 uid="uid://b7nxt2hc4kqp8"] -[ext_resource type="Script" uid="uid://b5q6yekyk0tld" path="res://scenes/lobby.gd" id="1_lobby"] +[ext_resource type="Script" uid="uid://b5q6yekyk0tld" path="res://scenes/lobby.gd" id="1_lp6xi"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"] content_margin_left = 24.0 @@ -89,7 +89,7 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -script = ExtResource("1_lobby") +script = ExtResource("1_lp6xi") [node name="Background" type="ColorRect" parent="."] layout_mode = 1 diff --git a/scenes/player.gd b/scenes/player.gd index b6a6be7..207161c 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -6,6 +6,12 @@ var race_manager var input_manager var playerboard_manager var action_manager +var special_tiles_manager + +# Special effect states +var is_frozen: bool = false +var is_invisible: bool = false +var original_movement_range: int = 1 @export var is_bot: bool = false @@ -212,6 +218,11 @@ func _init_managers(): action_manager.name = "ActionManager" add_child(action_manager) action_manager.initialize(self, enhanced_gridmap) + + special_tiles_manager = load("res://scripts/managers/special_tiles_manager.gd").new() + special_tiles_manager.name = "SpecialTilesManager" + add_child(special_tiles_manager) + special_tiles_manager.initialize(self, enhanced_gridmap) # Add function to check if position is at finish line func is_at_finish_line() -> bool: diff --git a/scripts/managers/player_movement_manager.gd b/scripts/managers/player_movement_manager.gd index 97be5e7..238d4f5 100644 --- a/scripts/managers/player_movement_manager.gd +++ b/scripts/managers/player_movement_manager.gd @@ -36,6 +36,10 @@ func rotate_towards_target(target_pos: Vector2i): func simple_move_to(grid_position: Vector2i) -> bool: if not player.is_multiplayer_authority() or is_moving: return false + + # Check if player is frozen + if player.get("is_frozen"): + return false # Check if target is within 1-tile range var distance: int @@ -79,6 +83,10 @@ func move_to_clicked_position(grid_position: Vector2i) -> bool: if not player.is_multiplayer_authority() or is_moving or player.action_points <= 0: return false + # Check if player is frozen + if player.get("is_frozen"): + return false + # Validate grid position is within bounds if not enhanced_gridmap.is_position_valid(grid_position): return false diff --git a/scripts/managers/playerboard_manager.gd b/scripts/managers/playerboard_manager.gd index 84d58e3..8bcff87 100644 --- a/scripts/managers/playerboard_manager.gd +++ b/scripts/managers/playerboard_manager.gd @@ -51,6 +51,16 @@ func grab_item(grid_position: Vector2i) -> bool: # === Optimistic Local Update (immediate visual feedback) === # Apply changes locally first, server will validate/sync enhanced_gridmap.set_cell_item(cell, -1) # Remove item visually immediately + + # Check if grabbed item is a holo tile (11-14) and trigger special effect + var is_holo = item >= 11 and item <= 14 + if is_holo: + var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") + if special_tiles_manager: + special_tiles_manager.trigger_random_effect() + # Convert holo tile to normal tile (11->7, 12->8, 13->9, 14->10) + item = item - 4 + player.playerboard[target_slot] = item # Add to playerboard immediately # Update UI immediately for responsiveness @@ -141,6 +151,12 @@ func bot_try_grab_item() -> bool: var empty_slot = player.playerboard.find(-1) if empty_slot != -1: if player.is_multiplayer_authority(): + # Check if grabbed item is a holo tile (11-14) + if item >= 11 and item <= 14: + var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") + if special_tiles_manager: + special_tiles_manager.trigger_random_effect() + item = item - 4 # Convert to normal tile player.playerboard[empty_slot] = item player.rpc("sync_grid_item", current_cell.x, current_cell.y, current_cell.z, -1) player.rpc("sync_playerboard", player.playerboard) diff --git a/scripts/managers/special_tiles_manager.gd b/scripts/managers/special_tiles_manager.gd new file mode 100644 index 0000000..a1005f8 --- /dev/null +++ b/scripts/managers/special_tiles_manager.gd @@ -0,0 +1,294 @@ +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 diff --git a/scripts/managers/special_tiles_manager.gd.uid b/scripts/managers/special_tiles_manager.gd.uid new file mode 100644 index 0000000..6bed405 --- /dev/null +++ b/scripts/managers/special_tiles_manager.gd.uid @@ -0,0 +1 @@ +uid://dw1euu2uxkggg