update
This commit is contained in:
+6
-76
@@ -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
File diff suppressed because one or more lines are too long
+90
-12
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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!")
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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.
Reference in New Issue
Block a user