diff --git a/assets/graphics/vfx/tile_vfx_freeze/freezeatas.png b/assets/graphics/vfx/tile_vfx_freeze/freezeatas.png new file mode 100644 index 0000000..9e572de Binary files /dev/null and b/assets/graphics/vfx/tile_vfx_freeze/freezeatas.png differ diff --git a/assets/graphics/vfx/tile_vfx_freeze/freezeatas.png.import b/assets/graphics/vfx/tile_vfx_freeze/freezeatas.png.import new file mode 100644 index 0000000..28b24f9 --- /dev/null +++ b/assets/graphics/vfx/tile_vfx_freeze/freezeatas.png.import @@ -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 diff --git a/assets/graphics/vfx/tile_vfx_freeze/freezebawah.png b/assets/graphics/vfx/tile_vfx_freeze/freezebawah.png new file mode 100644 index 0000000..598af30 Binary files /dev/null and b/assets/graphics/vfx/tile_vfx_freeze/freezebawah.png differ diff --git a/assets/graphics/vfx/tile_vfx_freeze/freezebawah.png.import b/assets/graphics/vfx/tile_vfx_freeze/freezebawah.png.import new file mode 100644 index 0000000..562bdb5 --- /dev/null +++ b/assets/graphics/vfx/tile_vfx_freeze/freezebawah.png.import @@ -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 diff --git a/assets/models/meshes/box_block.tscn b/assets/models/meshes/box_block.tscn new file mode 100644 index 0000000..cb27c1c --- /dev/null +++ b/assets/models/meshes/box_block.tscn @@ -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")] diff --git a/assets/models/props/Box_block_2.gltf b/assets/models/props/box_block.gltf similarity index 100% rename from assets/models/props/Box_block_2.gltf rename to assets/models/props/box_block.gltf diff --git a/assets/models/props/Box_block_2.gltf.import b/assets/models/props/box_block.gltf.import similarity index 78% rename from assets/models/props/Box_block_2.gltf.import rename to assets/models/props/box_block.gltf.import index 73d477d..ddd587b 100644 --- a/assets/models/props/Box_block_2.gltf.import +++ b/assets/models/props/box_block.gltf.import @@ -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] diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 19525c7..636c3dd 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -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 # ============================================================================= @@ -248,6 +252,13 @@ func _ready(): # Connect UserProfileManager signals 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: diff --git a/scenes/main.gd b/scenes/main.gd index 2f148e9..18b5f79 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -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}, ...] diff --git a/scenes/main.tscn b/scenes/main.tscn index f6fd109..ac5fed4 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -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 diff --git a/scenes/player.gd b/scenes/player.gd index 473928d..cf89590 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -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" diff --git a/scenes/static_tekton.tscn b/scenes/static_tekton.tscn index 4b91ab8..0b5ea12 100644 --- a/scenes/static_tekton.tscn +++ b/scenes/static_tekton.tscn @@ -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 diff --git a/scenes/ui/lobby_main_menu.gd b/scenes/ui/lobby_main_menu.gd index a8c2ad3..dd95960 100644 --- a/scenes/ui/lobby_main_menu.gd +++ b/scenes/ui/lobby_main_menu.gd @@ -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)] diff --git a/scripts/managers/special_tiles_manager.gd b/scripts/managers/special_tiles_manager.gd index 6e17e0b..26e1719 100644 --- a/scripts/managers/special_tiles_manager.gd +++ b/scripts/managers/special_tiles_manager.gd @@ -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") diff --git a/scripts/managers/stop_n_go_manager.gd b/scripts/managers/stop_n_go_manager.gd index 6ebcdae..c126707 100644 --- a/scripts/managers/stop_n_go_manager.gd +++ b/scripts/managers/stop_n_go_manager.gd @@ -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(): diff --git a/scripts/managers/user_profile_manager.gd b/scripts/managers/user_profile_manager.gd index bb33166..d5a8fea 100644 --- a/scripts/managers/user_profile_manager.gd +++ b/scripts/managers/user_profile_manager.gd @@ -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) diff --git a/scripts/nakama_manager.gd b/scripts/nakama_manager.gd index 8cba8e7..5a25f62 100644 --- a/scripts/nakama_manager.gd +++ b/scripts/nakama_manager.gd @@ -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) diff --git a/scripts/tekton_fishing_autoplay.gd b/scripts/tekton_fishing_autoplay.gd index 5abb802..cb72ad9 100644 --- a/scripts/tekton_fishing_autoplay.gd +++ b/scripts/tekton_fishing_autoplay.gd @@ -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)