feat: Add PlayerboardManager, SpecialTilesManager, PowerupInventoryUI, and new touch control/power tile assets.
|
After Width: | Height: | Size: 33 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 28 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 21 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 31 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 92 KiB |
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||