diff --git a/assets/graphics/touch_control/attack_mode.png b/assets/graphics/touch_control/attack_mode.png new file mode 100644 index 0000000..2c2a36d Binary files /dev/null and b/assets/graphics/touch_control/attack_mode.png differ diff --git a/assets/graphics/touch_control/attack_mode.png.import b/assets/graphics/touch_control/attack_mode.png.import new file mode 100644 index 0000000..2882e84 --- /dev/null +++ b/assets/graphics/touch_control/attack_mode.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://umw3e8nfe3vr" +path="res://.godot/imported/attack_mode.png-c8d2e720b153f717981c069694b99c1d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/graphics/touch_control/attack_mode.png" +dest_files=["res://.godot/imported/attack_mode.png-c8d2e720b153f717981c069694b99c1d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/graphics/touch_control/freeze_area.png b/assets/graphics/touch_control/freeze_area.png new file mode 100644 index 0000000..9aa6916 Binary files /dev/null and b/assets/graphics/touch_control/freeze_area.png differ diff --git a/assets/graphics/touch_control/freeze_area.png.import b/assets/graphics/touch_control/freeze_area.png.import new file mode 100644 index 0000000..86853bd --- /dev/null +++ b/assets/graphics/touch_control/freeze_area.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://3up2su2e0lfa" +path="res://.godot/imported/freeze_area.png-637e16813f4e334856ce1077dd0a8f60.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/graphics/touch_control/freeze_area.png" +dest_files=["res://.godot/imported/freeze_area.png-637e16813f4e334856ce1077dd0a8f60.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/graphics/touch_control/put_tile.png b/assets/graphics/touch_control/put_tile.png new file mode 100644 index 0000000..3b8ccff Binary files /dev/null and b/assets/graphics/touch_control/put_tile.png differ diff --git a/assets/graphics/touch_control/put_tile.png.import b/assets/graphics/touch_control/put_tile.png.import new file mode 100644 index 0000000..fe9fb0d --- /dev/null +++ b/assets/graphics/touch_control/put_tile.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://pwxo4lb87yi" +path="res://.godot/imported/put_tile.png-076fc15f3cb4549d9803338227d28dc3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/graphics/touch_control/put_tile.png" +dest_files=["res://.godot/imported/put_tile.png-076fc15f3cb4549d9803338227d28dc3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/graphics/touch_control/spawn_tile.png b/assets/graphics/touch_control/spawn_tile.png new file mode 100644 index 0000000..701e8e6 Binary files /dev/null and b/assets/graphics/touch_control/spawn_tile.png differ diff --git a/assets/graphics/touch_control/spawn_tile.png.import b/assets/graphics/touch_control/spawn_tile.png.import new file mode 100644 index 0000000..9782427 --- /dev/null +++ b/assets/graphics/touch_control/spawn_tile.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ckhdyxnho6sjp" +path="res://.godot/imported/spawn_tile.png-1538ef0a9fcda66388ef4cde6070b0fa.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/graphics/touch_control/spawn_tile.png" +dest_files=["res://.godot/imported/spawn_tile.png-1538ef0a9fcda66388ef4cde6070b0fa.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/graphics/touch_control/speed.png b/assets/graphics/touch_control/speed.png new file mode 100644 index 0000000..53e887a Binary files /dev/null and b/assets/graphics/touch_control/speed.png differ diff --git a/assets/graphics/touch_control/speed.png.import b/assets/graphics/touch_control/speed.png.import new file mode 100644 index 0000000..c1bd1b2 --- /dev/null +++ b/assets/graphics/touch_control/speed.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bsgqrjx2ity4c" +path="res://.godot/imported/speed.png-b1b011b3242f4e45ee38a2ab2d8e9378.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/graphics/touch_control/speed.png" +dest_files=["res://.godot/imported/speed.png-b1b011b3242f4e45ee38a2ab2d8e9378.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/graphics/touch_control/take_tile.png b/assets/graphics/touch_control/take_tile.png new file mode 100644 index 0000000..3fb1dc5 Binary files /dev/null and b/assets/graphics/touch_control/take_tile.png differ diff --git a/assets/graphics/touch_control/take_tile.png.import b/assets/graphics/touch_control/take_tile.png.import new file mode 100644 index 0000000..9780c81 --- /dev/null +++ b/assets/graphics/touch_control/take_tile.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ba80xnybpixw2" +path="res://.godot/imported/take_tile.png-e2d53446f322555ce2a2ebc189d0edb9.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/graphics/touch_control/take_tile.png" +dest_files=["res://.godot/imported/take_tile.png-e2d53446f322555ce2a2ebc189d0edb9.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/graphics/touch_control/wall.png b/assets/graphics/touch_control/wall.png new file mode 100644 index 0000000..3fa796f Binary files /dev/null and b/assets/graphics/touch_control/wall.png differ diff --git a/assets/graphics/touch_control/wall.png.import b/assets/graphics/touch_control/wall.png.import new file mode 100644 index 0000000..a37eb84 --- /dev/null +++ b/assets/graphics/touch_control/wall.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cupfmb5m15kmf" +path="res://.godot/imported/wall.png-3792c1a2a09d1bc9ce89d91015b9b816.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/graphics/touch_control/wall.png" +dest_files=["res://.godot/imported/wall.png-3792c1a2a09d1bc9ce89d91015b9b816.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/assets/textures/power tile_tile/freeze_area_tile.png b/assets/textures/power tile_tile/freeze_area_tile.png new file mode 100644 index 0000000..6732e0b Binary files /dev/null and b/assets/textures/power tile_tile/freeze_area_tile.png differ diff --git a/assets/textures/power tile_tile/speed_tile.png b/assets/textures/power tile_tile/speed_tile.png new file mode 100644 index 0000000..d83cb9c Binary files /dev/null and b/assets/textures/power tile_tile/speed_tile.png differ diff --git a/assets/textures/power tile_tile/wall_tile.png b/assets/textures/power tile_tile/wall_tile.png new file mode 100644 index 0000000..faf88a0 Binary files /dev/null and b/assets/textures/power tile_tile/wall_tile.png differ diff --git a/scripts/managers/playerboard_manager.gd b/scripts/managers/playerboard_manager.gd index ad2ce03..3dbf5d7 100644 --- a/scripts/managers/playerboard_manager.gd +++ b/scripts/managers/playerboard_manager.gd @@ -15,9 +15,7 @@ func initialize(p_player: Node3D, p_gridmap: Node): enhanced_gridmap = p_gridmap func _normalize_tile(tile: int) -> int: - """Convert holo tiles (11-14) to normal tiles (7-10) for goal comparison.""" - if tile >= 11 and tile <= 14: - return tile - 4 + """Normal tiles 7-10 are goals. 11-14 are powerups and not goals.""" return tile # ============================================================================= @@ -48,10 +46,15 @@ func grab_item(grid_position: Vector2i) -> bool: return false # === AUTO-ARRANGE LOGIC (Client-side pre-check) === - var target_slot = find_best_goal_slot_for_item(item) - if target_slot == -1: - print("Player: No valid slot found for item.") - return false # no space + # If item is powerup (11-14), we don't need a slot. + var is_powerup = (item >= 11 and item <= 14) + var target_slot = -1 + + if not is_powerup: + target_slot = find_best_goal_slot_for_item(item) + if target_slot == -1: + print("Player: No valid slot found for item.") + return false # no space if not player.is_multiplayer_authority(): return false @@ -64,19 +67,22 @@ func grab_item(grid_position: Vector2i) -> bool: # Apply changes locally first, server will validate/sync enhanced_gridmap.set_cell_item(cell, -1) # Remove item visually immediately - # Handle Power-Up / Holo Tiles - # Holo Matrix: 11->7 (Heart), 12->8 (Diamond), 13->9 (Star), 14->10 (Coin) + # === Power-Up Consumption (Instant Unlock) === + # IDs 11-14 are Ability Power-Ups. They are consumed on pickup, not placed on board. if item >= 11 and item <= 14: - item = item - 4 # Convert to normal tile ID - - # Check if it's a power up tile (7-10) - if item >= 7 and item <= 10: var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") if special_tiles_manager: - # Add to inventory special_tiles_manager.add_powerup_from_item(item) + + # Animation for powerup? + # ... + + # Skip adding to playerboard. Just consume. + else: + # Normal Tile: Add to playerboard + player.playerboard[target_slot] = item - player.playerboard[target_slot] = item # Add to playerboard immediately + # Update UI immediately for responsiveness # Update UI immediately for responsiveness var main = player.get_tree().get_root().get_node_or_null("Main") @@ -137,11 +143,14 @@ func _execute_grab(grid_pos: Vector2i, cell: Vector3i, item_id: int): return false # 2. Server-side Auto-Arrange - var target_slot = find_best_goal_slot_for_item(item_id) - if target_slot == -1: - print("Server: Player has no valid slot for item.") - _force_sync_to_client(cell, server_item) - return false + var is_powerup = (item_id >= 11 and item_id <= 14) + var target_slot = -1 + if not is_powerup: + target_slot = find_best_goal_slot_for_item(item_id) + if target_slot == -1: + print("Server: Player has no valid slot for item.") + _force_sync_to_client(cell, server_item) + return false # 3. Server Executes the Action @@ -149,14 +158,22 @@ func _execute_grab(grid_pos: Vector2i, cell: Vector3i, item_id: int): main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1) # 3b. Update playerboard state (on this server-side instance) - player.playerboard[target_slot] = item_id + if is_powerup: + var special_tiles_manager = player.get_node_or_null("SpecialTilesManager") + if special_tiles_manager: + special_tiles_manager.add_powerup_from_item(item_id) + # Do not add to playerboard + else: + player.playerboard[target_slot] = item_id # 3c. Broadcast the new playerboard state to all clients var peer_id = player.name.to_int() main.rpc("sync_playerboard", peer_id, player.playerboard) # 3d. Check if goal is completed (SERVER-SIDE - this triggers goal regeneration for clients!) - _check_goal_completion() + # Logic only runs if board changed, but theoretically powerup pickup shouldn't trigger goal + if not is_powerup: + _check_goal_completion() # 3e. Consume action points player.has_performed_action = true diff --git a/scripts/managers/special_tiles_manager.gd b/scripts/managers/special_tiles_manager.gd index aeba3ae..f664328 100644 --- a/scripts/managers/special_tiles_manager.gd +++ b/scripts/managers/special_tiles_manager.gd @@ -6,13 +6,26 @@ extends Node 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 + 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 FASTER_DURATION = 5.0 +const FREEZE_SLOW_DURATION = 3.0 + +signal cooldown_updated(effect: int, time_left: float, max_time: float) +signal powerup_unlocked(effect: int, level: int) + # 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)], @@ -45,12 +58,9 @@ var invisible_timer: float = 0.0 # 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.FASTER_SPEED: false, + SpecialEffect.AREA_FREEZE: false, SpecialEffect.BLOCK_FLOOR: false, - SpecialEffect.FREEZE_PLAYER: false, SpecialEffect.INVISIBLE_MODE: false } @@ -68,37 +78,55 @@ func initialize(p_player: Node3D, p_gridmap: Node): # ============================================================================= 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) + 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: + if effect == -1: return + + # Unlock or Level Up + if not inventory.get(effect, false): + # New Unlock inventory[effect] = true + powerup_levels[effect] = 1 emit_signal("inventory_updated", inventory) - print("Player %s picked up powerup for effect %s" % [player.name, SpecialEffect.keys()[effect]]) + emit_signal("powerup_unlocked", effect, 1) + print("Player %s unlocked powerup %s (Lvl 1)" % [player.name, SpecialEffect.keys()[effect]]) + else: + # Level Up + var current_lvl = powerup_levels.get(effect, 1) + if current_lvl < 8: + powerup_levels[effect] = current_lvl + 1 + emit_signal("powerup_unlocked", effect, powerup_levels[effect]) + print("Player %s leveled up %s to Lvl %d" % [player.name, SpecialEffect.keys()[effect], powerup_levels[effect]]) - if player.is_multiplayer_authority(): - rpc("sync_inventory_add", effect) + if player.is_multiplayer_authority(): + rpc("sync_inventory_add", effect, powerup_levels[effect]) @rpc("any_peer", "call_local", "reliable") -func sync_inventory_add(effect: int): +func sync_inventory_add(effect: int, level: int): inventory[effect] = true + powerup_levels[effect] = level emit_signal("inventory_updated", inventory) + emit_signal("powerup_unlocked", effect, level) func remove_powerup(effect: int): - inventory[effect] = false - emit_signal("inventory_updated", inventory) - if player.is_multiplayer_authority(): - rpc("sync_inventory_remove", effect) + # We DO NOT remove item from inventory on use anymore (for reusable leveling system)? + # Wait, user request: "cooldown is decarese and max is level 8" + # Does "activate it" consume the item? + # User says "it will cooldown agian for 15 seconds". + # Implies PERMANENT UNLOCK once picked up, reusable with cooldown. + # So we NEVER set inventory[effect] = false unless reset. + pass @rpc("any_peer", "call_local", "reliable") func sync_inventory_remove(effect: int): - inventory[effect] = false - emit_signal("inventory_updated", inventory) + # Deprecated for new system, but kept for compatibility if needed + pass # ============================================================================= # Activate Effect (Explicit Target) @@ -107,37 +135,40 @@ func sync_inventory_remove(effect: int): func activate_effect(effect: int, target_player: Node3D = null): # Validation if not inventory.get(effect, false): - return # Start/Client mismatch + print("PowerUp %s not found in inventory or false. Inventory: %s" % [effect, inventory]) + return - # Consume - remove_powerup(effect) + # Check Cooldown + if powerup_cooldowns.get(effect, 0.0) > 0: + print("PowerUp %s on cooldown." % SpecialEffect.keys()[effect]) + return + + # Calculate Cooldown based on Level + var level = powerup_levels.get(effect, 1) + # Linear Interp: Lvl 1 = 15s, Lvl 8 = 5s + # Slope = (5 - 15) / (8 - 1) = -10 / 7 = -1.428... + var cooldown_time = COOLDOWN_L1 + ((level - 1) * (COOLDOWN_L8 - COOLDOWN_L1) / 7.0) - print("[SpecialTiles] Player %s activated %s on %s" % [player.name, SpecialEffect.keys()[effect], target_player.name if target_player else "Self"]) + powerup_cooldowns[effect] = cooldown_time + emit_signal("cooldown_updated", effect, cooldown_time, cooldown_time) + + print("[SpecialTiles] Player %s activated %s (Lvl %d). Cooldown: %.1fs" % [player.name, SpecialEffect.keys()[effect], level, cooldown_time]) 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.FASTER_SPEED: + _execute_faster_speed() + SpecialEffect.AREA_FREEZE: + _execute_area_freeze() SpecialEffect.BLOCK_FLOOR: - if target_player: - _execute_block_floor(target_player) - - SpecialEffect.FREEZE_PLAYER: - _execute_freeze_zones() # No target needed anymore - + _execute_block_floor(target_player if target_player else player) # Self or Target? Usually defensive wall? "Wall Block" SpecialEffect.INVISIBLE_MODE: - # Always self - _execute_invisible_mode(player) + _execute_invisible_mode(player) # Or whatever ID 14 is # Play generic cast animation or sound? if player.is_multiplayer_authority(): player.rpc("trigger_screen_shake", "light") + # Sync cooldown to others not strictly needed unless UI shows it? + # Probably local UI only. # ============================================================================= @@ -147,144 +178,85 @@ func activate_effect(effect: int, target_player: Node3D = null): 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 +# Effect Implementations (New) # ============================================================================= -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_faster_speed(): + if player.movement_manager: + player.movement_manager.set_speed_multiplier(1.5) # 50% faster + active_buffs[SpecialEffect.FASTER_SPEED] = FASTER_DURATION + NotificationManager.send_message(player, "Speed Boost! (5s)", NotificationManager.MessageType.POWERUP) - -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 +func _execute_area_freeze(): + # "Area Freeze... slow their speed movement for 3s" + # Reuse freeze logic but simpler duration 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] + # 3x3 around player + var radius = 1 + print("Player %s executing Area Freeze" % player.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(): + # Get enemies in radius + var all_players = player.get_tree().get_nodes_in_group("Players") + for p in all_players: + if p == player: continue + var dist = Vector2(p.current_position.x - center.x, p.current_position.y - center.y).length() + if dist <= 1.5: # Adjacent or on top + p.rpc("apply_slow_effect", FREEZE_SLOW_DURATION) + + # Visual Feedback (Icy Floor for 3s?) + if player.is_multiplayer_authority(): + for x in range(-1, 2): + for y in range(-1, 2): + var pos = center + Vector2i(x, y) 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) + if main: main.rpc("sync_grid_item", pos.x, 2, pos.y, 15) # Icy decal + + # Cleanup visual timer (managed locally by author) + get_tree().create_timer(FREEZE_SLOW_DURATION).timeout.connect(func(): + for x in range(-1, 2): + for y in range(-1, 2): + var pos = center + Vector2i(x, y) + var main = player.get_tree().get_root().get_node_or_null("Main") + if main: main.rpc("sync_grid_item", pos.x, 2, pos.y, -1) + ) 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? + # Existing logic for blocking, reused + # "Wall Block" usually means block WHERE YOU ARE or FRONT? + # Original code blocked target's floor. + # If target is self, blocks self's floor? Maybe defensive. + # "Have the same cooldown as Faster" -> Just logic reuse. var center = target.current_position - var neighbors = enhanced_gridmap.get_neighbors(center, 1) # Include diagonals - neighbors.append({"position": center}) # Add center + var neighbors = enhanced_gridmap.get_neighbors(center, 1) + neighbors.append({"position": 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) + var original_item = 0 # Assume floor + # If we have logic to save original, fine, but for now just 0 blocked_tiles.append({ "position": block_pos, - "original_item": original_item if original_item != 4 else 0, # Restore to 0 (floor) if confused + "original_item": 0, "timer": BLOCK_DURATION }) - - NotificationManager.send_message(target, NotificationManager.MESSAGES.FLOOR_BLOCKED, NotificationManager.MessageType.WARNING) + NotificationManager.send_message(target, "Wall Block Created!", NotificationManager.MessageType.POWERUP) func _execute_invisible_mode(target: Node3D): + # Existing logic kept as ID 14 placeholder 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) + NotificationManager.send_message(target, "Invisible Mode!", NotificationManager.MessageType.POWERUP) + # ============================================================================= @@ -293,7 +265,7 @@ func _execute_invisible_mode(target: Node3D): 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) + # New PowerUp Tiles are 11, 12, 13, 14 var radius = 2 for x in range(-radius, radius + 1): for y in range(-radius, radius + 1): @@ -302,7 +274,7 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true): # Random chance if rng.randf() > 0.4: continue - var item_id = rng.randi_range(7, 10) # 7-10 are the special tiles + var item_id = rng.randi_range(11, 14) # 11-14 are the new powerups var cell = Vector3i(pos.x, 1, pos.y) if player.is_multiplayer_authority(): @@ -350,6 +322,29 @@ func _check_for_icy_floor(): pass func _process(delta): + # Update Cooldowns + for effect in powerup_cooldowns.keys(): + if powerup_cooldowns[effect] > 0: + powerup_cooldowns[effect] -= delta + # Emit signal occasionally or only on change? Every frame might be too much for UI? + # Optimization: Emit every 0.1s or if diff is significant? + # For snappy UI text, frame sync is okay for local player. + emit_signal("cooldown_updated", effect, powerup_cooldowns[effect], 0.0) # max unused for tick + + if powerup_cooldowns[effect] <= 0: + powerup_cooldowns[effect] = 0 + emit_signal("cooldown_updated", effect, 0, 0) + print("Cooldown finished for %s" % SpecialEffect.keys()[effect]) + + # 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) @@ -399,15 +394,6 @@ func _update_invisible_timer(delta: float): # 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): diff --git a/scripts/ui/powerup_inventory_ui.gd b/scripts/ui/powerup_inventory_ui.gd index e6eaa96..e811b6f 100644 --- a/scripts/ui/powerup_inventory_ui.gd +++ b/scripts/ui/powerup_inventory_ui.gd @@ -8,6 +8,7 @@ var selection_indicators: Dictionary = {} # { EffectEnum: Control } # Local State var selected_effect: int = -1 +var special_manager_ref: Node = null # Reference to SpecialTilesManager signal effect_selected(effect: int) @@ -22,14 +23,19 @@ func _ready(): print("PowerUpUI: Container not found") return - # SpecialEffect.BURN_TILES (0) -> Coin - _setup_icon(0, container.get_node_or_null("CoinIcon")) - # SpecialEffect.BLOCK_FLOOR (3) -> Heart - _setup_icon(3, container.get_node_or_null("HeartIcon")) - # SpecialEffect.FREEZE_PLAYER (2) -> Diamond - _setup_icon(2, container.get_node_or_null("DiamondIcon")) - # SpecialEffect.INVISIBLE_MODE (4) -> Star - _setup_icon(4, container.get_node_or_null("StarIcon")) + # ID 11 = Faster (Coin Icon?) User said: "Coin : random between two" originally, + # but now "11 (Faster)". Let's use CoinIcon for Speed? Or Star? + # User Request: "CoinIcon, HeartIcon, DiamondIcon and StarIcon" + # Let's map: + # 11 (Faster) -> CoinIcon + # 12 (Freeze) -> DiamondIcon + # 13 (Block) -> HeartIcon + # 14 (Invisible) -> StarIcon + + _setup_icon(11, container.get_node_or_null("CoinIcon")) + _setup_icon(12, container.get_node_or_null("DiamondIcon")) + _setup_icon(13, container.get_node_or_null("HeartIcon")) + _setup_icon(14, container.get_node_or_null("StarIcon")) # Note: SpecialEffect enum values from SpecialTilesManager: # BURN_TILES = 0 @@ -50,24 +56,95 @@ func _setup_icon(effect_id: int, node: Control): selection_indicators[effect_id] = select_rect select_rect.visible = false + # Add Cooldown Label if missing + if not node.has_node("CooldownLabel"): + var lbl = Label.new() + lbl.name = "CooldownLabel" + lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE # Ensure input passes to icon + # Style the label + lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + lbl.set_anchors_preset(Control.PRESET_FULL_RECT) # Cover icon + lbl.add_theme_color_override("font_shadow_color", Color.BLACK) + lbl.add_theme_constant_override("shadow_offset_x", 1) + lbl.add_theme_constant_override("shadow_offset_y", 1) + lbl.add_theme_font_size_override("font_size", 20) # Big text + lbl.text = "" + lbl.hide() + node.add_child(lbl) + # Connect click event if not node.gui_input.is_connected(_on_icon_input): node.gui_input.connect(_on_icon_input.bind(effect_id)) func _on_icon_input(event, effect_id: int): if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: - if selected_effect == effect_id: - deselect() + print("[PowerUpUI] Clicked Icon %d. Manager: %s" % [effect_id, special_manager_ref]) + # User Request: "Click the CoinIcon for activate" + # Instead of just selecting, we ACTIVATE it immediately. + if special_manager_ref: + special_manager_ref.activate_effect(effect_id) else: - select_effect(effect_id) + print("[PowerUpUI] ERROR: SpecialManagerRef is null!") + + # Visual feedback (still useful) + select_effect(effect_id) + + # Auto-deselect after short delay? + # For now, keep selection highlight as feedback of "last used" or "active"? + # Or just flash it? + + # If it's a "selection for target" ability (like Block Floor on Target?), + # we might need selection state first, then clicking target. + # But user said "CoinIcon ... activate faster speed". Speed is self-cast. + # Freeze is 3x3 self-centered. + # Invisible is self. + # Block is "Wall Block"... maybe checks target? + # SpecialTilesManager.activate_effect defaults target to self if null. + # So direct activation is safe for IsInstant effects. func setup(player_node): var special_manager = player_node.get_node_or_null("SpecialTilesManager") if special_manager: + special_manager_ref = special_manager # Store reference for activation logic if not special_manager.is_connected("inventory_updated", _on_inventory_updated): special_manager.connect("inventory_updated", _on_inventory_updated) + if not special_manager.is_connected("cooldown_updated", _on_cooldown_updated): + special_manager.connect("cooldown_updated", _on_cooldown_updated) + _on_inventory_updated(special_manager.inventory) +func _on_cooldown_updated(effect: int, time_left: float, max_time: float): + if icon_containers.has(effect): + var node = icon_containers[effect] + var lbl = node.get_node_or_null("CooldownLabel") + if lbl: + if time_left > 0: + lbl.text = "%.1fs" % time_left + lbl.show() + node.modulate = Color(0.5, 0.5, 0.5, 1.0) # Dimmed + else: + lbl.hide() + # Check inventory to restore proper modulate + var has_item = false # Need ref to inventory... or rely on next inventory update? + # Inventory update might not fire when cooldown ends. + # Let's restore bright if we have it? + # We don't have inventory reference here easily without storing it. + node.modulate = Color.WHITE # Assume we have it if we used it? + # Or better, just restore to WHITE and let inventory logic handle "not owned" graying. + # But wait, logic in _on_inventory_updated sets GRAY if not owned. + # If we set WHITE here, we might un-gray an unowned item? + # The only way cooldown runs is if we ACTIVATED it, so we HAD it. + # And we treat it as infinite consumable now. So we still have it. + pass + + # Better modulate handling: + # If cooldown > 0, DIM. + # If cooldown == 0, check owned state? + # For now, simplistic: if cooldown ends, set white. + if time_left <= 0: + node.modulate = Color.WHITE + func _on_inventory_updated(inventory: Dictionary): # Update UI icons (Dimmed vs Lit) for effect in icon_containers: