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
Binary file not shown.

After

Width:  |  Height:  |  Size: 854 KiB

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dhxg7146rcgnk"
path="res://.godot/imported/freezeatas.png-bf12d086ba74eeafdcd33ce2cbe77666.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/graphics/vfx/tile_vfx_freeze/freezeatas.png"
dest_files=["res://.godot/imported/freezeatas.png-bf12d086ba74eeafdcd33ce2cbe77666.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

@@ -0,0 +1,42 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://byh7506oksbhb"
path.s3tc="res://.godot/imported/freezebawah.png-6da5f32f6dc1d9aab209331638cef664.s3tc.ctex"
path.etc2="res://.godot/imported/freezebawah.png-6da5f32f6dc1d9aab209331638cef664.etc2.ctex"
metadata={
"imported_formats": ["s3tc_bptc", "etc2_astc"],
"vram_texture": true
}
[deps]
source_file="res://assets/graphics/vfx/tile_vfx_freeze/freezebawah.png"
dest_files=["res://.godot/imported/freezebawah.png-6da5f32f6dc1d9aab209331638cef664.s3tc.ctex", "res://.godot/imported/freezebawah.png-6da5f32f6dc1d9aab209331638cef664.etc2.ctex"]
[params]
compress/mode=2
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=true
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=0
+5
View File
@@ -0,0 +1,5 @@
[gd_scene format=3 uid="uid://d3m0u8gl5odhw"]
[ext_resource type="PackedScene" uid="uid://b6xusldnaa288" path="res://assets/models/props/box_block.gltf" id="1_okim6"]
[node name="box_block" unique_id=1480078977 instance=ExtResource("1_okim6")]
@@ -4,12 +4,12 @@ importer="scene"
importer_version=1
type="PackedScene"
uid="uid://b6xusldnaa288"
path="res://.godot/imported/Box_block_2.gltf-6b278c07bb52956b8b62d9b428e8f6a6.scn"
path="res://.godot/imported/box_block.gltf-4e37264bb9b1903a1ad0284c85551176.scn"
[deps]
source_file="res://assets/models/props/Box_block_2.gltf"
dest_files=["res://.godot/imported/Box_block_2.gltf-6b278c07bb52956b8b62d9b428e8f6a6.scn"]
source_file="res://assets/models/props/box_block.gltf"
dest_files=["res://.godot/imported/box_block.gltf-4e37264bb9b1903a1ad0284c85551176.scn"]
[params]
+11
View File
@@ -140,6 +140,10 @@ var _bot_names: Dictionary = {}
# Room list filter ("" = all, "Freemode", "Stop n Go", etc.)
var _room_mode_filter: String = ""
# Re-entry guard for the async host flow (prevents double-click from starting
# two overlapping create_match() calls).
var _is_hosting: bool = false
# =============================================================================
# Chat System
# =============================================================================
@@ -249,6 +253,13 @@ func _ready():
UserProfileManager.profile_loaded.connect(func(_p): _sync_room_profile_card())
UserProfileManager.profile_updated.connect(func(): _sync_room_profile_card())
# Clear the host re-entry guard whenever the host flow concludes — success,
# failure, or leaving the room.
LobbyManager.room_joined.connect(func(_r): _is_hosting = false)
LobbyManager.room_left.connect(func(): _is_hosting = false)
NakamaManager.match_join_error.connect(func(_e): _is_hosting = false)
NakamaManager.connection_failed.connect(func(_e): _is_hosting = false)
# Connect Mailbox UI
if MailManager:
MailManager.unread_count_changed.connect(_on_mail_unread_count_changed)
+138
View File
@@ -1919,6 +1919,144 @@ func sync_grid_item(x: int, y: int, z: int, item: int):
if enhanced_gridmap.has_method("update_grid_data"):
enhanced_gridmap.update_grid_data()
@rpc("any_peer", "call_local", "reliable")
func play_freeze_floor_vfx(center_x: int, center_z: int, radius: int, duration: float) -> void:
# Spawns a flat freeze-floor AnimatedSprite3D that covers the (2*radius+1)^2
# frozen area, centered on the given grid cell and sitting just above the
# floor surface. The template under vfxController/AnimatedSprite3D (hidden) is
# laid flat and sized for ~1 tile; we duplicate it, then size it to the area
# from its measured footprint. Runs on every peer.
var vfx_root := get_node_or_null("vfxController")
if not vfx_root:
return
var template := vfx_root.get_node_or_null("AnimatedSprite3D") as AnimatedSprite3D
if not template:
return
var enhanced_gridmap = $EnhancedGridMap
if not enhanced_gridmap:
return
var cell: Vector3 = enhanced_gridmap.cell_size
var vfx := template.duplicate() as AnimatedSprite3D
# --- Scale to cover the whole area -------------------------------------
# The template's world footprint = its local sprite size (texture * pixel_size,
# from the AABB) times each basis axis length. Scale each flat axis (X = width,
# Y = depth, since the sprite is rotated to lie flat) so it spans the area.
# AREA_SIZE_FACTOR oversizes slightly so the visible art (which has transparent
# padding inside the frame) actually fills the (2*radius+1)-tile region — tune
# this if it reads a touch small or large.
const AREA_SIZE_FACTOR := 1.25
var aabb := template.get_aabb()
var b := template.transform.basis
var base_w: float = absf(aabb.size.x) * b.x.length()
var base_d: float = absf(aabb.size.y) * b.y.length()
var want_w: float = float(2 * radius + 1) * cell.x * AREA_SIZE_FACTOR
var want_d: float = float(2 * radius + 1) * cell.z * AREA_SIZE_FACTOR
if base_w > 0.0001:
b.x = b.x * (want_w / base_w)
if base_d > 0.0001:
b.y = b.y * (want_d / base_d)
# --- World center of the freeze area -----------------------------------
# X/Z use the same convention players use (grid_to_world): cell center =
# index*size + half a cell. Y is taken from the template's own position in the
# scene, so the height can be tuned in the editor (move vfxController/
# AnimatedSprite3D up/down) without touching code. vfxController/Main sit at
# the origin, so this local point is also the world point.
var world_center := Vector3(
center_x * cell.x + cell.x * 0.5,
template.position.y,
center_z * cell.z + cell.z * 0.5
)
# Assign basis + origin together in one shot so nothing overwrites the position.
vfx.transform = Transform3D(b, world_center)
vfx.visible = true
vfx_root.add_child(vfx)
vfx.play()
print("[FreezeVFX] center cell=(%d,%d) -> world=%s, scale=(%.2f x %.2f)" % [center_x, center_z, world_center, want_w, want_d])
await get_tree().create_timer(duration).timeout
if is_instance_valid(vfx):
vfx.queue_free()
@rpc("any_peer", "call_local", "reliable")
func play_block_floor_vfx(cells: PackedVector2Array, duration: float) -> void:
# Spawns one box_block VFX per blocked cell (each covers 1x1), autoplaying its
# wall animation. The template under vfxController/box_block (hidden) is sized
# for one cell; we duplicate it per cell, keeping its Y/scale from the scene so
# height can be tuned in the editor. Runs on every peer.
var vfx_root := get_node_or_null("vfxController")
if not vfx_root:
return
var template := vfx_root.get_node_or_null("box_block")
if not template:
return
var enhanced_gridmap = $EnhancedGridMap
if not enhanced_gridmap:
return
var cell: Vector3 = enhanced_gridmap.cell_size
var spawned: Array = []
for c in cells:
# DUPLICATE_USE_INSTANTIATION so the instanced glb subtree (incl. its
# AnimationPlayer) is recreated in the copy rather than dropped.
var box := template.duplicate(DUPLICATE_USE_INSTANTIATION)
# X/Z = cell center (same convention as players), Y/scale kept from template.
var pos := Vector3(
int(c.x) * cell.x + cell.x * 0.5,
template.position.y,
int(c.y) * cell.z + cell.z * 0.5
)
box.transform = Transform3D(template.transform.basis, pos)
box.visible = true
vfx_root.add_child(box)
_autoplay_vfx_animation(box)
spawned.append(box)
await get_tree().create_timer(duration).timeout
for box in spawned:
if is_instance_valid(box):
box.queue_free()
func _autoplay_vfx_animation(node: Node) -> void:
# Find the AnimationPlayer Godot creates for an imported glb/scene and play its
# first clip once (one-shot) when the VFX is spawned.
var anim_player := node.get_node_or_null("AnimationPlayer") as AnimationPlayer
if not anim_player:
for child in node.get_children():
if child is AnimationPlayer:
anim_player = child
break
if not anim_player:
return
var list := anim_player.get_animation_list()
if list.is_empty():
return
var anim_name: String = list[0]
var anim := anim_player.get_animation(anim_name)
if anim:
anim.loop_mode = Animation.LOOP_NONE
anim_player.play(anim_name)
func play_playerboard_scatter_vfx() -> void:
# One-shot "scatter" animation over the local player's board UI. Triggered for
# the player whose tiles were scattered (caught outside the safe zone in
# Stop n Go). The clip is already non-looping in the SpriteFrames.
var vfx := get_node_or_null("PlayerBoardUI/AnimatedSprite2D") as AnimatedSprite2D
if not vfx:
return
vfx.visible = true
vfx.play("scatter")
await vfx.animation_finished
vfx.visible = false
@rpc("any_peer", "call_local", "reliable")
func sync_grid_items_batch(data: Array):
# data is an array of dictionaries: [{x: int, y: int, z: int, item: int}, ...]
+335
View File
@@ -2,6 +2,8 @@
[ext_resource type="MeshLibrary" uid="uid://kcv6ans86ug7" path="res://addons/enhanced_gridmap/meshlibrary/default.tres" id="1_110wo"]
[ext_resource type="Script" uid="uid://co1ads72by6na" path="res://scenes/main.gd" id="1_xcpe3"]
[ext_resource type="Texture2D" uid="uid://byh7506oksbhb" path="res://assets/graphics/vfx/tile_vfx_freeze/freezebawah.png" id="2_2cjbq"]
[ext_resource type="PackedScene" uid="uid://d3m0u8gl5odhw" path="res://assets/models/meshes/box_block.tscn" id="2_chjal"]
[ext_resource type="Script" uid="uid://bja8ixryvthu0" path="res://addons/enhanced_gridmap/enhanced_gridmap.gd" id="2_hbe1v"]
[ext_resource type="Environment" uid="uid://jbptgqvstei3" path="res://assets/main-environment.tres" id="4_ky38j"]
[ext_resource type="StyleBox" uid="uid://dlw1ogamn741n" path="res://assets/styles/box_flat.tres" id="5_dvx6y"]
@@ -14,6 +16,7 @@
[ext_resource type="Texture2D" uid="uid://68x88jj25yxg" path="res://assets/textures/Adjacent.png" id="9_6gcb6"]
[ext_resource type="Texture2D" uid="uid://dasaeaytvhll0" path="res://assets/models/pboard/AdjacentRect.tres" id="9_aspsw"]
[ext_resource type="FontFile" uid="uid://xnjx058n4tsw" path="res://assets/fonts/Nougat-ExtraBlack.ttf" id="13_j8jky"]
[ext_resource type="Texture2D" uid="uid://cmoyhmh4jpep0" path="res://assets/graphics/vfx/playerboard_scatter.png" id="14_5poiv"]
[ext_resource type="Texture2D" uid="uid://bafocgx2apwjm" path="res://assets/graphics/gui/stop_timer/Segment0_empty.png" id="14_fv21b"]
[ext_resource type="Texture2D" uid="uid://c4xpg3j7p7g33" path="res://assets/graphics/gui/global_match_timer/timer.png" id="14_tel4y"]
[ext_resource type="Texture2D" uid="uid://73ayhl1lqdpt" path="res://assets/graphics/gui/gauge/PowerLabel.png" id="14_vxglm"]
@@ -41,6 +44,316 @@
[ext_resource type="Script" uid="uid://b54tfa0n6kogi" path="res://scripts/managers/touch_controls.gd" id="touch_manager"]
[ext_resource type="Script" uid="uid://djiml4sh61dc1" path="res://scripts/ui/virtual_joystick.gd" id="virtual_joystick"]
[sub_resource type="AtlasTexture" id="AtlasTexture_0loja"]
atlas = ExtResource("2_2cjbq")
region = Rect2(0, 0, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_ids8r"]
atlas = ExtResource("2_2cjbq")
region = Rect2(500, 0, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_dnhof"]
atlas = ExtResource("2_2cjbq")
region = Rect2(1000, 0, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_1v6qk"]
atlas = ExtResource("2_2cjbq")
region = Rect2(1500, 0, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_augu2"]
atlas = ExtResource("2_2cjbq")
region = Rect2(2000, 0, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_tm6hj"]
atlas = ExtResource("2_2cjbq")
region = Rect2(2500, 0, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_kf5aa"]
atlas = ExtResource("2_2cjbq")
region = Rect2(0, 500, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_t5ykj"]
atlas = ExtResource("2_2cjbq")
region = Rect2(500, 500, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_e755i"]
atlas = ExtResource("2_2cjbq")
region = Rect2(1000, 500, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_ro3en"]
atlas = ExtResource("2_2cjbq")
region = Rect2(1500, 500, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_od4ux"]
atlas = ExtResource("2_2cjbq")
region = Rect2(2000, 500, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_xhfw6"]
atlas = ExtResource("2_2cjbq")
region = Rect2(2500, 500, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_1epgm"]
atlas = ExtResource("2_2cjbq")
region = Rect2(0, 1000, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_152kg"]
atlas = ExtResource("2_2cjbq")
region = Rect2(500, 1000, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_r4e17"]
atlas = ExtResource("2_2cjbq")
region = Rect2(1000, 1000, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_y57op"]
atlas = ExtResource("2_2cjbq")
region = Rect2(1500, 1000, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_mlv25"]
atlas = ExtResource("2_2cjbq")
region = Rect2(2000, 1000, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_a82wo"]
atlas = ExtResource("2_2cjbq")
region = Rect2(2500, 1000, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_gdvew"]
atlas = ExtResource("2_2cjbq")
region = Rect2(0, 1500, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_kjtvk"]
atlas = ExtResource("2_2cjbq")
region = Rect2(500, 1500, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_xrs0c"]
atlas = ExtResource("2_2cjbq")
region = Rect2(1000, 1500, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_xljpf"]
atlas = ExtResource("2_2cjbq")
region = Rect2(1500, 1500, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_j4bwh"]
atlas = ExtResource("2_2cjbq")
region = Rect2(2000, 1500, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_ksmap"]
atlas = ExtResource("2_2cjbq")
region = Rect2(2500, 1500, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_ygd8o"]
atlas = ExtResource("2_2cjbq")
region = Rect2(0, 2000, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_2s0kr"]
atlas = ExtResource("2_2cjbq")
region = Rect2(500, 2000, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_xaipn"]
atlas = ExtResource("2_2cjbq")
region = Rect2(1000, 2000, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_ndrjj"]
atlas = ExtResource("2_2cjbq")
region = Rect2(1500, 2000, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_ih3m7"]
atlas = ExtResource("2_2cjbq")
region = Rect2(2000, 2000, 500, 500)
[sub_resource type="AtlasTexture" id="AtlasTexture_11yus"]
atlas = ExtResource("2_2cjbq")
region = Rect2(2500, 2000, 500, 500)
[sub_resource type="SpriteFrames" id="SpriteFrames_5poiv"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": SubResource("AtlasTexture_0loja")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_ids8r")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_dnhof")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_1v6qk")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_augu2")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_tm6hj")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_kf5aa")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_t5ykj")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_e755i")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_ro3en")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_od4ux")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_xhfw6")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_1epgm")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_152kg")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_r4e17")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_y57op")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_mlv25")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_a82wo")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_gdvew")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_kjtvk")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_xrs0c")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_xljpf")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_j4bwh")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_ksmap")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_ygd8o")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_2s0kr")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_xaipn")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_ndrjj")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_ih3m7")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_11yus")
}],
"loop": false,
"name": &"freeze_floor",
"speed": 15.0
}]
[sub_resource type="AtlasTexture" id="AtlasTexture_2cjbq"]
atlas = ExtResource("14_5poiv")
region = Rect2(0, 0, 357, 340)
[sub_resource type="AtlasTexture" id="AtlasTexture_chjal"]
atlas = ExtResource("14_5poiv")
region = Rect2(357, 0, 357, 340)
[sub_resource type="AtlasTexture" id="AtlasTexture_cjqg0"]
atlas = ExtResource("14_5poiv")
region = Rect2(714, 0, 357, 340)
[sub_resource type="AtlasTexture" id="AtlasTexture_vchkt"]
atlas = ExtResource("14_5poiv")
region = Rect2(1071, 0, 357, 340)
[sub_resource type="AtlasTexture" id="AtlasTexture_txyw0"]
atlas = ExtResource("14_5poiv")
region = Rect2(0, 340, 357, 340)
[sub_resource type="AtlasTexture" id="AtlasTexture_vc5cj"]
atlas = ExtResource("14_5poiv")
region = Rect2(357, 340, 357, 340)
[sub_resource type="AtlasTexture" id="AtlasTexture_nvyfr"]
atlas = ExtResource("14_5poiv")
region = Rect2(714, 340, 357, 340)
[sub_resource type="AtlasTexture" id="AtlasTexture_ty1g6"]
atlas = ExtResource("14_5poiv")
region = Rect2(1071, 340, 357, 340)
[sub_resource type="AtlasTexture" id="AtlasTexture_j2jfr"]
atlas = ExtResource("14_5poiv")
region = Rect2(0, 680, 357, 340)
[sub_resource type="AtlasTexture" id="AtlasTexture_bjg73"]
atlas = ExtResource("14_5poiv")
region = Rect2(357, 680, 357, 340)
[sub_resource type="AtlasTexture" id="AtlasTexture_bu4jf"]
atlas = ExtResource("14_5poiv")
region = Rect2(714, 680, 357, 340)
[sub_resource type="AtlasTexture" id="AtlasTexture_o7ddy"]
atlas = ExtResource("14_5poiv")
region = Rect2(1071, 680, 357, 340)
[sub_resource type="SpriteFrames" id="SpriteFrames_0loja"]
animations = [{
"frames": [{
"duration": 1.0,
"texture": SubResource("AtlasTexture_2cjbq")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_chjal")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_cjqg0")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_vchkt")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_txyw0")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_vc5cj")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_nvyfr")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_ty1g6")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_j2jfr")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_bjg73")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_bu4jf")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_o7ddy")
}],
"loop": false,
"name": &"scatter",
"speed": 30.0
}]
[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_c6pm6"]
content_margin_bottom = 20.0
texture = ExtResource("14_tel4y")
@@ -104,6 +417,20 @@ texture = SubResource("CompressedTexture2D_chjal")
[node name="Main" type="Node3D" unique_id=864552263]
script = ExtResource("1_xcpe3")
[node name="vfxController" type="Node3D" parent="." unique_id=968868013]
[node name="box_block" parent="vfxController" unique_id=1480078977 instance=ExtResource("2_chjal")]
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0.5, 0.35, 0.5)
visible = false
[node name="AnimatedSprite3D" type="AnimatedSprite3D" parent="vfxController" unique_id=1360317189]
transform = Transform3D(0.2, 0, 0, 0, -8.742278e-09, 0.15, 0, -0.2, -6.5567085e-09, 0.5, 0.4, 0.5)
visible = false
sprite_frames = SubResource("SpriteFrames_5poiv")
animation = &"freeze_floor"
frame = 29
frame_progress = 1.0
[node name="EnhancedGridMap" type="GridMap" parent="." unique_id=1838552857]
mesh_library = ExtResource("1_110wo")
cell_size = Vector3(1, 0.2, 1)
@@ -1077,6 +1404,14 @@ theme_override_fonts/font = ExtResource("13_j8jky")
theme_override_font_sizes/font_size = 32
text = "X0"
[node name="AnimatedSprite2D" type="AnimatedSprite2D" parent="PlayerBoardUI" unique_id=390404299]
visible = false
position = Vector2(117.56293, 165.96373)
sprite_frames = SubResource("SpriteFrames_0loja")
animation = &"scatter"
frame = 11
frame_progress = 1.0
[node name="GlobalMatchTimer" type="PanelContainer" parent="." unique_id=1714357974]
custom_minimum_size = Vector2(372, 162)
anchors_preset = 5
+12
View File
@@ -413,6 +413,18 @@ func play_scatter_knock():
await vfx_scatter_knock.animation_finished
vfx_scatter_knock.visible = false
@rpc("any_peer", "call_local", "reliable")
func play_playerboard_scatter():
"""Show the one-shot scatter VFX over the playerboard UI, but only on the
local human player's own client (the board UI belongs to them)."""
var is_local = name == str(multiplayer.get_unique_id())
var is_bot_check = is_bot or is_in_group("Bots")
if not is_local or is_bot_check:
return
var main = get_tree().get_root().get_node_or_null("Main")
if main and main.has_method("play_playerboard_scatter_vfx"):
main.play_playerboard_scatter_vfx()
func _init_managers():
movement_manager = load("res://scripts/managers/player_movement_manager.gd").new()
movement_manager.name = "MovementManager"
+2 -1
View File
@@ -2,7 +2,7 @@
[ext_resource type="Script" uid="uid://dyovwailce5tf" path="res://scripts/tekton.gd" id="1_tekton"]
[ext_resource type="Script" uid="uid://c67yq846u8y68" path="res://scripts/tekton_controller.gd" id="2_controller"]
[ext_resource type="PackedScene" uid="uid://statictektonmesh001" path="res://scenes/static_tekton_mesh.tscn" id="3_d2kpk"]
[ext_resource type="PackedScene" uid="uid://df7h7y7y7y7y7" path="res://scenes/static_tekton_mesh.tscn" id="3_d2kpk"]
[ext_resource type="Texture2D" uid="uid://biun2yvglxgij" path="res://assets/graphics/touch_control/grab_tekton.png" id="4_grab_icon"]
[sub_resource type="BoxShape3D" id="BoxShape3D_tekton"]
@@ -17,6 +17,7 @@ script = ExtResource("2_controller")
[node name="Visuals" type="Node3D" parent="." unique_id=1698719440]
[node name="tekton" parent="Visuals" unique_id=2052742928 instance=ExtResource("3_d2kpk")]
transform = Transform3D(-0.2, 0, -1.7484556e-08, 0, 0.2, 0, 1.7484556e-08, 0, -0.2, 0, 0, 0)
[node name="HitArea" type="Area3D" parent="." unique_id=2139590311]
collision_layer = 4
+8
View File
@@ -72,6 +72,13 @@ func on_create_room_pressed() -> void:
lobby._sync_room_profile_card()
func host_room(game_mode: String) -> void:
# Guard against double-clicks: the create flow is async, and a second press
# before it finishes re-enters create_match() while the bridge is still
# JOINING, which throws "Cannot create match when state is JOINING".
if lobby._is_hosting:
return
lobby._is_hosting = true
if AuthManager.is_guest:
if LobbyManager.local_player_name.is_empty() or LobbyManager.local_player_name == "Player":
LobbyManager.local_player_name = NameGenerator.generate_guest_name()
@@ -92,6 +99,7 @@ func host_room(game_mode: String) -> void:
var ok = await LobbyManager.create_room_lan(room_label)
if not ok:
lobby.connection_status.text = "Failed to start LAN room. Check port 7777."
lobby._is_hosting = false
else:
lobby.connection_status.text = "Creating Nakama room..."
var room_label := "%sRoom %d" % [mode_prefix, randi_range(1000, 9999)]
+20 -38
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,6 +447,7 @@ 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
@@ -489,6 +463,7 @@ func _execute_block_floor(target_pos: Vector2i = Vector2i(-999, -999)):
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({
@@ -500,6 +475,13 @@ func _execute_block_floor(target_pos: Vector2i = Vector2i(-999, -999)):
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")
NotificationManager.send_message(player, "Defensive Wall Deployed!", NotificationManager.MessageType.POWERUP)
+8
View File
@@ -1026,6 +1026,14 @@ func _scatter_player_tiles(player_node: Node):
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():
if player_node.has_method("trigger_screen_shake"):
+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)