This commit is contained in:
2026-01-29 03:04:24 +08:00
parent 6949e20a1f
commit e66ba7542c
12 changed files with 687 additions and 549 deletions
+6 -76
View File
@@ -11,40 +11,26 @@ config_version=5
[application] [application]
config/name="tekton-local" config/name="tekton-local"
run/main_scene="res://scenes/lobby.tscn" run/main_scene="res://scenes/main_scene.tscn"
config/features=PackedStringArray("4.5", "Forward Plus") config/features=PackedStringArray("4.5", "Forward Plus")
config/icon="res://icon.svg" config/icon="res://icon.svg"
[autoload] [autoload]
NakamaManager="*res://scripts/nakama_manager.gd" BeehaveGlobalMetrics="*res://addons/beehave/metrics/beehave_global_metrics.gd"
NetworkManager="*res://scripts/network_manager.gd" BeehaveGlobalDebugger="*res://addons/beehave/debug/global_debugger.gd"
Nakama="*res://addons/com.heroiclabs.nakama/Nakama.gd"
PlayerManager="*res://scripts/managers/player_manager.gd"
TurnManager="*res://scripts/managers/turn_manager.gd"
GoalManager="*res://scripts/managers/goal_manager.gd"
GameStateManager="*res://scripts/managers/game_state_manager.gd"
LobbyManager="*res://scripts/managers/lobby_manager.gd"
GameUpdateManager="*res://scripts/managers/game_update_manager.gd"
AuthManager="*res://scripts/managers/auth_manager.gd"
UserProfileManager="*res://scripts/managers/user_profile_manager.gd"
Satori="*res://addons/com.heroiclabs.nakama/Satori.gd"
[display] [display]
window/size/viewport_width=1366 window/size/viewport_width=1366
window/size/viewport_height=720 window/size/viewport_height=720
window/size/window_width_override=1280 window/size/window_width_override=1024
window/size/window_height_override=720 window/size/window_height_override=576
window/stretch/mode="viewport" window/stretch/mode="viewport"
[editor]
run/main_run_args="--touch"
[editor_plugins] [editor_plugins]
enabled=PackedStringArray("res://addons/enhanced_gridmap/plugin.cfg", "res://addons/com.heroiclabs.nakama/plugin.cfg") enabled=PackedStringArray("res://addons/enhanced_gridmap/plugin.cfg")
[input] [input]
@@ -58,59 +44,3 @@ put_item={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
] ]
} }
move_northwest={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"key_label":0,"unicode":81,"location":0,"echo":false,"script":null)
]
}
move_north={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":87,"location":0,"echo":false,"script":null)
]
}
move_northeast={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
]
}
move_west={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
]
}
move_east={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
]
}
move_southwest={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":90,"key_label":0,"unicode":122,"location":0,"echo":false,"script":null)
]
}
move_south={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":88,"key_label":0,"unicode":120,"location":0,"echo":false,"script":null)
]
}
move_southeast={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null)
]
}
action_grab={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
]
}
action_put={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":83,"location":0,"echo":false,"script":null)
]
}
use_powerup={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":70,"location":0,"echo":false,"script":null)
]
}
+96 -1
View File
File diff suppressed because one or more lines are too long
+90 -12
View File
@@ -20,6 +20,15 @@ var is_frozen: bool = false
var is_invisible: bool = false var is_invisible: bool = false
var original_movement_range: int = 1 var original_movement_range: int = 1
var is_attack_mode: bool = false:
set(value):
is_attack_mode = value
# Visual feedback for attack mode (Red Tint)
if is_attack_mode:
_apply_tint_recursive(self, Color(1.0, 0.5, 0.5))
else:
_apply_tint_recursive(self, Color.WHITE)
@export var is_bot: bool = false @export var is_bot: bool = false
@export var enhanced_gridmap_path: NodePath = "/root/Main/EnhancedGridMap" @export var enhanced_gridmap_path: NodePath = "/root/Main/EnhancedGridMap"
@@ -668,11 +677,31 @@ func drop_random_item():
rpc("display_message", "Dropped item!", 4) rpc("display_message", "Dropped item!", 4)
print("Player %s dropped item %d at %s" % [name, item_id, drop_pos]) print("Player %s dropped item %d at %s" % [name, item_id, drop_pos])
func playerboard_is_empty() -> bool:
for item in playerboard: @rpc("any_peer", "call_local")
if item != -1: func drop_all_tiles():
return false """Drops all tiles from playerboard. Used by Super Push."""
return true var dropped_count = 0
for i in range(playerboard.size()):
if playerboard[i] != -1:
var item_id = playerboard[i]
playerboard[i] = -1
# Find a spot to drop? Or just destroy?
# User: "drop all tiles on playerboard" -> usually implies they scatter on floor
# But scattering 25 tiles is chaotic.
# Let's drop a few random ones and destroy rest?
# Or per user "spawn nearby tiles into power up" -> Maybe the dropped tiles BECOME powerups?
# User request: "victim drops all tiles... and spawn / replace your nearby tiles into power up"
# I will just clear the board here. Spawning is handled by spawn_powerups_around.
dropped_count += 1
if dropped_count > 0:
rpc("sync_playerboard", playerboard)
rpc("trigger_screen_shake", "targeted")
rpc("display_message", "CRITICALLY HIT!", 4)
print("Player %s dropped %d tiles due to Super Push" % [name, dropped_count])
func _find_valid_drop_position() -> Vector2i: func _find_valid_drop_position() -> Vector2i:
# Try random adjacent cells # Try random adjacent cells
@@ -681,16 +710,65 @@ func _find_valid_drop_position() -> Vector2i:
for neighbor in neighbors: for neighbor in neighbors:
var pos = neighbor.position var pos = neighbor.position
if enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y)) == -1: # Empty floor? No, 0 is floor. -1 is void? # Check item layer
# Wait, items are on layer 1 usually? var item_cell = Vector3i(pos.x, 1, pos.y)
# Check logic: grab_item uses y=1? if enhanced_gridmap.get_cell_item(item_cell) == -1:
var item_cell = Vector3i(pos.x, 1, pos.y) if not is_position_occupied(pos):
if enhanced_gridmap.get_cell_item(item_cell) == -1: return pos
if not is_position_occupied(pos):
return pos
return Vector2i(-1, -1) return Vector2i(-1, -1)
# =============================================================================
# Targeting Action
# =============================================================================
func attempt_target_action(target_index: int):
# 1. Get Selected Effect from UI
var main = get_tree().get_root().get_node_or_null("Main")
if not main or not main.ui_manager:
return
# Quick check if UI exists and has a selection
# We need to access the script variable 'selected_effect' from the dynamically created/loaded UI.
# Let's assume ui_manager has a reference or we find it.
# Based on previous step, we haven't integrated PowerUpInventoryUI into UIManager yet.
# So we might fail here if not wired up.
# For now, let's look for "PowerUpInventoryUI" in CanvasLayer.
var inventory_ui = main.ui_manager.get_node_or_null("PowerUpInventoryUI")
# Or check if ui_manager tracks it.
# Note: We haven't instantiated it yet in UIManager. We will need to do that.
if not inventory_ui or inventory_ui.selected_effect == -1:
return # No effect selected using mouse/touch first
var effect = inventory_ui.selected_effect
# 2. Find Target Player
var player_manager = main.get_node_or_null("PlayerManager")
if not player_manager:
return
if target_index < 0 or target_index >= player_manager.connected_peer_ids.size():
return
var target_id = player_manager.connected_peer_ids[target_index]
var target_player = main.get_node_or_null(str(target_id))
if not target_player:
return
if target_player == self and effect != 4: # 4 = INVISIBLE (Self)
# Trying to target self with harmful effect?
rpc("display_message", "Can't target self!", 3)
return
# 3. Activate Effect
var st_manager = get_node_or_null("SpecialTilesManager")
if st_manager:
st_manager.activate_effect(effect, target_player)
inventory_ui.deselect()
func _process(delta): func _process(delta):
if is_multiplayer_authority(): if is_multiplayer_authority():
+64 -1
View File
@@ -1,9 +1,13 @@
extends Node extends Node
# GoalManager - Manages goal generation and synchronization # GoalManager - Manages goal generation, synchronization, and speed tracking
var preset_goals: Array = [] var preset_goals: Array = []
# Speed Tracking
var player_completion_times: Dictionary = {} # { player_id: [time_taken, time_taken, ...] }
var player_start_times: Dictionary = {} # { player_id: timestamp_msec }
func initialize_random_goals(size: int, min_value: int, max_value: int, null_count: float) -> Array: func initialize_random_goals(size: int, min_value: int, max_value: int, null_count: float) -> Array:
var goals = [] var goals = []
var rng = RandomNumberGenerator.new() var rng = RandomNumberGenerator.new()
@@ -38,3 +42,62 @@ func get_goals_for_player(player_index: int) -> Array:
if player_index >= 0 and player_index < preset_goals.size(): if player_index >= 0 and player_index < preset_goals.size():
return preset_goals[player_index].duplicate() return preset_goals[player_index].duplicate()
return [] return []
# =============================================================================
# Speed Tracking Logic
# =============================================================================
func mark_goal_start(player_id: int):
player_start_times[player_id] = Time.get_ticks_msec()
func mark_goal_complete(player_id: int):
if not player_start_times.has(player_id):
return
var duration_sec = (Time.get_ticks_msec() - player_start_times[player_id]) / 1000.0
if not player_completion_times.has(player_id):
player_completion_times[player_id] = []
player_completion_times[player_id].append(duration_sec)
# Reset start time for next goal
player_start_times[player_id] = Time.get_ticks_msec()
# print("Player %s completed goal in %.2fs" % [player_id, duration_sec])
func get_player_average_time(player_id: int) -> float:
if not player_completion_times.has(player_id) or player_completion_times[player_id].is_empty():
return 10.0 # Default baseline (10 seconds)
var total = 0.0
for t in player_completion_times[player_id]:
total += t
return total / player_completion_times[player_id].size()
func get_global_average_time() -> float:
var total_avg = 0.0
var count = 0
for pid in player_completion_times:
var p_avg = get_player_average_time(pid)
total_avg += p_avg
count += 1
if count == 0:
return 10.0
return total_avg / count
func get_boost_multiplier(player_id: int) -> float:
var p_avg = get_player_average_time(player_id)
var g_avg = get_global_average_time()
if p_avg > g_avg:
# Player is slower than average -> Boost fills faster
# Scale up to 1.5x based on how much slower (capped)
var ratio = p_avg / max(g_avg, 0.1)
return min(ratio, 1.5)
else:
# Player is faster than average -> Boost fills slower
# Scale down to 0.8x
return 0.8
+10
View File
@@ -46,6 +46,16 @@ func _process(delta):
if target_position != player.current_position: if target_position != player.current_position:
movement_manager.simple_move_to(target_position) movement_manager.simple_move_to(target_position)
# Targeting Inputs (1, 2, 3, 4)
if Input.is_key_pressed(KEY_1):
player.attempt_target_action(0)
elif Input.is_key_pressed(KEY_2):
player.attempt_target_action(1)
elif Input.is_key_pressed(KEY_3):
player.attempt_target_action(2)
elif Input.is_key_pressed(KEY_4):
player.attempt_target_action(3)
func handle_unhandled_input(event): func handle_unhandled_input(event):
# Early return if not authorized human player # Early return if not authorized human player
if not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots"): if not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots"):
+37 -83
View File
@@ -97,94 +97,48 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
var other_player = player.get_player_at_position(target_pos) var other_player = player.get_player_at_position(target_pos)
if not other_player: if not other_player:
return false # Should be occupied if we called this, but safety check return false
# Calculate where they will be pushed to # === NEW LOGIC: Only allow push if in ATTACK MODE ===
var pushed_to_pos = target_pos + direction if not player.get("is_attack_mode"):
# Standard bumping effect or nothing?
# Check if pushed destination is valid # User said "Remove standard push", so we just do nothing or small shake
if not enhanced_gridmap.is_position_valid(pushed_to_pos):
# Blocked by world bounds -> Double Push!
other_player.rpc("apply_stagger", 1.5)
return false
# Check walkability of pushed destination
var cell_item = enhanced_gridmap.get_cell_item(Vector3i(pushed_to_pos.x, 0, pushed_to_pos.y))
if cell_item == -1 or cell_item in enhanced_gridmap.non_walkable_items:
# Blocked by obstacle -> Double Push!
other_player.rpc("apply_stagger", 1.5)
return false
# Check if pushed destination is ALREADY occupied (Double Push / Crush)
if player.is_position_occupied(pushed_to_pos):
# Blocked by another player -> Double Push!
other_player.rpc("apply_stagger", 1.5)
return false
# Check if other player is currently moving (don't push moving players to avoid sync issues)
if other_player.is_player_moving:
return false return false
# EXECUTE PUSH # === SUPER PUSH (Attack Mode) ===
print("Player %s PUSHING %s to %s" % [player.name, other_player.name, pushed_to_pos]) print("Player %s SUPER PUSHING %s!" % [player.name, other_player.name])
# Force move the other player # 1. Drop Victim's Tiles
# We use rpc to sync this change. Since we are authority, we can dictate pos. if other_player.has_method("drop_all_tiles"):
other_player.rpc("drop_all_tiles") # Sync drop
# 1. Update their position variable immediately so our move check passes next frame? # 2. Spawn PowerUps around Victim
# actually simple_move_to continues immediately. # We delegate this to the attacker's SpecialTilesManager to handle the spawning authority
if player.special_tiles_manager and player.special_tiles_manager.has_method("spawn_powerups_around"):
# We need to forcefully animate/move them. player.special_tiles_manager.spawn_powerups_around(other_player.current_position)
# Let's use their own movement RPC if possible, or a special push RPC.
# For simplicity, we'll assume they snap-move or use the standard path movement but faster? # 3. Knockback / Stagger
# Let's use the standard movement for now to ensure consistency. # Push them away
var pushed_to_pos = target_pos + direction
var path = [Vector2(target_pos.x, target_pos.y), Vector2(pushed_to_pos.x, pushed_to_pos.y)] if enhanced_gridmap.is_position_valid(pushed_to_pos) and \
# path.pop_front() # start_movement_along_path expects full path? enhanced_gridmap.get_cell_item(Vector3i(pushed_to_pos.x, 0, pushed_to_pos.y)) != -1 and \
# Actually start_movement_along_path usually takes [start, end] or [end]? not player.is_position_occupied(pushed_to_pos):
# Looking at simple_move_to above:
# path = [current, target] # Valid push
# path.pop_front() -> path is [target] var push_path = [Vector2(pushed_to_pos.x, pushed_to_pos.y)]
# So it expects just the waypoints excluding start. other_player.rpc("start_movement_along_path", push_path, false)
other_player.target_position = pushed_to_pos # Logical update
var push_path = [Vector2(pushed_to_pos.x, pushed_to_pos.y)]
else:
# Call RPC on the OTHER player # Wall/Blocked -> Stagger in place
other_player.rpc("start_movement_along_path", push_path, false) # false = no cam checks? other_player.rpc("apply_stagger", 1.5)
# Wait, the boolean arg is "is_local_human"?
# In player.gd call: player.rpc("start_movement_along_path", path, not (player.is_bot...)) # 4. Consume Boost
if player.powerup_manager:
# Update their current_position immediately so `is_position_occupied` returns false for us player.powerup_manager.reset_boost()
# (The RPC handles the visual tween, but we need logical update)
# However, start_movement_along_path usually updates current_position at start or end? # 5. Reset Attack Mode
# If we don't update it now, our `player.is_position_occupied(grid_position)` check in subsequent frames might be ok, player.is_attack_mode = false
# but `simple_move_to` is synchronous-ish.
# Crucial: We need to make sure `player.is_position_occupied` returns false for `grid_position`
# RIGHT NOW so we can return true and move into it.
# But `is_position_occupied` checks `current_position` and `target_position`.
# So we need to update `other_player`'s state.
other_player.target_position = pushed_to_pos
other_player.is_player_moving = true # Mark them as moving so they occupy the NEW spot vs OLD spot?
# Actually is_position_occupied checks BOTH current and target.
# So if they are moving, they occupy BOTH until finished?
# See player.gd:
# if player.is_player_moving and player.target_position == pos: return true
# if player.current_position == pos: return true
# This implies a moving player blocks 2 tiles.
# If we want to move into `target_pos`, `other_player` must NOT count as occupying it.
# But they ARE at `target_pos`.
# We physically can't be at the same spot.
# So we essentially need them to VACATE `target_pos` logically.
# Hack/Fix: We manually update their `current_position` to `pushed_to_pos` immediately?
# No, that breaks visual interpolation.
# Solution: The push logic implies simultaneity or high speed.
# If we assume the push is instant-ish logic:
# We update valid logical positions. Visuals catch up.
return true return true
+21 -15
View File
@@ -64,15 +64,17 @@ func grab_item(grid_position: Vector2i) -> bool:
# Apply changes locally first, server will validate/sync # Apply changes locally first, server will validate/sync
enhanced_gridmap.set_cell_item(cell, -1) # Remove item visually immediately enhanced_gridmap.set_cell_item(cell, -1) # Remove item visually immediately
# Check if grabbed item is a holo tile (11-14) - add to powerup instead of triggering effect # Handle Power-Up / Holo Tiles
var is_holo = item >= 11 and item <= 14 # Holo Matrix: 11->7 (Heart), 12->8 (Diamond), 13->9 (Star), 14->10 (Coin)
if is_holo: if item >= 11 and item <= 14:
# Add holo pickup to power-up manager (4 pickups = 1 bar) item = item - 4 # Convert to normal tile ID
var powerup_manager = player.get_node_or_null("PowerUpManager")
if powerup_manager: # Check if it's a power up tile (7-10)
powerup_manager.add_holo_pickup() if item >= 7 and item <= 10:
# Convert holo tile to normal tile (11->7, 12->8, 13->9, 14->10) var special_tiles_manager = player.get_node_or_null("SpecialTilesManager")
item = item - 4 if special_tiles_manager:
# Add to inventory
special_tiles_manager.add_powerup_from_item(item)
player.playerboard[target_slot] = item # Add to playerboard immediately player.playerboard[target_slot] = item # Add to playerboard immediately
@@ -83,7 +85,7 @@ func grab_item(grid_position: Vector2i) -> bool:
# Check if goal is completed after grabbing # Check if goal is completed after grabbing
_check_goal_completion() _check_goal_completion()
# === Server Sync === # === Server Sync ===
if multiplayer.is_server(): if multiplayer.is_server():
# HOST/SERVER: Broadcast to all clients # HOST/SERVER: Broadcast to all clients
@@ -207,12 +209,16 @@ func bot_try_grab_item() -> bool:
var empty_slot = player.playerboard.find(-1) var empty_slot = player.playerboard.find(-1)
if empty_slot != -1: if empty_slot != -1:
if player.is_multiplayer_authority(): if player.is_multiplayer_authority():
# Check if grabbed item is a holo tile (11-14) - add to powerup # Convert Holo (11-14)
if item >= 11 and item <= 14: if item >= 11 and item <= 14:
var powerup_manager = player.get_node_or_null("PowerUpManager") item = item - 4
if powerup_manager:
powerup_manager.add_holo_pickup() # Inventory Add
item = item - 4 # Convert to normal tile if item >= 7 and item <= 10:
var special_tiles_manager = player.get_node_or_null("SpecialTilesManager")
if special_tiles_manager:
special_tiles_manager.add_powerup_from_item(item)
player.playerboard[empty_slot] = item player.playerboard[empty_slot] = item
player.rpc("sync_grid_item", current_cell.x, current_cell.y, current_cell.z, -1) player.rpc("sync_grid_item", current_cell.x, current_cell.y, current_cell.z, -1)
player.rpc("sync_playerboard", player.playerboard) player.rpc("sync_playerboard", player.playerboard)
+55 -121
View File
@@ -1,161 +1,95 @@
extends Node extends Node
# PowerUpManager - Handles power-up points, holo tile tracking, and special effect usage # PowerUpManager - Handles Boost Meter (Time-based logic)
# Note: Inventory logic is now in SpecialTilesManager + PlayerboardManager
const MAX_POINTS: int = 12 const MAX_BOOST: float = 100.0
const POINTS_PER_BAR: int = 4 const BASE_FILL_RATE: float = 4.0 # 4 points per second baseline (25s to full)
const MAX_BARS: int = 4
const HOLO_PICKUPS_PER_BAR: int = 4
const SPECIAL_COOLDOWN: float = 4.0 # 4 second cooldown
var player: Node3D var player: Node3D
var enhanced_gridmap: Node var enhanced_gridmap: Node
var goal_manager: Node
# Power-up state # Boost State
var current_points: int = 0 var current_boost: float = 0.0
var holo_pickup_count: int = 0
var special_cooldown_timer: float = 0.0 # Current cooldown remaining
signal points_changed(current: int, max_points: int) signal points_changed(current: int, max_points: int) # Reused for UI (int casting)
signal bar_filled() signal bar_filled()
signal effect_used() signal boost_reset()
func initialize(p_player: Node3D, p_gridmap: Node): func initialize(p_player: Node3D, p_gridmap: Node):
player = p_player player = p_player
enhanced_gridmap = p_gridmap enhanced_gridmap = p_gridmap
# Find GoalManager
var main = player.get_tree().get_root().get_node_or_null("Main")
if main:
goal_manager = main.get_node_or_null("GoalManager")
set_process(true) set_process(true)
func _process(delta): func _process(delta):
# Update cooldown timer if not is_instance_valid(player) or not player.is_multiplayer_authority():
if special_cooldown_timer > 0: return
special_cooldown_timer -= delta
# Only fill if not full
if current_boost < MAX_BOOST:
var multiplier = 1.0
if goal_manager:
# Use authority ID for lookup
multiplier = goal_manager.get_boost_multiplier(player.get_multiplayer_authority())
current_boost += BASE_FILL_RATE * multiplier * delta
current_boost = min(current_boost, MAX_BOOST)
# Update UI (Cast to int for compatibility with existing UI slider/bar)
emit_signal("points_changed", int(current_boost), int(MAX_BOOST))
if current_boost >= MAX_BOOST:
_on_boost_full()
# ============================================================================= func _on_boost_full():
# Holo Tile Pickup player.is_attack_mode = true
# =============================================================================
func add_holo_pickup():
"""Called when player picks up a holo tile (11-14)."""
holo_pickup_count += 1
if holo_pickup_count >= HOLO_PICKUPS_PER_BAR:
holo_pickup_count = 0
_add_bar()
print("[PowerUp] Player %s picked up holo tile. Count: %d/4" % [player.name, holo_pickup_count])
if player.is_multiplayer_authority():
rpc("sync_holo_count", holo_pickup_count, current_points)
func _add_bar():
"""Add one full bar (4 points) of power-up."""
var points_to_add = POINTS_PER_BAR
current_points = min(current_points + points_to_add, MAX_POINTS)
emit_signal("bar_filled") emit_signal("bar_filled")
emit_signal("points_changed", current_points, MAX_POINTS) player.rpc("display_message", "ATTACK MODE READY!", 1)
print("[PowerUp] Player %s Boost Full! Entering Attack Mode." % player.name)
# Type 1 = POWERUP message for special styling
player.rpc("display_message", "Power-up bar filled!", 1)
print("[PowerUp] Player %s gained 1 bar! Total: %d/%d points" % [player.name, current_points, MAX_POINTS])
if player.is_multiplayer_authority(): if player.is_multiplayer_authority():
player.get_node("PowerUpManager").rpc("sync_points", current_points) rpc("sync_boost", current_boost)
# ============================================================================= func reset_boost():
# Goal Completion Reward current_boost = 0.0
# ============================================================================= player.is_attack_mode = false
emit_signal("points_changed", 0, int(MAX_BOOST))
func acquire_smash_bonus(): emit_signal("boost_reset")
"""Called when player is smashed. Grants 1 bar up to a max of 2 bars."""
if get_bars() < 2:
_add_bar()
print("[PowerUp] Player %s gained smash bonus bar! Total: %d/%d" % [player.name, current_points, MAX_POINTS])
else:
print("[PowerUp] Player %s smash bonus capped (already has >= 2 bars)" % player.name)
func add_goal_completion_reward():
"""Called when player completes a goal pattern. Awards 1 bar."""
_add_bar()
print("[PowerUp] Player %s completed goal - awarded 1 bar" % [player.name])
# =============================================================================
# Using Special Effects
# =============================================================================
func can_use_special() -> bool:
"""Returns true if player has at least 1 bar (4 points) AND IS NOT ON COOLDOWN."""
return current_points >= POINTS_PER_BAR and special_cooldown_timer <= 0
func get_bars() -> int:
"""Returns current number of full bars."""
return current_points / POINTS_PER_BAR
func use_special_effect():
"""Consume 1 bar and trigger a random special effect."""
if not can_use_special():
# Type 3 = WARNING message
player.rpc("display_message", "Not enough power-up!", 3)
return false
# Check cooldown
if special_cooldown_timer > 0:
player.rpc("display_message", "Special on cooldown! (%.1fs)" % special_cooldown_timer, 3)
return false
# Consume 1 bar
current_points -= POINTS_PER_BAR
emit_signal("effect_used")
emit_signal("points_changed", current_points, MAX_POINTS)
# Start cooldown
special_cooldown_timer = SPECIAL_COOLDOWN
# Play special animation (backflip) - synced across network
if player.is_multiplayer_authority() and player.has_method("sync_special_animation"):
player.rpc("sync_special_animation")
# Trigger random special effect via SpecialTilesManager
var special_tiles_manager = player.get_node_or_null("SpecialTilesManager")
if special_tiles_manager:
special_tiles_manager.trigger_random_effect()
print("[PowerUp] Player %s used special effect! Remaining: %d/%d points, Cooldown: %.1fs" % [player.name, current_points, MAX_POINTS, SPECIAL_COOLDOWN])
if player.is_multiplayer_authority(): if player.is_multiplayer_authority():
rpc("sync_points", current_points) rpc("sync_boost", 0.0)
return true
# ============================================================================= # =============================================================================
# Sync # Sync
# ============================================================================= # =============================================================================
@rpc("any_peer", "call_local", "reliable") @rpc("any_peer", "call_local", "reliable")
func sync_holo_count(count: int, points: int): func sync_boost(value: float):
holo_pickup_count = count current_boost = value
current_points = points emit_signal("points_changed", int(current_boost), int(MAX_BOOST))
emit_signal("points_changed", current_points, MAX_POINTS)
# Client-side Attack Mode visual check (?)
@rpc("any_peer", "call_local", "reliable") if current_boost >= MAX_BOOST:
func sync_points(points: int): # Could trigger visual effect here
current_points = points pass
emit_signal("points_changed", current_points, MAX_POINTS)
# ============================================================================= # =============================================================================
# Getters # Getters
# ============================================================================= # =============================================================================
func get_points() -> int: func get_points() -> int:
return current_points return int(current_boost)
func get_max_points() -> int: func get_max_points() -> int:
return MAX_POINTS return int(MAX_BOOST)
func get_fill_percentage() -> float: func get_fill_percentage() -> float:
return float(current_points) / float(MAX_POINTS) return current_boost / MAX_BOOST
func reset():
current_points = 0
holo_pickup_count = 0
emit_signal("points_changed", current_points, MAX_POINTS)
+188 -229
View File
@@ -39,6 +39,21 @@ const INVISIBLE_DURATION = 6.0
var blocked_tiles: Array[Dictionary] = [] # {position: Vector3i, original_item: int, timer: float} var blocked_tiles: Array[Dictionary] = [] # {position: Vector3i, original_item: int, timer: float}
var invisible_timer: float = 0.0 var invisible_timer: float = 0.0
# INVENTORY SYSTEM
# Stores count of each power-up type. Max 1 per type as per user request?
# "player can store 1 of each different power up"
var inventory = {
SpecialEffect.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.BLOCK_FLOOR: false,
SpecialEffect.FREEZE_PLAYER: false,
SpecialEffect.INVISIBLE_MODE: false
}
# Signal for UI
signal inventory_updated(inventory_data: Dictionary)
func initialize(p_player: Node3D, p_gridmap: Node): func initialize(p_player: Node3D, p_gridmap: Node):
player = p_player player = p_player
@@ -46,6 +61,83 @@ func initialize(p_player: Node3D, p_gridmap: Node):
rng = RandomNumberGenerator.new() rng = RandomNumberGenerator.new()
rng.randomize() rng.randomize()
# =============================================================================
# Helper: Item ID to Effect Enum
# =============================================================================
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)
_: return -1
func add_powerup_from_item(item_id: int):
var effect = get_effect_from_item(item_id)
if effect != -1:
inventory[effect] = true
emit_signal("inventory_updated", inventory)
print("Player %s picked up powerup for effect %s" % [player.name, SpecialEffect.keys()[effect]])
if player.is_multiplayer_authority():
rpc("sync_inventory_add", effect)
@rpc("any_peer", "call_local", "reliable")
func sync_inventory_add(effect: int):
inventory[effect] = true
emit_signal("inventory_updated", inventory)
func remove_powerup(effect: int):
inventory[effect] = false
emit_signal("inventory_updated", inventory)
if player.is_multiplayer_authority():
rpc("sync_inventory_remove", effect)
@rpc("any_peer", "call_local", "reliable")
func sync_inventory_remove(effect: int):
inventory[effect] = false
emit_signal("inventory_updated", inventory)
# =============================================================================
# Activate Effect (Explicit Target)
# =============================================================================
func activate_effect(effect: int, target_player: Node3D = null):
# Validation
if not inventory.get(effect, false):
return # Start/Client mismatch
# Consume
remove_powerup(effect)
print("[SpecialTiles] Player %s activated %s on %s" % [player.name, SpecialEffect.keys()[effect], target_player.name if target_player else "Self"])
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.BLOCK_FLOOR:
if target_player:
_execute_block_floor(target_player)
SpecialEffect.FREEZE_PLAYER:
if target_player:
_execute_freeze_player(target_player)
SpecialEffect.INVISIBLE_MODE:
# Always self
_execute_invisible_mode(player)
# Play generic cast animation or sound?
if player.is_multiplayer_authority():
player.rpc("trigger_screen_shake", "light")
# ============================================================================= # =============================================================================
# Check if item is a holo tile # Check if item is a holo tile
@@ -65,15 +157,15 @@ func trigger_random_effect():
match effect: match effect:
SpecialEffect.BURN_TILES: SpecialEffect.BURN_TILES:
_execute_burn_tiles() _execute_burn_tiles(_get_random_opponent())
SpecialEffect.SPAWN_TILES: SpecialEffect.SPAWN_TILES:
_execute_spawn_tiles() _execute_spawn_tiles(player)
SpecialEffect.FREEZE_PLAYER: SpecialEffect.FREEZE_PLAYER:
_execute_freeze_player() _execute_freeze_player(_get_random_opponent())
SpecialEffect.BLOCK_FLOOR: SpecialEffect.BLOCK_FLOOR:
_execute_block_floor() _execute_block_floor(player)
SpecialEffect.INVISIBLE_MODE: SpecialEffect.INVISIBLE_MODE:
_execute_invisible_mode() _execute_invisible_mode(player)
# Sync effect to all clients # Sync effect to all clients
if player.is_multiplayer_authority(): if player.is_multiplayer_authority():
@@ -87,262 +179,122 @@ func sync_effect_triggered(effect: int):
# Effect Implementations # Effect Implementations
# ============================================================================= # =============================================================================
func _execute_burn_tiles(): func _execute_burn_tiles(target: Node3D):
# NEW LOGIC: Put back random target tiles from their playerboard to their position nearest if not target: return
# Find random opponent
var opponent = _get_random_opponent()
if not opponent:
print("[SpecialTiles] No opponent found for BURN_TILES")
return
# Get opponent's playerboard items # Knock tiles from target's board
var board_indices = [] var board_indices = []
for i in range(opponent.playerboard.size()): for i in range(target.playerboard.size()):
if opponent.playerboard[i] != -1: if target.playerboard[i] != -1:
board_indices.append(i) board_indices.append(i)
if board_indices.is_empty(): if board_indices.is_empty():
return # Nothing to burn return
# Pick random 1x (3x3 equivalent = ~3-4 tiles) or 2x amount
# Let's say we burn 3 to 6 tiles
var burn_count = rng.randi_range(3, 6) var burn_count = rng.randi_range(3, 6)
board_indices.shuffle() board_indices.shuffle()
var tiles_burned = 0 # Drop logic similar to burn but we just destroy them or scatter?
# "BURN_TILES, # Remove 3x3 pattern tiles" -> User request says Remove pattern.
# Get valid empty spots near opponent to dump tiles # Original code did burn.
var empty_spots = _get_empty_neighbors_recursive(opponent.current_position, 2) # Let's just remove them.
empty_spots.shuffle()
for i in range(min(burn_count, board_indices.size())): for i in range(min(burn_count, board_indices.size())):
var slot_idx = board_indices[i] target.playerboard[board_indices[i]] = -1
var item = opponent.playerboard[slot_idx]
# Remove from opponent board if player.is_multiplayer_authority():
opponent.playerboard[slot_idx] = -1 # Sync the change on the target
var main = player.get_tree().get_root().get_node_or_null("Main")
# Determine where to put it if main:
var target_pos = Vector3i.ZERO main.rpc("sync_playerboard", target.name.to_int(), target.playerboard)
var target_item = item
if not empty_spots.is_empty():
# Place on empty spot
var pos_2d = empty_spots.pop_back()
target_pos = Vector3i(pos_2d.x, 1, pos_2d.y)
else:
# No empty spots? "Replace it with new one" at a random nearby non-empty spot?
# Or just find ANY nearby spot and overwrite
var neighbors = enhanced_gridmap.get_neighbors(opponent.current_position, 1)
if not neighbors.is_empty():
var rand_n = neighbors[rng.randi() % neighbors.size()]
target_pos = Vector3i(rand_n.position.x, 1, rand_n.position.y)
# If we are overwriting or essentially "spawning" a new one to replace it
target_item = rng.randi_range(7, 10) # As per request "replace it with new one" if floor not empty
if target_pos != Vector3i.ZERO:
if player.is_multiplayer_authority():
var main = player.get_tree().get_root().get_node_or_null("Main")
if main:
main.rpc("sync_grid_item", target_pos.x, target_pos.y, target_pos.z, target_item)
# Sync opponent board change
main.rpc("sync_playerboard", opponent.name.to_int(), opponent.playerboard)
tiles_burned += 1
if tiles_burned > 0:
# Trigger screen shake
if opponent.is_multiplayer_authority():
opponent.rpc("trigger_screen_shake", "targeted")
else:
opponent.rpc_id(opponent.get_multiplayer_authority(), "trigger_screen_shake", "targeted")
print("[SpecialTiles] BURN_TILES: Knocked %d tiles from %s" % [tiles_burned, opponent.name]) target.rpc("display_message", "Burned by %s!" % player.display_name, 3)
player.rpc("display_message", "Knocked tiles from %s!" % opponent.display_name)
opponent.rpc("display_message", "%s knocked tiles out of your bag!" % player.display_name)
func _execute_spawn_tiles(): func _execute_spawn_tiles(target: Node3D):
# NEW LOGIC: Spawn more in neighbor space (radius 2) # Spawn tiles around TARGET (usually Self for Coin)
var radius = 2 spawn_powerups_around(target.current_position, false) # False = normal tiles? User says "Spawn 3x3 pattern tiles"
var candidates = [] # 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.
for x in range(-radius, radius + 1): target.rpc("display_message", "Tiles Spawned!", 2)
for y in range(-radius, radius + 1):
if x == 0 and y == 0: continue
var pos = player.current_position + Vector2i(x, y)
if enhanced_gridmap.is_position_valid(pos):
var cell = Vector3i(pos.x, 1, pos.y)
if enhanced_gridmap.get_cell_item(cell) == -1:
candidates.append(cell)
var spawn_count = rng.randi_range(3, 8) # Spawn a bunch
candidates.shuffle()
var actual_spawned = 0
for i in range(min(spawn_count, candidates.size())):
var cell = candidates[i]
var new_tile = rng.randi_range(7, 10)
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)
actual_spawned += 1
print("[SpecialTiles] SPAWN_TILES: Spawned %d tiles around %s" % [actual_spawned, player.name])
player.rpc("display_message", "Spawned tiles nearby!")
func _execute_freeze_player(): func _execute_freeze_player(target: Node3D):
# Find random opponent if not target:
var opponent = _get_random_opponent()
if not opponent:
print("[SpecialTiles] No opponent found for FREEZE_PLAYER") print("[SpecialTiles] No opponent found for FREEZE_PLAYER")
return return
# Freeze the opponent if target.has_method("apply_stagger"): # Stagger = Freeze roughly
if opponent.has_method("apply_freeze"): target.rpc("apply_stagger", FREEZE_DURATION)
opponent.apply_freeze(FREEZE_DURATION)
else: else:
# Fallback: directly set frozen state target.set("is_frozen", true)
opponent.set("is_frozen", true) _create_unfreeze_timer(target, FREEZE_DURATION)
_create_unfreeze_timer(opponent, FREEZE_DURATION)
# Trigger screen shake on the frozen opponent
if opponent.is_multiplayer_authority():
opponent.rpc("trigger_screen_shake", "targeted")
else:
opponent.rpc_id(opponent.get_multiplayer_authority(), "trigger_screen_shake", "targeted")
print("[SpecialTiles] FREEZE_PLAYER: Froze %s for %ds" % [opponent.name, FREEZE_DURATION])
player.rpc("display_message", "Froze %s!" % opponent.display_name)
opponent.rpc("display_message", "%s froze you!" % player.display_name)
# Visual effect: Ice Blue
# Use RPC to sync visual effect to everyone (call_local handles our screen)
if opponent.has_method("sync_modulate"):
opponent.rpc("sync_modulate", Color(0.5, 0.8, 1.0))
# Standard players sync via network transform but modulation might not sync automatically unless handled.
# Let's hope basic property sync or local effect handles it enough for now,
# but ideally we should RPC a visual update method on the player.
# Checking player.gd again, there isn't a sync_modulate.
# We can just set it locally and rely on the RPCs below for syncing the EFFECT STATUS,
# but we should probably RPC the color change to be sure everyone sees it.
# Actually, since we don't have a generic sync_proeprty, we will just set it locally on the authority
# and rely on the target itself to perhaps propogate it? No, that won't work traversing network.
# We need a way to tell clients "Painter this player blue".
# The simplest safe way without modifying Player.gd extensively is to rpc a method call if available,
# or just set it on the proxy if we are the server.
# But special_tiles is running on the player who TRIGGERED it.
# If I am client A, targeting client B. I am authority of ME. B is authority of B.
# I can't set properties on B directly and expect them to sync.
# I must RPC B to freeze himself.
# The _execute_freeze_player logic calls opponent.apply_freeze or sets is_frozen.
# If opponent has authority, they will run their own logic?
# Wait, special_tiles_manager runs on the client who picked up the tile?
# "if player.is_multiplayer_authority(): rpc(...)" implies we are the authority of the player who picked it up.
# We find an opponent (which is a proxy version on our machine).
# We call methods on that proxy.
# "opponent.rpc(...)" sends a message to the authority of that opponent.
# So we should validly call an RPC on opponent to change color.
# But Player.gd doesn't have "set_modulate_rpc".
# Use "set" works locally.
# We need to add visual sync support to Player.gd or just rely on what we have.
# Given constraints, I'll add the modulate locally and maybe the opponent-side logic should handle it?
# _create_unfreeze_timer runs on OUR machine mostly? No, "await player.get_tree()..."
# If we are A, targeting B.
# We call opponent.apply_freeze(). If B has that method, good.
# If B lacks it, we set is_frozen on B's proxy and run a timer on A's machine?
# That only freezes B on A's screen if logic relies on is_frozen?
# Actually, `opponent.rpc("display_message", ...)` works.
# Let's add a `sync_visual_effect` to Player.gd if needed, or just standard property setting if supported.
# For now, I will just set it and see if I can add a dedicated RPC in Player.gd in the next step if this is insufficient,
# OR better: I'll use `opponent.rpc("sync_modulate", ...)` and add that method to Player.gd in a separate tool call.
# For this tool call, I'll update the text and set local modulate.
func _create_unfreeze_timer(target_player: Node3D, duration: float):
if not is_instance_valid(player) or not is_instance_valid(target_player):
return
await player.get_tree().create_timer(duration).timeout target.rpc("display_message", "Frozen by %s!" % player.display_name, 3)
if is_instance_valid(target_player):
target_player.set("is_frozen", false)
# Reset visuals
if target_player.has_method("sync_modulate"):
target_player.rpc("sync_modulate", Color.WHITE)
target_player.rpc("display_message", "Unfrozen!")
func _execute_block_floor(target: Node3D):
func _execute_block_floor(): # Make nearby tile non-walkable for 9 seconds
# NEW LOGIC: Block 3 to 9 tiles in a line (Horizontal/Vertical/Diagonal) # Target the floor UNDER or NEAR the target?
# Find valid start neighbor # "block ( other player ) spawn BLOCK_FLOOR Make nearby tile non-walkable"
var neighbors = enhanced_gridmap.get_neighbors(player.current_position, 0) # Let's block the tile they are standing on + neighbors?
var valid_neighbors = neighbors.filter(func(n): return n.is_walkable) var center = target.current_position
var neighbors = enhanced_gridmap.get_neighbors(center, 1) # Include diagonals
neighbors.append({"position": center}) # Add center
if valid_neighbors.is_empty(): for n in neighbors:
return var pos = n.position
var block_pos = Vector3i(pos.x, 0, pos.y)
var start_neighbor = valid_neighbors[rng.randi() % valid_neighbors.size()]
var start_pos = start_neighbor.position
# Random direction: H, V, D1, D2
var directions = [
Vector2i(1, 0), Vector2i(-1, 0), # Horizontal
Vector2i(0, 1), Vector2i(0, -1), # Vertical
Vector2i(1, 1), Vector2i(-1, -1), # Diagonal
Vector2i(1, -1), Vector2i(-1, 1)
]
var dir = directions[rng.randi() % directions.size()]
var count = rng.randi_range(3, 9)
var valid_block_count = 0
for i in range(count):
var target_pos_2d = start_pos + (dir * i)
# Check if valid grid position
if not enhanced_gridmap.is_position_valid(target_pos_2d):
break # Stop if we hit edge of map
var block_pos = Vector3i(target_pos_2d.x, 0, target_pos_2d.y)
var original_item = enhanced_gridmap.get_cell_item(block_pos)
# Make tile non-walkable # Block it
var blocked_item = 4
if enhanced_gridmap.non_walkable_items.size() > 0:
blocked_item = enhanced_gridmap.non_walkable_items[0]
if player.is_multiplayer_authority(): if player.is_multiplayer_authority():
var main = player.get_tree().get_root().get_node_or_null("Main") var main = player.get_tree().get_root().get_node_or_null("Main")
if main: if main:
main.rpc("sync_grid_item", block_pos.x, block_pos.y, block_pos.z, blocked_item) # 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)
blocked_tiles.append({ blocked_tiles.append({
"position": block_pos, "position": block_pos,
"original_item": original_item, "original_item": original_item if original_item != 4 else 0, # Restore to 0 (floor) if confused
"timer": BLOCK_DURATION "timer": BLOCK_DURATION
}) })
valid_block_count += 1
target.rpc("display_message", "Floor Blocked!", 3)
if valid_block_count > 0:
enhanced_gridmap.initialize_astar()
print("[SpecialTiles] BLOCK_FLOOR: Blocked line of %d tiles" % valid_block_count)
player.rpc("display_message", "Blocked a wall of tiles!")
func _execute_invisible_mode(): func _execute_invisible_mode(target: Node3D):
# Set invisible mode on player target.is_invisible = true
# NEW LOGIC: Also enables auto-grab in _process # Auto-disable after duration handled in Player._process or here?
if player.has_method("apply_invisible_mode"): # SpecialTilesManager seems to handle effect timers.
player.apply_invisible_mode(INVISIBLE_DURATION) invisible_timer = INVISIBLE_DURATION
else: target.rpc("display_message", "Invisible!", 2)
player.set("is_invisible", true)
player.set("original_movement_range", player.movement_range)
player.movement_range = player.movement_range + 2
invisible_timer = INVISIBLE_DURATION
# =============================================================================
# Helper: Spawn Powerups (For Super Push)
# =============================================================================
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)
print("[SpecialTiles] INVISIBLE_MODE: Activated") var radius = 2
player.rpc("display_message", "Invisible Mode Active!") for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
var pos = center + Vector2i(x, y)
if enhanced_gridmap.is_position_valid(pos):
# Random chance
if rng.randf() > 0.4: continue
var item_id = rng.randi_range(7, 10) # 7-10 are the special tiles
var cell = Vector3i(pos.x, 1, pos.y)
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, item_id)
func _process(delta): func _process(delta):
@@ -355,10 +307,8 @@ func _update_invisible_timer(delta: float):
if invisible_timer <= 0: if invisible_timer <= 0:
invisible_timer = 0 invisible_timer = 0
if is_instance_valid(player): if is_instance_valid(player):
player.set("is_invisible", false) player.is_invisible = false
if player.get("original_movement_range"): player.rpc("display_message", "Invisibility Ended")
player.movement_range = player.original_movement_range
player.rpc("display_message", "Invisible mode ended!")
# ============================================================================= # =============================================================================
@@ -419,3 +369,12 @@ func check_shield_and_cancel_effect() -> bool:
player.rpc("display_message", "Shield blocked an attack!") player.rpc("display_message", "Shield blocked an attack!")
return true return true
return false return false
func _create_unfreeze_timer(target_player: Node3D, duration: float):
await player.get_tree().create_timer(duration).timeout
if is_instance_valid(target_player):
target_player.set("is_frozen", false)
# Reset visuals
if target_player.has_method("sync_modulate"):
target_player.rpc("sync_modulate", Color.WHITE)
target_player.rpc("display_message", "Unfrozen!")
+26 -11
View File
@@ -16,8 +16,14 @@ var move_button
var grab_button var grab_button
var put_button var put_button
var randomize_button var randomize_button
var arrange_button var victory_ui_scene = preload("res://scenes/ui/victory_ui.tscn")
var powerup_inventory_ui_script = preload("res://scripts/ui/powerup_inventory_ui.gd")
var main_menu_instance
var victory_ui_instance
var playerboard_ui var playerboard_ui
var action_menu_instance
var powerup_inventory_ui
var local_player_character var local_player_character
var _previous_playerboard_state: Array = [] var _previous_playerboard_state: Array = []
@@ -26,22 +32,31 @@ enum ActionState {
NONE, NONE,
MOVING, MOVING,
GRABBING, GRABBING,
PUTTING,
RANDOMIZING,
ARRANGING, ARRANGING,
RANDOMIZING
} }
var current_action_state = ActionState.NONE var current_action_state = ActionState.NONE
func initialize(main_node): func initialize(player_node):
# Get PowerUp Inventory UI from scene
powerup_inventory_ui = player_node.get_node_or_null("PowerUpInventoryUI")
# Get node references from main scene # Get node references from main scene
action_menu = main_node.get_node("ActionMenu") randomize_button = player_node.get_node("ActionMenu/ActionButtonContainer/RandomizeButton") # renamed main_node to player_node which is Main
move_button = main_node.get_node("ActionMenu/ActionButtonContainer/MoveButton") arrange_button = player_node.get_node("ActionMenu/ActionButtonContainer/ArrangeButton")
grab_button = main_node.get_node("ActionMenu/ActionButtonContainer/GrabButton") playerboard_ui = player_node.get_node("PlayerboardUI")
put_button = main_node.get_node("ActionMenu/ActionButtonContainer/PutButton")
randomize_button = main_node.get_node("ActionMenu/ActionButtonContainer/RandomizeButton") # ... (skipping unchanged functions) ...
arrange_button = main_node.get_node("ActionMenu/ActionButtonContainer/ArrangeButton")
playerboard_ui = main_node.get_node("PlayerboardUI") func set_local_player(player):
local_player_character = player
if powerup_inventory_ui:
powerup_inventory_ui.setup(player)
# Connect to powerup signals with deferred call (manager needs time to initialize)
_connect_powerup_manager_deferred(player)
func setup_action_buttons(action_state_callback): func setup_action_buttons(action_state_callback):
move_button.pressed.connect(func(): action_state_callback.call(ActionState.MOVING)) move_button.pressed.connect(func(): action_state_callback.call(ActionState.MOVING))
+94
View File
@@ -0,0 +1,94 @@
extends Control
# PowerUpInventoryUI - Displays stored powerups and handles selection
# UI References
var icon_containers: Dictionary = {} # { EffectEnum: TextureRect }
var selection_indicators: Dictionary = {} # { EffectEnum: Control }
# Local State
var selected_effect: int = -1
signal effect_selected(effect: int)
func _ready():
# Wait for children to be ready
await get_tree().process_frame
# Map Effect Enum to UI Nodes (Assumes specific names in main.tscn)
# Default structure: HBoxContainer > CoinIcon, HeartIcon, etc.
var container = get_node_or_null("HBoxContainer")
if not container:
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"))
# Note: SpecialEffect enum values from SpecialTilesManager:
# BURN_TILES = 0
# SPAWN_TILES = 1 (we map Coin to 0, merged)
# FREEZE_PLAYER = 2
# BLOCK_FLOOR = 3
# INVISIBLE_MODE = 4
func _setup_icon(effect_id: int, node: Control):
if not node: return
icon_containers[effect_id] = node
# Assume node has specific children for selection state?
# "SelectRect" child?
var select_rect = node.get_node_or_null("SelectRect")
if select_rect:
selection_indicators[effect_id] = select_rect
select_rect.visible = false
# 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()
else:
select_effect(effect_id)
func setup(player_node):
var special_manager = player_node.get_node_or_null("SpecialTilesManager")
if special_manager:
if not special_manager.is_connected("inventory_updated", _on_inventory_updated):
special_manager.connect("inventory_updated", _on_inventory_updated)
_on_inventory_updated(special_manager.inventory)
func _on_inventory_updated(inventory: Dictionary):
# Update UI icons (Dimmed vs Lit)
for effect in icon_containers:
if inventory.has(effect):
var has_item = inventory[effect]
icon_containers[effect].modulate = Color.WHITE if has_item else Color(0.3, 0.3, 0.3, 0.5)
if not has_item and selected_effect == effect:
deselect()
func select_effect(effect: int):
# Check if we own it first? The UI click handler should check.
selected_effect = effect
emit_signal("effect_selected", effect)
_update_selection_visuals()
func deselect():
selected_effect = -1
emit_signal("effect_selected", -1)
_update_selection_visuals()
func _update_selection_visuals():
for effect in selection_indicators:
selection_indicators[effect].visible = (effect == selected_effect)
Binary file not shown.