feat: update vfx

This commit is contained in:
2026-06-18 14:04:06 +08:00
parent 5354d8b30f
commit 78f071b728
18 changed files with 704 additions and 62 deletions
+25 -43
View File
@@ -389,6 +389,14 @@ func _execute_area_freeze(target_pos: Vector2i = Vector2i(-9999, -9999)):
SfxManager.rpc("play_rpc", "freeze")
# Floor VFX: cover the whole frozen area on every peer.
var main_vfx = get_node_or_null("/root/Main")
if main_vfx and main_vfx.has_method("play_freeze_floor_vfx"):
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
main_vfx.rpc("play_freeze_floor_vfx", center_pos.x, center_pos.y, radius, FREEZE_SLOW_DURATION)
else:
main_vfx.play_freeze_floor_vfx(center_pos.x, center_pos.y, radius, FREEZE_SLOW_DURATION)
if hit_count > 0 and player.is_multiplayer_authority():
var is_sng = LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO)
if not is_sng:
@@ -403,44 +411,9 @@ func _execute_area_freeze(target_pos: Vector2i = Vector2i(-9999, -9999)):
else:
NotificationManager.send_message(player, "Hit %d Players!" % hit_count, NotificationManager.MessageType.GOAL)
# Visual Feedback (Layer 0 - Ground Level, matching Wall logic)
if player.is_multiplayer_authority():
var main_node = get_node_or_null("/root/Main")
if main_node and main_node.has_method("sync_grid_items_batch"):
var batch_data = []
var restoration_data = [] # Stores {pos, item} to restore later
for rx in range(-radius, radius + 1):
for rz in range(-radius, radius + 1):
var cx = center_pos.x + rx
var cz = center_pos.y + rz
var pos = Vector3i(cx, 0, cz)
# Get original ground item to restore later
var original_ground = enhanced_gridmap.get_cell_item(pos)
# Ignore if it is an immutable tile (Safe Zone 2, Wall 4, etc)
if original_ground in [1, 2, 3, 4, 15, 16]: continue
restoration_data.append({"pos": pos, "item": original_ground})
batch_data.append({"x": cx, "y": 0, "z": cz, "item": 5})
if not batch_data.is_empty():
main_node.rpc("sync_grid_items_batch", batch_data)
# Removal timer with accurate restoration
get_tree().create_timer(FREEZE_SLOW_DURATION).timeout.connect(func():
var cl_node = get_node_or_null("/root/Main")
if not cl_node: return
var clear_batch = []
for entry in restoration_data:
var p = entry.pos
# Only restore if it is STILL our Freeze tile
if enhanced_gridmap.get_cell_item(p) == 5:
clear_batch.append({"x": p.x, "y": 0, "z": p.z, "item": entry.item})
if not clear_batch.is_empty():
cl_node.rpc("sync_grid_items_batch", clear_batch)
)
# Visual feedback is the freeze-floor VFX (play_freeze_floor_vfx above). The
# blue layer-0 highlight tiles were removed so only the VFX shows; the slow
# effect is driven by active_freeze_zones / layer-2 checks, not these tiles.
func _execute_block_floor(target_pos: Vector2i = Vector2i(-999, -999)):
@@ -474,31 +447,40 @@ func _execute_block_floor(target_pos: Vector2i = Vector2i(-999, -999)):
var main = get_node_or_null("/root/Main")
if main and main.has_method("sync_grid_items_batch"):
var batch_data = []
var vfx_cells := PackedVector2Array()
for n in neighbors:
var pos = n.position
if _is_position_blocked_by_stand(pos): continue
var block_pos = Vector3i(pos.x, 0, pos.y)
var original_item = enhanced_gridmap.get_cell_item(block_pos)
# PROTECTED FLOOR CHECK: avoid overwriting Start (1), Safe (2), Finish (3), or Wall (4)
var is_immutable = false
if "immutable_items" in enhanced_gridmap:
if original_item in enhanced_gridmap.immutable_items:
is_immutable = true
if original_item in [1, 2, 3, 4, 15, 16] or is_immutable: continue
batch_data.append({"x": block_pos.x, "y": 0, "z": block_pos.z, "item": 4})
vfx_cells.append(Vector2(block_pos.x, block_pos.z))
# Record for restoration
blocked_tiles.append({
"position": block_pos,
"original_item": original_item,
"timer": BLOCK_DURATION
})
if not batch_data.is_empty():
main.rpc("sync_grid_items_batch", batch_data)
# Block VFX: one box_block per cell, animated, on every peer.
if not vfx_cells.is_empty() and main.has_method("play_block_floor_vfx"):
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
main.rpc("play_block_floor_vfx", vfx_cells, BLOCK_DURATION)
else:
main.play_block_floor_vfx(vfx_cells, BLOCK_DURATION)
# Notify
SfxManager.rpc("play_rpc", "wall")
+8
View File
@@ -1025,6 +1025,14 @@ func _scatter_player_tiles(player_node: Node):
# Notify the player
SfxManager.rpc("play_rpc", "tile_scatter")
NotificationManager.send_message(player_node, "Not in Safe Zone! Tiles scattered!", NotificationManager.MessageType.WARNING)
# Playerboard scatter VFX: one-shot, only on the affected player's own UI.
# Routed through the player node (same pattern as play_scatter_knock) because
# the player-node name is not a valid network peer ID for rpc_id here.
if can_rpc():
player_node.rpc("play_playerboard_scatter")
else:
player_node.play_playerboard_scatter()
# Screen shake and animation
if can_rpc():
+6
View File
@@ -491,6 +491,12 @@ func submit_to_leaderboard() -> void:
func get_display_name(fallback: String = "Guest") -> String:
if not is_profile_loaded:
# Profile storage (inventory/stats/fragments) may still be loading, but
# AuthManager.current_user is populated before auth_completed fires, so the
# real name is already available — prefer it over the guest fallback to
# avoid a logged-in host being registered as "Guest" when acting quickly.
if AuthManager.current_user.has("display_name"):
return AuthManager.current_user["display_name"]
return fallback
return profile.get("display_name", fallback)
+14 -4
View File
@@ -187,11 +187,21 @@ func host_game(room_meta: Dictionary = {}):
if not bridge:
printerr("Cannot host: Bridge not initialized")
return
print("Hosting match via Nakama Bridge...")
var result = await bridge.create_match()
if result and result.is_exception():
emit_signal("match_join_error", result.get_exception().message)
# Guard against re-entry: create_match() rejects any state other than
# DISCONNECTED. A double-click on the mode button (or a retry after a stalled
# attempt) can re-enter here while a prior create_match() is still JOINING.
if bridge.match_state == NakamaMultiplayerBridge.MatchState.CONNECTED:
print("[NakamaManager] Already hosting a match; ignoring duplicate host request.")
return
if bridge.match_state != NakamaMultiplayerBridge.MatchState.DISCONNECTED:
# Stranded mid-join (e.g. previous attempt never resolved). Reset so the
# bridge is idle before we try again.
print("[NakamaManager] Bridge busy (%s); resetting before hosting." % NakamaMultiplayerBridge.MatchState.keys()[bridge.match_state])
await bridge.leave()
print("Hosting match via Nakama Bridge...")
await bridge.create_match()
# Store room metadata in Nakama storage so other players can see it in listings
if session and current_match_id and room_meta.size() > 0:
var meta_json = JSON.stringify(room_meta)
+55 -11
View File
@@ -1,10 +1,15 @@
extends Node3D
## Autoplays all animations in the AnimationPlayer simultaneously and loops them.
## Attach to any node that has an AnimationPlayer child (e.g. tekton_fishing_animation instances).
## Merges every animation in the AnimationPlayer into a single looping clip and
## plays it, so meshes driven by different animations (e.g. ted_bones and
## fishing_acc) all animate simultaneously instead of overwriting each other.
## Attach to any node that has an AnimationPlayer child.
@onready var anim_player: AnimationPlayer = $AnimationPlayer
const MERGED_LIB := "merged"
const MERGED_NAME := "merged/all"
func _ready() -> void:
if not anim_player:
push_warning("tekton_fishing_autoplay: No AnimationPlayer found")
@@ -13,13 +18,52 @@ func _ready() -> void:
# Wait one frame for the scene tree to settle
await get_tree().process_frame
# Play every animation simultaneously, all looping
for anim_name in anim_player.get_animation_list():
anim_player.play(anim_name)
anim_player.advance(0.0) # ensure it starts
var source_names := anim_player.get_animation_list()
if source_names.is_empty():
return
# Set all to loop
for anim_name in anim_player.get_animation_list():
var anim: Animation = anim_player.get_animation(anim_name)
if anim:
anim.loop_mode = Animation.LOOP_LINEAR
var merged := _build_merged_animation(source_names)
if not merged:
return
var lib := AnimationLibrary.new()
lib.add_animation("all", merged)
if anim_player.has_animation_library(MERGED_LIB):
anim_player.remove_animation_library(MERGED_LIB)
anim_player.add_animation_library(MERGED_LIB, lib)
anim_player.play(MERGED_NAME)
## Combines the tracks of every source animation into one Animation. The source
## animations target disjoint node sets, so simply concatenating their tracks
## drives all meshes at once.
func _build_merged_animation(source_names: Array) -> Animation:
var merged := Animation.new()
merged.loop_mode = Animation.LOOP_LINEAR
var max_length := 0.0
for anim_name in source_names:
var src: Animation = anim_player.get_animation(anim_name)
if not src:
continue
max_length = max(max_length, src.length)
for t in src.get_track_count():
_copy_track(src, t, merged)
merged.length = max_length
return merged
func _copy_track(src: Animation, src_track: int, dst: Animation) -> void:
var new_track := dst.add_track(src.track_get_type(src_track))
dst.track_set_path(new_track, src.track_get_path(src_track))
dst.track_set_interpolation_type(new_track, src.track_get_interpolation_type(src_track))
dst.track_set_interpolation_loop_wrap(new_track, src.track_get_interpolation_loop_wrap(src_track))
dst.track_set_enabled(new_track, src.track_is_enabled(src_track))
for k in src.track_get_key_count(src_track):
var time := src.track_get_key_time(src_track, k)
var value = src.track_get_key_value(src_track, k)
var transition := src.track_get_key_transition(src_track, k)
dst.track_insert_key(new_track, time, value, transition)