added so you can choose race when starting the game.
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.7 KiB |
BIN
src/assets/gfx/character_select/characters.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
40
src/assets/gfx/character_select/characters.png.import
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cj3adlbm2ksl4"
|
||||||
|
path="res://.godot/imported/characters.png-553a9ff6a8c9c16b8c48788bb39c11f3.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/gfx/character_select/characters.png"
|
||||||
|
dest_files=["res://.godot/imported/characters.png-553a9ff6a8c9c16b8c48788bb39c11f3.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
|
||||||
BIN
src/assets/gfx/character_select/characters2.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
40
src/assets/gfx/character_select/characters2.png.import
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dp8hmks54a5pn"
|
||||||
|
path="res://.godot/imported/characters2.png-83b775982bc6db3ebbc0de7f992fcc55.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/gfx/character_select/characters2.png"
|
||||||
|
dest_files=["res://.godot/imported/characters2.png-83b775982bc6db3ebbc0de7f992fcc55.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
|
||||||
BIN
src/assets/gfx/character_select/dwarf.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
40
src/assets/gfx/character_select/dwarf.png.import
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://c3vkon4wpa0kw"
|
||||||
|
path="res://.godot/imported/dwarf.png-af088bfc09b3ad67a721acb4793a2349.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/gfx/character_select/dwarf.png"
|
||||||
|
dest_files=["res://.godot/imported/dwarf.png-af088bfc09b3ad67a721acb4793a2349.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
|
||||||
BIN
src/assets/gfx/character_select/elf.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
40
src/assets/gfx/character_select/elf.png.import
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b3r1hyhgtv7sf"
|
||||||
|
path="res://.godot/imported/elf.png-d51237727e8d3e47b74708b9daddac02.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/gfx/character_select/elf.png"
|
||||||
|
dest_files=["res://.godot/imported/elf.png-d51237727e8d3e47b74708b9daddac02.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
|
||||||
BIN
src/assets/gfx/character_select/wizard.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
40
src/assets/gfx/character_select/wizard.png.import
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://d3qv0erfu3xtl"
|
||||||
|
path="res://.godot/imported/wizard.png-88804da8863366e120bc2fc50367834e.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/gfx/character_select/wizard.png"
|
||||||
|
dest_files=["res://.godot/imported/wizard.png-88804da8863366e120bc2fc50367834e.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
|
||||||
BIN
src/assets/gfx/enemies/boss/SpiderBat/Spiderbat death.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://du3q527l26q1d"
|
||||||
|
path="res://.godot/imported/Spiderbat death.png-6a9578b9aaedfddc0a0b3de871bd4fd6.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/gfx/enemies/boss/SpiderBat/Spiderbat death.png"
|
||||||
|
dest_files=["res://.godot/imported/Spiderbat death.png-6a9578b9aaedfddc0a0b3de871bd4fd6.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
|
||||||
BIN
src/assets/gfx/enemies/boss/SpiderBat/down_right.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
40
src/assets/gfx/enemies/boss/SpiderBat/down_right.png.import
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cr5mc326wpob3"
|
||||||
|
path="res://.godot/imported/down_right.png-6d523ff59778709e04a9a3ea67a6b897.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/gfx/enemies/boss/SpiderBat/down_right.png"
|
||||||
|
dest_files=["res://.godot/imported/down_right.png-6d523ff59778709e04a9a3ea67a6b897.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
|
||||||
BIN
src/assets/gfx/enemies/boss/SpiderBat/flying_down.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
40
src/assets/gfx/enemies/boss/SpiderBat/flying_down.png.import
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://fevkanam8r2s"
|
||||||
|
path="res://.godot/imported/flying_down.png-376108b3e3498c8a5a7b1f340a520add.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/gfx/enemies/boss/SpiderBat/flying_down.png"
|
||||||
|
dest_files=["res://.godot/imported/flying_down.png-376108b3e3498c8a5a7b1f340a520add.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
|
||||||
BIN
src/assets/gfx/enemies/boss/SpiderBat/right.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
40
src/assets/gfx/enemies/boss/SpiderBat/right.png.import
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dsb72u2au1rkq"
|
||||||
|
path="res://.godot/imported/right.png-750fa141b376a0cc18fd164159e95203.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/gfx/enemies/boss/SpiderBat/right.png"
|
||||||
|
dest_files=["res://.godot/imported/right.png-750fa141b376a0cc18fd164159e95203.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
|
||||||
@@ -23,6 +23,7 @@ buses/default_bus_layout="uid://psistrevppd1"
|
|||||||
|
|
||||||
[autoload]
|
[autoload]
|
||||||
|
|
||||||
|
GameState="*res://scripts/game_state.gd"
|
||||||
NetworkManager="*res://scripts/network_manager.gd"
|
NetworkManager="*res://scripts/network_manager.gd"
|
||||||
LogManager="*res://scripts/log_manager.gd"
|
LogManager="*res://scripts/log_manager.gd"
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
[ext_resource type="AudioStream" uid="uid://4vulahdsj4i2" path="res://assets/audio/sfx/swoosh/throw_01.wav.mp3" id="3_v2p0x"]
|
[ext_resource type="AudioStream" uid="uid://4vulahdsj4i2" path="res://assets/audio/sfx/swoosh/throw_01.wav.mp3" id="3_v2p0x"]
|
||||||
[ext_resource type="AudioStream" uid="uid://uerx5rib87a6" path="res://assets/audio/sfx/weapons/bone_hit_wall_01.wav.mp3" id="4_ul7bj"]
|
[ext_resource type="AudioStream" uid="uid://uerx5rib87a6" path="res://assets/audio/sfx/weapons/bone_hit_wall_01.wav.mp3" id="4_ul7bj"]
|
||||||
[ext_resource type="AudioStream" uid="uid://dc7nt8gnjt5u5" path="res://assets/audio/sfx/weapons/melee_attack_12.wav.mp3" id="5_whqew"]
|
[ext_resource type="AudioStream" uid="uid://dc7nt8gnjt5u5" path="res://assets/audio/sfx/weapons/melee_attack_12.wav.mp3" id="5_whqew"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://7r43xnr812km" path="res://assets/gfx/Puny-Characters/Layer 0 - Skins/Human1.png" id="6_whfey"]
|
||||||
|
|
||||||
[sub_resource type="Animation" id="Animation_6bxep"]
|
[sub_resource type="Animation" id="Animation_6bxep"]
|
||||||
length = 0.001
|
length = 0.001
|
||||||
@@ -46,12 +47,609 @@ _data = {
|
|||||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_3jdng"]
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_3jdng"]
|
||||||
size = Vector2(12, 12)
|
size = Vector2(12, 12)
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_whfey"]
|
||||||
|
length = 0.001
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("PlayerTest:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [5]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Sprite2D:position")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(0, 0)]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Sprite2D:rotation")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [0.44505897]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("DamageArea:position")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(0, 0)]
|
||||||
|
}
|
||||||
|
tracks/4/type = "value"
|
||||||
|
tracks/4/imported = false
|
||||||
|
tracks/4/enabled = true
|
||||||
|
tracks/4/path = NodePath("Sprite2D:scale")
|
||||||
|
tracks/4/interp = 1
|
||||||
|
tracks/4/loop_wrap = true
|
||||||
|
tracks/4/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_6tili"]
|
||||||
|
resource_name = "new_animation"
|
||||||
|
length = 0.26982978
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("PlayerTest:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.04016361, 0.10101918, 0.1907425),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [5, 6, 7, 8]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Sprite2D:position")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.039631248, 0.04101525, 0.08727307, 0.100975364, 0.120369375, 0.13681212, 0.15641695, 0.19119969, 0.1968914, 0.26877576),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-8.385, -7.685), Vector2(-8.385, -7.685), Vector2(-8.385, -8.64), Vector2(-8.385, -8.64), Vector2(-12.263, -0.402), Vector2(-8.866, 9.504), Vector2(-2.31, 12.034), Vector2(1.625, 11.899), Vector2(8.165, 6.394), Vector2(8.165, 6.394), Vector2(8.165, 2.224)]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Sprite2D:rotation")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.040804446, 0.08727307, 0.13688956, 0.1914105, 0.19731301, 0.26898658),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [0.44505897, 0.44505897, 0.44505897, -2.199114857512855, -3.6651914291880923, -3.6651914, -4.091051766674709]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("DamageArea:position")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0.00021080452, 0.090856746, 0.099288926, 0.12163421, 0.1403958, 0.18803762, 0.2527546),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-22.394, 0), Vector2(-22.394, 0), Vector2(-32.05, 0), Vector2(-29.735, 12.04), Vector2(-20.673, 11.571), Vector2(-9.739, 9.129), Vector2(-9.254, 3.723)]
|
||||||
|
}
|
||||||
|
tracks/4/type = "value"
|
||||||
|
tracks/4/imported = false
|
||||||
|
tracks/4/enabled = true
|
||||||
|
tracks/4/path = NodePath("Sprite2D:scale")
|
||||||
|
tracks/4/interp = 1
|
||||||
|
tracks/4/loop_wrap = true
|
||||||
|
tracks/4/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_0lt3b"]
|
||||||
|
resource_name = "attack_down_left"
|
||||||
|
length = 0.27031633
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("PlayerTest:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.03993078, 0.10122998, 0.19097532),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [250, 251, 252, 253]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Sprite2D:position")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.040279698, 0.0775326, 0.10221264, 0.19371508),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-6.435, -8), Vector2(-7.305, -7.955), Vector2(-11.835, -3.09), Vector2(-11.74, 4.555), Vector2(-6.375, 11.365)]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Sprite2D:rotation")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.040279698, 0.077299766, 0.10174698, 0.19301659),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [0.4398229715025708, 0.3909537524467294, -0.24783675378319459, -1.0471975511965976, -2.0298179200694055]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("DamageArea:position")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0.00044363513, 0.07292879, 0.1013844, 0.1403958, 0.18827045),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1.5157166),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-25.874, -8.58), Vector2(-31.929, -4.05), Vector2(-33.3, 4.63), Vector2(-30.693, 8.711), Vector2(-27.919, 11.414)]
|
||||||
|
}
|
||||||
|
tracks/4/type = "value"
|
||||||
|
tracks/4/imported = false
|
||||||
|
tracks/4/enabled = true
|
||||||
|
tracks/4/path = NodePath("Sprite2D:scale")
|
||||||
|
tracks/4/interp = 1
|
||||||
|
tracks/4/loop_wrap = true
|
||||||
|
tracks/4/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_3ek3x"]
|
||||||
|
resource_name = "attack_down_right"
|
||||||
|
length = 0.26982978
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("PlayerTest:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.04016361, 0.10122998, 0.1907425),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [40, 41, 42, 43]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Sprite2D:position")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.039631248, 0.04101525, 0.09401881, 0.09971053, 0.11003995, 0.12395305, 0.13681212, 0.15726018, 0.26898655),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-8.385, -7.685), Vector2(-8.385, -7.685), Vector2(-8.385, -8.64), Vector2(-8.385, -8.64), Vector2(-9.993, 7.435), Vector2(-4.231, 11.549), Vector2(2.785, 11.038), Vector2(9.175, 4.754), Vector2(8.95, 2.804), Vector2(8.95, 2.804)]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Sprite2D:rotation")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.040804446, 0.093597196, 0.10076456, 0.10982916, 0.13710035, 0.15747097, 0.26919734),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [0.44505897, 0.44505897, 0.44505897, -1.6423548261266645, -2.1415189921970423, -3.743731245527837, -4.036946559862884, -4.036946559862884]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("DamageArea:position")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0.00021080452, 0.090856746, 0.099288926, 0.12163421, 0.1403958, 0.18803762, 0.2527546),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-22.394, 0), Vector2(-22.394, 0), Vector2(-28.065, 4.33), Vector2(-17.055, 8.945), Vector2(-11.473, 9.766), Vector2(-9.739, 4.169), Vector2(-9.254, 2.583)]
|
||||||
|
}
|
||||||
|
tracks/4/type = "value"
|
||||||
|
tracks/4/imported = false
|
||||||
|
tracks/4/enabled = true
|
||||||
|
tracks/4/path = NodePath("Sprite2D:scale")
|
||||||
|
tracks/4/interp = 1
|
||||||
|
tracks/4/loop_wrap = true
|
||||||
|
tracks/4/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_3qvhs"]
|
||||||
|
resource_name = "attack_left"
|
||||||
|
length = 0.27031633
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("PlayerTest:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.04016361, 0.10122998, 0.19097532),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [215, 216, 217, 218]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Sprite2D:position")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.040279698, 0.0775326, 0.10221264, 0.19371508),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(11.015, -5.165), Vector2(8.29, -7.325), Vector2(-1.53, -8.86), Vector2(-10.76, -1.445), Vector2(-10.47, 8.395)]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Sprite2D:rotation")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.040279698, 0.077299766, 0.10174698, 0.19324942),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [1.722639971718403, 1.349139511791617, 0.49741883681838395, -0.3019419605950192, -1.4364059743913333]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("DamageArea:position")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0.00044363513, 0.07292879, 0.1013844, 0.1403958, 0.18803762),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-9.294, -5.345), Vector2(-19.749, -8.55), Vector2(-32.415, -5.325), Vector2(-32.588, 0.961), Vector2(-32.564, 7.954)]
|
||||||
|
}
|
||||||
|
tracks/4/type = "value"
|
||||||
|
tracks/4/imported = false
|
||||||
|
tracks/4/enabled = true
|
||||||
|
tracks/4/path = NodePath("Sprite2D:scale")
|
||||||
|
tracks/4/interp = 1
|
||||||
|
tracks/4/loop_wrap = true
|
||||||
|
tracks/4/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_58psj"]
|
||||||
|
resource_name = "attack_right"
|
||||||
|
length = 0.26982978
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("PlayerTest:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.04016361, 0.10122998, 0.1907425),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [75, 76, 77, 77]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Sprite2D:position")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.04101525, 0.09401881, 0.09971053, 0.11003995, 0.12395305, 0.13681212, 0.15726018, 0.26898655),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-6.16, -7.685), Vector2(-6.16, -8.64), Vector2(-6.16, -8.64), Vector2(-9.993, 7.435), Vector2(-4.231, 11.549), Vector2(2.785, 11.038), Vector2(9.175, 4.754), Vector2(8.95, 2.804), Vector2(10.545, -0.231)]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Sprite2D:rotation")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.040804446, 0.093597196, 0.10076456, 0.10982916, 0.13710035, 0.15747097, 0.26919734),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [0.44505897, 0.44505897, 0.44505897, -1.6423548261266645, -2.1415189921970423, -3.743731245527837, -4.036946559862884, -4.356341812977846]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("DamageArea:position")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0.00021080452, 0.090856746, 0.099288926, 0.12163421, 0.1403958, 0.18803762, 0.26814333),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-22.394, 0), Vector2(-22.394, 0), Vector2(-28.065, 4.33), Vector2(-17.055, 8.945), Vector2(-11.473, 9.766), Vector2(-9.739, 4.169), Vector2(-5.094, 0.598)]
|
||||||
|
}
|
||||||
|
tracks/4/type = "value"
|
||||||
|
tracks/4/imported = false
|
||||||
|
tracks/4/enabled = true
|
||||||
|
tracks/4/path = NodePath("Sprite2D:scale")
|
||||||
|
tracks/4/interp = 1
|
||||||
|
tracks/4/loop_wrap = true
|
||||||
|
tracks/4/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_b4m4v"]
|
||||||
|
resource_name = "attack_up"
|
||||||
|
length = 0.27031633
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("PlayerTest:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.04016361, 0.10122998, 0.1907425),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [145, 146, 147, 148]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Sprite2D:position")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.04068527, 0.070071764, 0.075806916, 0.07635719, 0.114121936, 0.14789572, 0.19222274, 0.22793879, 0.2691677),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(8.04, -7.355), Vector2(8.225, -8.145), Vector2(8.04, -9.18), Vector2(8.04, -9.18), Vector2(12, 0.49), Vector2(7.546, -5.247), Vector2(-4.945, -10.381), Vector2(-7.771, -5.086), Vector2(-8.07, -1.565), Vector2(-8.07, -1.565)]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Sprite2D:rotation")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.04068527, 0.069488026, 0.07536515, 0.07555858, 0.07566956, 0.114121936, 0.1478039, 0.19203907, 0.22792329, 0.2691522),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [-0.5131268000863325, -0.5131268000863325, -0.5131268000863325, -0.5131268000863325, -3.2288591161895095, -4.134684997974567, -4.717624968140673, -6.005677956112488, -6.283185307179586, -6.806784082777885, -6.806784082777885]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("DamageArea:position")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0.00021080452, 0.090856746, 0.09463231, 0.1403958, 0.18803762, 0.26837614),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-22.394, 0), Vector2(-22.394, 0), Vector2(-7, 2.075), Vector2(-19.748, -11.654), Vector2(-28.329, -7.576), Vector2(-30.434, -2.707)]
|
||||||
|
}
|
||||||
|
tracks/4/type = "value"
|
||||||
|
tracks/4/imported = false
|
||||||
|
tracks/4/enabled = true
|
||||||
|
tracks/4/path = NodePath("Sprite2D:scale")
|
||||||
|
tracks/4/interp = 1
|
||||||
|
tracks/4/loop_wrap = true
|
||||||
|
tracks/4/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.07590186, 0.07619396, 0.26969346),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-1, 1), Vector2(-1, 1), Vector2(1, 1), Vector2(1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_r6tgr"]
|
||||||
|
resource_name = "attack_up_left"
|
||||||
|
length = 0.27031633
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("PlayerTest:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.04016361, 0.10122998, 0.19097532),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [180, 181, 182, 183]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Sprite2D:position")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.04068527, 0.0696061, 0.071848795, 0.07239907, 0.09828946, 0.10197981, 0.14789572, 0.19222274, 0.22793879, 0.2691677),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(3.075, -7.355), Vector2(3.385, -8.145), Vector2(4.125, -9.18), Vector2(8.04, -9.18), Vector2(7.46, -4.12), Vector2(-3.684, -5.822), Vector2(-5.898, -5.584), Vector2(-10.925, -0.251), Vector2(-12.136, 4.719), Vector2(-11.89, 4.77), Vector2(-11.89, 4.77)]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Sprite2D:rotation")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.04068527, 0.069488026, 0.07140703, 0.07160046, 0.071711436, 0.09875512, 0.1478039, 0.19203907, 0.22792329, 0.2691522),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 0.8408964, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [-0.5131268000863325, -0.5131268000863325, -0.5131268000863325, -0.5131268000863325, -3.2288591161895095, -4.714134309636685, -5.782275811857214, -6.806784082777885, -7.3303828583761845, -7.3303828583761845, -7.3303828583761845]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("DamageArea:position")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0.00021080452, 0.07292879, 0.09486514, 0.1403958, 0.18803762, 0.26837614),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-22.394, 0), Vector2(-10.474, -5.42), Vector2(-20.235, -7.885), Vector2(-32.358, -3.184), Vector2(-34.939, 3.129), Vector2(-34.469, 4.153)]
|
||||||
|
}
|
||||||
|
tracks/4/type = "value"
|
||||||
|
tracks/4/imported = false
|
||||||
|
tracks/4/enabled = true
|
||||||
|
tracks/4/path = NodePath("Sprite2D:scale")
|
||||||
|
tracks/4/interp = 1
|
||||||
|
tracks/4/loop_wrap = true
|
||||||
|
tracks/4/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.07194374, 0.07223584, 0.26969346),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-1, 1), Vector2(-1, 1), Vector2(1, 1), Vector2(1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_masq6"]
|
||||||
|
resource_name = "attack_up_right"
|
||||||
|
length = 0.27031633
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("PlayerTest:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.04016361, 0.10122998, 0.1907425),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [110, 111, 112, 113]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Sprite2D:position")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.04068527, 0.069488026, 0.06983893, 0.09338019, 0.09405232, 0.10155232, 0.107935, 0.17927958, 0.27015913),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(3.885, -7.355), Vector2(3.885, -8.145), Vector2(3.885, -8.145), Vector2(4.89, 7.675), Vector2(4.89, 7.675), Vector2(4.89, 7.675), Vector2(11.41, 4.38), Vector2(11.52, -0.24), Vector2(8.145, -7.405), Vector2(8.145, -7.405)]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Sprite2D:rotation")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.04068527, 0.069488026, 0.06982343, 0.093364686, 0.09403682, 0.10157588, 0.10791949, 0.17904675, 0.26969346),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [-0.5131268000863325, -0.5131268000863325, -0.5131268000863325, -3.2288591161895095, -3.2288591161895095, -3.2288591161895095, -3.9217548292312583, -4.508185457901353, -5.040510879759624, -5.040510879759624]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("DamageArea:position")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0.00021080452, 0.090856746, 0.099288926, 0.1403958, 0.18803762, 0.26837614),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-22.394, 0), Vector2(-22.394, 0), Vector2(-8.56, 5.56), Vector2(-8.968, -4.474), Vector2(-11.929, -10.041), Vector2(-12.239, -8.572)]
|
||||||
|
}
|
||||||
|
tracks/4/type = "value"
|
||||||
|
tracks/4/imported = false
|
||||||
|
tracks/4/enabled = true
|
||||||
|
tracks/4/path = NodePath("Sprite2D:scale")
|
||||||
|
tracks/4/interp = 1
|
||||||
|
tracks/4/loop_wrap = true
|
||||||
|
tracks/4/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.06944155, 0.0697763, 0.26969346),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-1, 1), Vector2(-1, 1), Vector2(1, 1), Vector2(1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="AnimationLibrary" id="AnimationLibrary_3ek3x"]
|
||||||
|
_data = {
|
||||||
|
&"RESET": SubResource("Animation_whfey"),
|
||||||
|
&"attack_down": SubResource("Animation_6tili"),
|
||||||
|
&"attack_down_left": SubResource("Animation_0lt3b"),
|
||||||
|
&"attack_down_right": SubResource("Animation_3ek3x"),
|
||||||
|
&"attack_left": SubResource("Animation_3qvhs"),
|
||||||
|
&"attack_right": SubResource("Animation_58psj"),
|
||||||
|
&"attack_up": SubResource("Animation_b4m4v"),
|
||||||
|
&"attack_up_left": SubResource("Animation_r6tgr"),
|
||||||
|
&"attack_up_right": SubResource("Animation_masq6")
|
||||||
|
}
|
||||||
|
|
||||||
[node name="AxeSwing" type="Node2D" unique_id=1568208090]
|
[node name="AxeSwing" type="Node2D" unique_id=1568208090]
|
||||||
z_index = 10
|
z_index = 10
|
||||||
y_sort_enabled = true
|
y_sort_enabled = true
|
||||||
script = ExtResource("1_xo3v0")
|
script = ExtResource("1_xo3v0")
|
||||||
|
|
||||||
|
[node name="PlayerTest" type="Sprite2D" parent="." unique_id=1790922634]
|
||||||
|
visible = false
|
||||||
|
texture = ExtResource("6_whfey")
|
||||||
|
hframes = 35
|
||||||
|
vframes = 8
|
||||||
|
frame = 5
|
||||||
|
|
||||||
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=461038063]
|
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=461038063]
|
||||||
|
z_index = 1
|
||||||
|
rotation = 0.44505897
|
||||||
texture = ExtResource("2_hb10f")
|
texture = ExtResource("2_hb10f")
|
||||||
hframes = 20
|
hframes = 20
|
||||||
vframes = 14
|
vframes = 14
|
||||||
@@ -66,10 +664,11 @@ pitch_scale = 0.74
|
|||||||
autoplay = true
|
autoplay = true
|
||||||
|
|
||||||
[node name="DamageArea" type="Area2D" parent="." unique_id=985585639]
|
[node name="DamageArea" type="Area2D" parent="." unique_id=985585639]
|
||||||
collision_layer = 0
|
collision_layer = 4
|
||||||
collision_mask = 75
|
collision_mask = 3
|
||||||
|
|
||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="DamageArea" unique_id=805714782]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="DamageArea" unique_id=805714782]
|
||||||
|
position = Vector2(20, 0)
|
||||||
shape = SubResource("RectangleShape2D_3jdng")
|
shape = SubResource("RectangleShape2D_3jdng")
|
||||||
debug_color = Color(0.7, 0, 0.18232, 0.42)
|
debug_color = Color(0.7, 0, 0.18232, 0.42)
|
||||||
|
|
||||||
@@ -85,5 +684,8 @@ volume_db = -5.622
|
|||||||
pitch_scale = 1.43
|
pitch_scale = 1.43
|
||||||
max_polyphony = 4
|
max_polyphony = 4
|
||||||
|
|
||||||
|
[node name="SwingAnimation" type="AnimationPlayer" parent="." unique_id=1169276342]
|
||||||
|
libraries/ = SubResource("AnimationLibrary_3ek3x")
|
||||||
|
|
||||||
[connection signal="area_entered" from="DamageArea" to="." method="_on_damage_area_area_entered"]
|
[connection signal="area_entered" from="DamageArea" to="." method="_on_damage_area_area_entered"]
|
||||||
[connection signal="body_entered" from="DamageArea" to="." method="_on_damage_area_body_entered"]
|
[connection signal="body_entered" from="DamageArea" to="." method="_on_damage_area_body_entered"]
|
||||||
|
|||||||
43
src/scenes/attack_punch.tscn
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
[gd_scene format=3 uid="uid://hldlevntj8c2"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://ddqd1nlmsb8k6" path="res://scripts/attack_punch.gd" id="1_script"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="2_tex"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://4vulahdsj4i2" path="res://assets/audio/sfx/swoosh/throw_01.wav.mp3" id="3_swosh"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://dc7nt8gnjt5u5" path="res://assets/audio/sfx/weapons/melee_attack_12.wav.mp3" id="4_impact"]
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_punch"]
|
||||||
|
|
||||||
|
[node name="AttackPunch" type="Node2D" unique_id=6975553]
|
||||||
|
z_index = 4
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1707910142]
|
||||||
|
texture = ExtResource("2_tex")
|
||||||
|
hframes = 105
|
||||||
|
vframes = 79
|
||||||
|
frame = 108
|
||||||
|
|
||||||
|
[node name="Area2D" type="Area2D" parent="." unique_id=1216417398]
|
||||||
|
collision_layer = 4
|
||||||
|
collision_mask = 3
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D" unique_id=689895866]
|
||||||
|
shape = SubResource("RectangleShape2D_punch")
|
||||||
|
|
||||||
|
[node name="SfxSwosh" type="AudioStreamPlayer2D" parent="." unique_id=1177568406]
|
||||||
|
stream = ExtResource("3_swosh")
|
||||||
|
pitch_scale = 0.67
|
||||||
|
max_distance = 983.0
|
||||||
|
attenuation = 7.999991
|
||||||
|
panning_strength = 1.1
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[node name="SfxImpact" type="AudioStreamPlayer2D" parent="." unique_id=1273542180]
|
||||||
|
stream = ExtResource("4_impact")
|
||||||
|
volume_db = -5.622
|
||||||
|
pitch_scale = 1.43
|
||||||
|
max_distance = 983.0
|
||||||
|
attenuation = 7.999991
|
||||||
|
max_polyphony = 4
|
||||||
|
panning_strength = 1.16
|
||||||
|
bus = &"Sfx"
|
||||||
@@ -43,5 +43,6 @@ shape = SubResource("RectangleShape2D_frost")
|
|||||||
stream = ExtResource("3_y7fsv")
|
stream = ExtResource("3_y7fsv")
|
||||||
max_distance = 1456.0
|
max_distance = 1456.0
|
||||||
attenuation = 5.4641595
|
attenuation = 5.4641595
|
||||||
|
max_polyphony = 3
|
||||||
panning_strength = 1.06
|
panning_strength = 1.06
|
||||||
bus = &"Sfx"
|
bus = &"Sfx"
|
||||||
|
|||||||
14
src/scenes/damage_effect_arrow.tscn
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[gd_scene format=3 uid="uid://damage_effect_arrow"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/damage_effect_arrow.gd" id="1_script"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="2_tex"]
|
||||||
|
|
||||||
|
[node name="DamageEffectArrow" type="Node2D"]
|
||||||
|
z_index = 5
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="FxSprite" type="Sprite2D" parent="."]
|
||||||
|
texture = ExtResource("2_tex")
|
||||||
|
hframes = 105
|
||||||
|
vframes = 79
|
||||||
|
frame = 335
|
||||||
14
src/scenes/damage_effect_axe.tscn
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[gd_scene format=3 uid="uid://damage_effect_axe"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/damage_effect_axe.gd" id="1_script"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="2_tex"]
|
||||||
|
|
||||||
|
[node name="DamageEffectAxe" type="Node2D"]
|
||||||
|
z_index = 5
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="FxSprite" type="Sprite2D" parent="."]
|
||||||
|
texture = ExtResource("2_tex")
|
||||||
|
hframes = 105
|
||||||
|
vframes = 79
|
||||||
|
frame = 1158
|
||||||
14
src/scenes/damage_effect_bite.tscn
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[gd_scene format=3 uid="uid://damage_effect_bite"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/damage_effect_bite.gd" id="1_script"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="2_tex"]
|
||||||
|
|
||||||
|
[node name="DamageEffectBite" type="Node2D"]
|
||||||
|
z_index = 5
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="FxSprite" type="Sprite2D" parent="."]
|
||||||
|
texture = ExtResource("2_tex")
|
||||||
|
hframes = 105
|
||||||
|
vframes = 79
|
||||||
|
frame = 148
|
||||||
14
src/scenes/damage_effect_punch.tscn
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[gd_scene format=3 uid="uid://damage_effect_punch"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/damage_effect_punch.gd" id="1_script"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="2_tex"]
|
||||||
|
|
||||||
|
[node name="DamageEffectPunch" type="Node2D"]
|
||||||
|
z_index = 5
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="FxSprite" type="Sprite2D" parent="."]
|
||||||
|
texture = ExtResource("2_tex")
|
||||||
|
hframes = 105
|
||||||
|
vframes = 79
|
||||||
|
frame = 451
|
||||||
14
src/scenes/damage_effect_slash.tscn
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[gd_scene format=3 uid="uid://damage_effect_slash"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scripts/damage_effect_slash.gd" id="1_script"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="2_tex"]
|
||||||
|
|
||||||
|
[node name="DamageEffectSlash" type="Node2D"]
|
||||||
|
z_index = 5
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="FxSprite" type="Sprite2D" parent="."]
|
||||||
|
texture = ExtResource("2_tex")
|
||||||
|
hframes = 105
|
||||||
|
vframes = 79
|
||||||
|
frame = 948
|
||||||
@@ -114,10 +114,10 @@ color = Color(0.69140625, 0.69140625, 0.69140625, 1)
|
|||||||
|
|
||||||
[node name="SfxWinds" type="AudioStreamPlayer" parent="." unique_id=1563020465]
|
[node name="SfxWinds" type="AudioStreamPlayer" parent="." unique_id=1563020465]
|
||||||
stream = ExtResource("6_6c6v5")
|
stream = ExtResource("6_6c6v5")
|
||||||
volume_db = -20.411
|
volume_db = -80.0
|
||||||
autoplay = true
|
autoplay = true
|
||||||
bus = &"Sfx"
|
bus = &"Sfx"
|
||||||
|
|
||||||
[node name="BgMusic" type="AudioStreamPlayer" parent="." unique_id=925983703]
|
[node name="BgMusic" type="AudioStreamPlayer" parent="." unique_id=925983703]
|
||||||
stream = ExtResource("8_pdbwf")
|
stream = ExtResource("8_pdbwf")
|
||||||
volume_db = -15.864
|
volume_db = -80.0
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ layout_mode = 3
|
|||||||
anchors_preset = 15
|
anchors_preset = 15
|
||||||
anchor_right = 1.0
|
anchor_right = 1.0
|
||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 2.0
|
||||||
|
offset_top = -9.0
|
||||||
|
offset_right = 2.0
|
||||||
|
offset_bottom = -9.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
|
|
||||||
@@ -131,6 +135,10 @@ autowrap_mode = 2
|
|||||||
custom_minimum_size = Vector2(0, 20)
|
custom_minimum_size = Vector2(0, 20)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="SelectRaceButton" type="Button" parent="Control/MainMenu/VBoxContainer" unique_id=1061067009]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Choose race"
|
||||||
|
|
||||||
[node name="HostButton" type="Button" parent="Control/MainMenu/VBoxContainer" unique_id=268532531]
|
[node name="HostButton" type="Button" parent="Control/MainMenu/VBoxContainer" unique_id=268532531]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "Host Game"
|
text = "Host Game"
|
||||||
|
|||||||
@@ -702,12 +702,6 @@ collision_mask = 3
|
|||||||
shape = SubResource("CircleShape2D_2")
|
shape = SubResource("CircleShape2D_2")
|
||||||
debug_color = Color(0.70196074, 0.6126261, 0.19635464, 0.41960785)
|
debug_color = Color(0.70196074, 0.6126261, 0.19635464, 0.41960785)
|
||||||
|
|
||||||
[node name="InteractionIndicator" type="Sprite2D" parent="." unique_id=1661043470]
|
|
||||||
visible = false
|
|
||||||
modulate = Color(1, 1, 0, 0.5)
|
|
||||||
position = Vector2(0, -12)
|
|
||||||
scale = Vector2(4, 4)
|
|
||||||
|
|
||||||
[node name="Label" type="Label" parent="." unique_id=227628720]
|
[node name="Label" type="Label" parent="." unique_id=227628720]
|
||||||
offset_left = -10.0
|
offset_left = -10.0
|
||||||
offset_top = -15.0
|
offset_top = -15.0
|
||||||
@@ -715,6 +709,12 @@ offset_right = 10.0
|
|||||||
offset_bottom = -9.0
|
offset_bottom = -9.0
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="InteractionIndicator" type="Sprite2D" parent="." unique_id=1661043470]
|
||||||
|
visible = false
|
||||||
|
modulate = Color(1, 1, 0, 0.5)
|
||||||
|
position = Vector2(0, -12)
|
||||||
|
scale = Vector2(4, 4)
|
||||||
|
|
||||||
[node name="SfxWalk" type="AudioStreamPlayer2D" parent="." unique_id=1693322702]
|
[node name="SfxWalk" type="AudioStreamPlayer2D" parent="." unique_id=1693322702]
|
||||||
stream = SubResource("AudioStreamRandomizer_l71n6")
|
stream = SubResource("AudioStreamRandomizer_l71n6")
|
||||||
volume_db = -18.527
|
volume_db = -18.527
|
||||||
|
|||||||
130
src/scenes/select_class.tscn
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
[gd_scene format=3 uid="uid://ljvf241rr1qj"]
|
||||||
|
|
||||||
|
[ext_resource type="Texture2D" uid="uid://dp8hmks54a5pn" path="res://assets/gfx/character_select/characters2.png" id="1_f45sh"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://cj3adlbm2ksl4" path="res://assets/gfx/character_select/characters.png" id="1_fiono"]
|
||||||
|
[ext_resource type="Script" uid="uid://cwbrfwrwt3krh" path="res://scripts/select_class.gd" id="1_script"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://c3vkon4wpa0kw" path="res://assets/gfx/character_select/dwarf.png" id="2_0qyep"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://b3r1hyhgtv7sf" path="res://assets/gfx/character_select/elf.png" id="3_ewbj8"]
|
||||||
|
[ext_resource type="FontFile" uid="uid://bajcvmidrnc33" path="res://assets/fonts/standard_font.png" id="3_f45sh"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://d3qv0erfu3xtl" path="res://assets/gfx/character_select/wizard.png" id="4_f45sh"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_bsj10"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_8nlan"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_4b24w"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1si1m"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_gkv83"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_3328y"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_q1cbd"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_yyf7y"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_7d5tl"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_1itgt"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_8xviw"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_oflmk"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_rl1cb"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_x8w6g"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_uihst"]
|
||||||
|
|
||||||
|
[node name="SelectClass" type="Node2D" unique_id=126568820]
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="Characters2" type="Sprite2D" parent="." unique_id=2011779728]
|
||||||
|
texture = ExtResource("1_f45sh")
|
||||||
|
|
||||||
|
[node name="Characters" type="Sprite2D" parent="Characters2" unique_id=1197243452]
|
||||||
|
modulate = Color(0.9472463, 0.94050175, 1, 1)
|
||||||
|
texture = ExtResource("1_fiono")
|
||||||
|
|
||||||
|
[node name="Dwarf" type="Sprite2D" parent="." unique_id=1905014075]
|
||||||
|
visible = false
|
||||||
|
texture = ExtResource("2_0qyep")
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="Dwarf" unique_id=1691050946]
|
||||||
|
offset_left = -88.0
|
||||||
|
offset_top = 82.0
|
||||||
|
offset_right = -48.0
|
||||||
|
offset_bottom = 105.0
|
||||||
|
theme_override_fonts/font = ExtResource("3_f45sh")
|
||||||
|
theme_override_font_sizes/font_size = 8
|
||||||
|
text = "DWARF"
|
||||||
|
|
||||||
|
[node name="Elf" type="Sprite2D" parent="." unique_id=172131227]
|
||||||
|
visible = false
|
||||||
|
texture = ExtResource("3_ewbj8")
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="Elf" unique_id=1853931362]
|
||||||
|
offset_left = 48.0
|
||||||
|
offset_top = 82.0
|
||||||
|
offset_right = 88.0
|
||||||
|
offset_bottom = 105.0
|
||||||
|
theme_override_fonts/font = ExtResource("3_f45sh")
|
||||||
|
theme_override_font_sizes/font_size = 8
|
||||||
|
text = "ELF"
|
||||||
|
|
||||||
|
[node name="Wizard" type="Sprite2D" parent="." unique_id=718483736]
|
||||||
|
visible = false
|
||||||
|
texture = ExtResource("4_f45sh")
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="Wizard" unique_id=244634907]
|
||||||
|
offset_left = -26.0
|
||||||
|
offset_top = 85.0
|
||||||
|
offset_right = 22.0
|
||||||
|
offset_bottom = 108.0
|
||||||
|
theme_override_fonts/font = ExtResource("3_f45sh")
|
||||||
|
theme_override_font_sizes/font_size = 8
|
||||||
|
text = "WIZARD"
|
||||||
|
|
||||||
|
[node name="Control" type="Control" parent="." unique_id=952415672]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 0
|
||||||
|
offset_right = 40.0
|
||||||
|
offset_bottom = 40.0
|
||||||
|
|
||||||
|
[node name="ButtonDwarf" type="Button" parent="Control" unique_id=70318818]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = -116.0
|
||||||
|
offset_top = -27.0
|
||||||
|
offset_right = -44.0
|
||||||
|
offset_bottom = 90.0
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxEmpty_bsj10")
|
||||||
|
theme_override_styles/pressed = SubResource("StyleBoxEmpty_8nlan")
|
||||||
|
theme_override_styles/hover = SubResource("StyleBoxEmpty_4b24w")
|
||||||
|
theme_override_styles/disabled = SubResource("StyleBoxEmpty_1si1m")
|
||||||
|
theme_override_styles/focus = SubResource("StyleBoxEmpty_gkv83")
|
||||||
|
|
||||||
|
[node name="ButtonElf" type="Button" parent="Control" unique_id=860222517]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 37.0
|
||||||
|
offset_top = -32.0
|
||||||
|
offset_right = 126.0
|
||||||
|
offset_bottom = 90.0
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxEmpty_3328y")
|
||||||
|
theme_override_styles/pressed = SubResource("StyleBoxEmpty_q1cbd")
|
||||||
|
theme_override_styles/hover = SubResource("StyleBoxEmpty_yyf7y")
|
||||||
|
theme_override_styles/disabled = SubResource("StyleBoxEmpty_7d5tl")
|
||||||
|
theme_override_styles/focus = SubResource("StyleBoxEmpty_1itgt")
|
||||||
|
|
||||||
|
[node name="ButtonWizard" type="Button" parent="Control" unique_id=170883452]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = -44.0
|
||||||
|
offset_top = -65.0
|
||||||
|
offset_right = 37.0
|
||||||
|
offset_bottom = 94.0
|
||||||
|
theme_override_styles/normal = SubResource("StyleBoxEmpty_8xviw")
|
||||||
|
theme_override_styles/pressed = SubResource("StyleBoxEmpty_oflmk")
|
||||||
|
theme_override_styles/hover = SubResource("StyleBoxEmpty_rl1cb")
|
||||||
|
theme_override_styles/disabled = SubResource("StyleBoxEmpty_x8w6g")
|
||||||
|
theme_override_styles/focus = SubResource("StyleBoxEmpty_uihst")
|
||||||
@@ -2,12 +2,14 @@ extends CharacterBody2D
|
|||||||
|
|
||||||
var speed = 300
|
var speed = 300
|
||||||
var direction = Vector2.ZERO
|
var direction = Vector2.ZERO
|
||||||
var stick_duration = 3.0 # How long the arrow stays stuck to enemies/players
|
var stick_duration = 3.0 # How long the arrow stays stuck to players
|
||||||
|
var stick_duration_enemy = 1.2 # Shorter when stuck to enemy - fade out faster
|
||||||
var wall_stick_duration = 30.0 # Much longer duration for wall-stuck arrows (30 seconds)
|
var wall_stick_duration = 30.0 # Much longer duration for wall-stuck arrows (30 seconds)
|
||||||
var is_stuck = false
|
var is_stuck = false
|
||||||
var is_collected = false
|
var is_collected = false
|
||||||
var stick_timer = 0.0
|
var stick_timer = 0.0
|
||||||
var stuck_to_wall = false # Track if stuck to wall (vs enemy/player)
|
var stuck_to_wall = false # Track if stuck to wall (vs enemy/player)
|
||||||
|
var stuck_to_enemy = false # Track if stuck to enemy (use shorter duration)
|
||||||
var can_be_collected = false # True after delay on wall
|
var can_be_collected = false # True after delay on wall
|
||||||
var shooter_can_collect = false # Shooter can collect after 0.2 seconds
|
var shooter_can_collect = false # Shooter can collect after 0.2 seconds
|
||||||
var shooter_collection_delay = 0.2 # Fast pickup for shooter
|
var shooter_collection_delay = 0.2 # Fast pickup for shooter
|
||||||
@@ -66,7 +68,7 @@ func _initialize_arrow() -> void:
|
|||||||
#var scale_factor = 0.28 + abs(velocity.x) / velocity_magnitude # Adjust the factor to your preference
|
#var scale_factor = 0.28 + abs(velocity.x) / velocity_magnitude # Adjust the factor to your preference
|
||||||
|
|
||||||
# Apply the scaling to the shadow
|
# Apply the scaling to the shadow
|
||||||
shadow.rotation = -(angle - PI / 2)
|
shadow.rotation = - (angle - PI / 2)
|
||||||
|
|
||||||
func shoot(shoot_direction: Vector2, start_pos: Vector2, owner_player: Node = null, charge_percentage: float = 1.0) -> void:
|
func shoot(shoot_direction: Vector2, start_pos: Vector2, owner_player: Node = null, charge_percentage: float = 1.0) -> void:
|
||||||
direction = shoot_direction.normalized()
|
direction = shoot_direction.normalized()
|
||||||
@@ -91,11 +93,9 @@ func _process(delta: float) -> void:
|
|||||||
# Track flight time and "land" arrow after max_flight_duration
|
# Track flight time and "land" arrow after max_flight_duration
|
||||||
flight_timer += delta
|
flight_timer += delta
|
||||||
if flight_timer >= max_flight_duration:
|
if flight_timer >= max_flight_duration:
|
||||||
# Arrow has flown for max duration - "land" it (stop and stick to ground)
|
# Defer landing so any body_entered from this frame's physics runs first.
|
||||||
can_deal_damage = false
|
# Otherwise we can set can_deal_damage=false before the overlap is processed.
|
||||||
$SfxLandsOnGround.play()
|
call_deferred("_land_arrow_from_flight")
|
||||||
_stick_to_wall() # Land on ground
|
|
||||||
print("Arrow landed after flying for ", flight_timer, " seconds")
|
|
||||||
return # Exit early to prevent further movement this frame
|
return # Exit early to prevent further movement this frame
|
||||||
|
|
||||||
# Continue flying
|
# Continue flying
|
||||||
@@ -115,8 +115,14 @@ func _process(delta: float) -> void:
|
|||||||
if stick_timer >= others_collection_delay and not can_be_collected:
|
if stick_timer >= others_collection_delay and not can_be_collected:
|
||||||
can_be_collected = true
|
can_be_collected = true
|
||||||
|
|
||||||
# Use appropriate duration based on what it's stuck to
|
# Use appropriate duration based on what it's stuck to (enemy = faster fade)
|
||||||
var duration = wall_stick_duration if stuck_to_wall else stick_duration
|
var duration: float
|
||||||
|
if stuck_to_wall:
|
||||||
|
duration = wall_stick_duration
|
||||||
|
elif stuck_to_enemy:
|
||||||
|
duration = stick_duration_enemy
|
||||||
|
else:
|
||||||
|
duration = stick_duration
|
||||||
|
|
||||||
if stick_timer >= duration:
|
if stick_timer >= duration:
|
||||||
# Start fading out after it sticks
|
# Start fading out after it sticks
|
||||||
@@ -157,29 +163,9 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
if body in hit_targets:
|
if body in hit_targets:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Deal damage to players
|
# Ignore other players - arrow passes through (no friendly fire)
|
||||||
if body.is_in_group("player") and body.has_method("rpc_take_damage"):
|
if body.is_in_group("player") and body.has_method("rpc_take_damage"):
|
||||||
# Add to hit_targets to prevent multiple hits on this target
|
|
||||||
hit_targets[body] = true
|
hit_targets[body] = true
|
||||||
|
|
||||||
play_impact()
|
|
||||||
|
|
||||||
# CRITICAL: Stick to target on ALL clients FIRST (before damage check)
|
|
||||||
# This ensures the arrow stops on all clients, not just the authority
|
|
||||||
_stick_to_target(body)
|
|
||||||
|
|
||||||
# CRITICAL: Only the projectile owner (authority) should deal damage to players
|
|
||||||
if player_owner and player_owner.is_multiplayer_authority():
|
|
||||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
|
||||||
var player_peer_id = body.get_multiplayer_authority()
|
|
||||||
if player_peer_id != 0:
|
|
||||||
if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id():
|
|
||||||
body.rpc_take_damage(20.0, attacker_pos) # TODO: Get actual damage from player
|
|
||||||
else:
|
|
||||||
body.rpc_take_damage.rpc_id(player_peer_id, 20.0, attacker_pos)
|
|
||||||
else:
|
|
||||||
body.rpc_take_damage.rpc(20.0, attacker_pos)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Deal damage to enemies
|
# Deal damage to enemies
|
||||||
@@ -205,13 +191,10 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
# MISS - arrow passes through enemy and continues flying!
|
# MISS - arrow passes through enemy and continues flying!
|
||||||
if body.has_method("_show_damage_number"):
|
if body.has_method("_show_damage_number"):
|
||||||
body._show_damage_number(0.0, attacker_pos, false, true, false) # is_miss = true
|
body._show_damage_number(0.0, attacker_pos, false, true, false) # is_miss = true
|
||||||
# Add to hit_targets so we don't check this enemy again
|
|
||||||
hit_targets[body] = true
|
hit_targets[body] = true
|
||||||
# Sync miss to all clients - arrow continues flying
|
# Sync miss to all clients - arrow continues flying
|
||||||
# CRITICAL: Validate body is still valid and use name instead of path
|
if is_inside_tree() and is_instance_valid(self) and is_instance_valid(body) and body.is_inside_tree():
|
||||||
if is_inside_tree() and is_instance_valid(body) and body.is_inside_tree():
|
_sync_arrow_miss_via_gameworld(name, body.name)
|
||||||
_sync_arrow_miss.rpc(body.name)
|
|
||||||
# Don't stick to target - let arrow continue flying
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check enemy dodge chance (based on enemy's DEX stat)
|
# Check enemy dodge chance (based on enemy's DEX stat)
|
||||||
@@ -225,13 +208,10 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
# DODGE - arrow passes through enemy and continues flying!
|
# DODGE - arrow passes through enemy and continues flying!
|
||||||
if body.has_method("_show_damage_number"):
|
if body.has_method("_show_damage_number"):
|
||||||
body._show_damage_number(0.0, attacker_pos, false, false, true) # is_dodged = true
|
body._show_damage_number(0.0, attacker_pos, false, false, true) # is_dodged = true
|
||||||
# Add to hit_targets so we don't check this enemy again
|
|
||||||
hit_targets[body] = true
|
hit_targets[body] = true
|
||||||
# Sync dodge to all clients - arrow continues flying
|
# Sync dodge to all clients - arrow continues flying
|
||||||
# CRITICAL: Validate body is still valid and use name instead of path
|
if is_inside_tree() and is_instance_valid(self) and is_instance_valid(body) and body.is_inside_tree():
|
||||||
if is_inside_tree() and is_instance_valid(body) and body.is_inside_tree():
|
_sync_arrow_dodge_via_gameworld(name, body.name)
|
||||||
_sync_arrow_dodge.rpc(body.name)
|
|
||||||
# Don't stick to target - let arrow continue flying
|
|
||||||
print(body.name, " DODGED arrow! Arrow continues flying...")
|
print(body.name, " DODGED arrow! Arrow continues flying...")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -250,26 +230,39 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
body.rpc_take_damage.rpc(damage, attacker_pos, false)
|
body.rpc_take_damage.rpc(damage, attacker_pos, false)
|
||||||
|
|
||||||
# Sync hit to all clients - arrow sticks
|
# Sync hit to all clients - arrow sticks
|
||||||
# CRITICAL: Validate body is still valid and use name instead of path
|
# CRITICAL: Route through game_world to avoid node path issues
|
||||||
if is_inside_tree() and is_instance_valid(body) and body.is_inside_tree():
|
if is_inside_tree() and is_instance_valid(self) and is_instance_valid(body) and body.is_inside_tree():
|
||||||
_sync_arrow_hit.rpc(body.name)
|
var arrow_name = name
|
||||||
|
_sync_arrow_hit_via_gameworld(arrow_name, body.name)
|
||||||
|
|
||||||
_stick_to_target(body)
|
# Arrow hit effect (sync so all clients see it)
|
||||||
|
var hit_pos = body.global_position
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw and gw.has_method("_sync_arrow_hit_effect"):
|
||||||
|
if multiplayer.is_server():
|
||||||
|
gw._sync_arrow_hit_effect(hit_pos.x, hit_pos.y)
|
||||||
|
if gw.has_method("_rpc_to_ready_peers"):
|
||||||
|
gw._rpc_to_ready_peers("_sync_arrow_hit_effect", [hit_pos.x, hit_pos.y])
|
||||||
|
else:
|
||||||
|
gw._request_arrow_hit_effect.rpc_id(1, hit_pos.x, hit_pos.y)
|
||||||
|
|
||||||
|
_stick_to_target(body, true)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Hit wall or other object
|
# Hit wall or other object
|
||||||
$SfxImpactWall.play()
|
$SfxImpactWall.play()
|
||||||
_stick_to_wall()
|
_stick_to_wall()
|
||||||
|
|
||||||
func _stick_to_target(target: Node2D):
|
func _stick_to_target(target: Node2D, to_enemy: bool = false):
|
||||||
# Stop the arrow
|
# Stop the arrow
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
is_stuck = true
|
is_stuck = true
|
||||||
|
stuck_to_enemy = to_enemy
|
||||||
stick_timer = 0.0
|
stick_timer = 0.0
|
||||||
arrow_area.set_deferred("monitoring", false)
|
arrow_area.set_deferred("monitoring", false)
|
||||||
|
|
||||||
# Calculate the collision point - move arrow slightly back from its direction
|
# Calculate the collision point - move arrow slightly back from its direction
|
||||||
var collision_normal = -direction
|
var collision_normal = - direction
|
||||||
var offset_distance = 8
|
var offset_distance = 8
|
||||||
var stick_position = global_position + (collision_normal * offset_distance)
|
var stick_position = global_position + (collision_normal * offset_distance)
|
||||||
|
|
||||||
@@ -280,6 +273,15 @@ func _stick_to_target(target: Node2D):
|
|||||||
self.set_deferred("global_position", stick_position)
|
self.set_deferred("global_position", stick_position)
|
||||||
self.set_deferred("global_rotation", global_rot)
|
self.set_deferred("global_rotation", global_rot)
|
||||||
|
|
||||||
|
func _land_arrow_from_flight():
|
||||||
|
# Called deferred when flight_timer expired - ensures body_entered from this frame ran first
|
||||||
|
if is_stuck:
|
||||||
|
return
|
||||||
|
can_deal_damage = false
|
||||||
|
$SfxLandsOnGround.play()
|
||||||
|
_stick_to_wall()
|
||||||
|
print("Arrow landed after flying for ", flight_timer, " seconds")
|
||||||
|
|
||||||
func _stick_to_wall():
|
func _stick_to_wall():
|
||||||
# Stop the arrow
|
# Stop the arrow
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
@@ -297,69 +299,43 @@ func _sync_arrow_collected_via_gameworld(arrow_name: String):
|
|||||||
if gw and gw.has_method("_sync_arrow_collected") and multiplayer.has_multiplayer_peer():
|
if gw and gw.has_method("_sync_arrow_collected") and multiplayer.has_multiplayer_peer():
|
||||||
gw._sync_arrow_collected.rpc(arrow_name)
|
gw._sync_arrow_collected.rpc(arrow_name)
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
func _sync_arrow_hit_via_gameworld(arrow_name: String, target_name: String):
|
||||||
func _sync_arrow_hit(target_name: String):
|
# Route arrow hit sync through game_world to avoid node path issues
|
||||||
# Authority determined arrow HIT enemy - stick to it on all clients
|
if arrow_name.is_empty() or target_name.is_empty():
|
||||||
# CRITICAL: Validate arrow is still valid before processing
|
|
||||||
if not is_instance_valid(self) or not is_inside_tree():
|
|
||||||
return
|
return
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw and gw.has_method("_sync_arrow_hit") and multiplayer.has_multiplayer_peer():
|
||||||
|
gw._sync_arrow_hit.rpc(arrow_name, target_name)
|
||||||
|
|
||||||
# Find target by name in Entities node
|
func _sync_arrow_miss_via_gameworld(arrow_name: String, target_name: String):
|
||||||
var target = null
|
# Route arrow miss sync through game_world to avoid node path issues
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
if arrow_name.is_empty() or target_name.is_empty():
|
||||||
if game_world:
|
return
|
||||||
var entities_node = game_world.get_node_or_null("Entities")
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
if entities_node:
|
if gw and gw.has_method("_sync_arrow_miss") and multiplayer.has_multiplayer_peer():
|
||||||
target = entities_node.get_node_or_null(target_name)
|
gw._sync_arrow_miss.rpc(arrow_name, target_name)
|
||||||
|
|
||||||
if not target:
|
func _sync_arrow_dodge_via_gameworld(arrow_name: String, target_name: String):
|
||||||
print("WARNING: Arrow hit target not found: ", target_name)
|
# Route arrow dodge sync through game_world to avoid node path issues
|
||||||
|
if arrow_name.is_empty() or target_name.is_empty():
|
||||||
|
return
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw and gw.has_method("_sync_arrow_dodge") and multiplayer.has_multiplayer_peer():
|
||||||
|
gw._sync_arrow_dodge.rpc(arrow_name, target_name)
|
||||||
|
|
||||||
|
# Helper method for game_world to process arrow hit sync
|
||||||
|
func _process_arrow_hit_sync(target: Node):
|
||||||
|
# Process arrow hit sync from game_world
|
||||||
|
if not is_instance_valid(self) or not is_inside_tree():
|
||||||
return
|
return
|
||||||
|
|
||||||
if target not in hit_targets:
|
if target not in hit_targets:
|
||||||
hit_targets[target] = true
|
hit_targets[target] = true
|
||||||
play_impact()
|
play_impact()
|
||||||
_stick_to_target(target)
|
var is_enemy = target.is_in_group("enemy") if target else false
|
||||||
|
_stick_to_target(target, is_enemy)
|
||||||
print("Arrow synced as HIT to: ", target.name)
|
print("Arrow synced as HIT to: ", target.name)
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
|
||||||
func _sync_arrow_miss(target_name: String):
|
|
||||||
# Authority determined arrow MISSED enemy - continues flying on all clients
|
|
||||||
# CRITICAL: Validate arrow is still valid before processing
|
|
||||||
if not is_instance_valid(self) or not is_inside_tree():
|
|
||||||
return
|
|
||||||
|
|
||||||
# Find target by name in Entities node
|
|
||||||
var target = null
|
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
|
||||||
if game_world:
|
|
||||||
var entities_node = game_world.get_node_or_null("Entities")
|
|
||||||
if entities_node:
|
|
||||||
target = entities_node.get_node_or_null(target_name)
|
|
||||||
|
|
||||||
if target and target not in hit_targets:
|
|
||||||
hit_targets[target] = true
|
|
||||||
print("Arrow synced as MISS - continuing through: ", target.name if target else "unknown")
|
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
|
||||||
func _sync_arrow_dodge(target_name: String):
|
|
||||||
# Authority determined enemy DODGED arrow - continues flying on all clients
|
|
||||||
# CRITICAL: Validate arrow is still valid before processing
|
|
||||||
if not is_instance_valid(self) or not is_inside_tree():
|
|
||||||
return
|
|
||||||
|
|
||||||
# Find target by name in Entities node
|
|
||||||
var target = null
|
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
|
||||||
if game_world:
|
|
||||||
var entities_node = game_world.get_node_or_null("Entities")
|
|
||||||
if entities_node:
|
|
||||||
target = entities_node.get_node_or_null(target_name)
|
|
||||||
|
|
||||||
if target and target not in hit_targets:
|
|
||||||
hit_targets[target] = true
|
|
||||||
print("Arrow synced as DODGE - continuing through: ", target.name if target else "unknown")
|
|
||||||
|
|
||||||
func _enable_collection_area():
|
func _enable_collection_area():
|
||||||
# Create an Area2D for collecting the arrow
|
# Create an Area2D for collecting the arrow
|
||||||
if collection_area:
|
if collection_area:
|
||||||
|
|||||||
@@ -1,64 +1,157 @@
|
|||||||
extends Node2D
|
extends Node2D
|
||||||
|
|
||||||
var direction := Vector2.ZERO # Default direction
|
# Axe Swing - stays on player, plays $SwingAnimation by direction (attack_down, attack_right, etc.).
|
||||||
var fade_delay := 0.14 # When to start fading (mid-move)
|
# Uses equipped axe texture/frame. On hit: deal damage, spawn damage_effect_axe.
|
||||||
var move_duration := 0.2 # Slash exists for 0.3 seconds
|
# Duration ~0.27s to match player AXE/SWORD animation.
|
||||||
var fade_duration := 0.06 # Time to fade out
|
|
||||||
var stretch_amount := Vector2(1, 1.4) # How much to stretch the sprite
|
const LIFETIME: float = 0.27
|
||||||
var slash_amount = 8
|
const DIR_ANIMATIONS: Array = [
|
||||||
var initiated_by: Node2D = null
|
"attack_right", # 0: right
|
||||||
|
"attack_down_right", # 1
|
||||||
|
"attack_down", # 2
|
||||||
|
"attack_down_left", # 3
|
||||||
|
"attack_left", # 4
|
||||||
|
"attack_up_left", # 5
|
||||||
|
"attack_up", # 6
|
||||||
|
"attack_up_right" # 7
|
||||||
|
]
|
||||||
|
|
||||||
|
@export var damage: float = 20.0
|
||||||
|
var elapsed_time: float = 0.0
|
||||||
|
var player_owner: Node = null
|
||||||
|
var hit_targets: Dictionary = {}
|
||||||
|
|
||||||
|
var damage_effect_axe_scene: PackedScene = preload("res://scenes/damage_effect_axe.tscn")
|
||||||
|
|
||||||
|
@onready var sprite: Sprite2D = $Sprite2D
|
||||||
|
@onready var hit_area: Area2D = $DamageArea
|
||||||
|
@onready var swing_animation: AnimationPlayer = $SwingAnimation
|
||||||
|
|
||||||
# Called when the node enters the scene tree for the first time.
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
call_deferred("_initialize_swing")
|
if hit_area:
|
||||||
pass # Replace with function body.
|
if not hit_area.body_entered.is_connected(_on_damage_area_body_entered):
|
||||||
|
hit_area.body_entered.connect(_on_damage_area_body_entered)
|
||||||
|
if has_node("AttackSwosh"):
|
||||||
|
$AttackSwosh.play()
|
||||||
|
|
||||||
func _initialize_swing():
|
func setup(attack_direction: Vector2, owner_player: Node, _arc_direction: float = 1.0, axe_item: Item = null) -> void:
|
||||||
var tween = create_tween()
|
player_owner = owner_player
|
||||||
var move_target = global_position + (direction.normalized() * slash_amount) # Moves in given direction
|
if owner_player and owner_player.character_stats:
|
||||||
tween.set_trans(Tween.TRANS_CUBIC) # Smooth acceleration & deceleration
|
damage = owner_player.character_stats.damage
|
||||||
tween.set_ease(Tween.EASE_OUT) # Fast start, then slows down
|
# Use equipped axe texture and frame
|
||||||
tween.tween_property(self, "global_position", move_target, move_duration)
|
if axe_item and sprite:
|
||||||
'
|
var tex = load(axe_item.spritePath) as Texture2D
|
||||||
# Create stretch tween (grow and shrink slightly)
|
if tex:
|
||||||
var stretch_tween = create_tween()
|
sprite.texture = tex
|
||||||
stretch_tween.set_trans(Tween.TRANS_CUBIC)
|
sprite.hframes = axe_item.spriteFrames.x if axe_item.spriteFrames.x > 0 else 20
|
||||||
stretch_tween.set_ease(Tween.EASE_OUT)
|
sprite.vframes = axe_item.spriteFrames.y if axe_item.spriteFrames.y > 0 else 14
|
||||||
stretch_tween.tween_property($Sprite2D, "scale", Vector2.ONE, move_duration / 2) # start normal
|
sprite.frame = axe_item.spriteFrame
|
||||||
stretch_tween.tween_property($Sprite2D, "scale", stretch_amount, move_duration / 2)
|
# Pick direction animation: 8 sectors by angle
|
||||||
'
|
var dir_norm = attack_direction.normalized()
|
||||||
|
var angle = dir_norm.angle()
|
||||||
|
var sector = int(round(angle / (TAU / 8.0))) % 8
|
||||||
|
if sector < 0:
|
||||||
|
sector += 8
|
||||||
|
var anim_name = DIR_ANIMATIONS[sector] if sector < DIR_ANIMATIONS.size() else "attack_down"
|
||||||
|
if swing_animation and swing_animation.has_animation(anim_name):
|
||||||
|
swing_animation.play(anim_name)
|
||||||
|
|
||||||
# Wait until mid-move to start fade
|
func _process(delta: float) -> void:
|
||||||
await get_tree().create_timer(fade_delay).timeout
|
elapsed_time += delta
|
||||||
|
if player_owner and is_instance_valid(player_owner):
|
||||||
# Start fade-out effect
|
global_position = player_owner.global_position
|
||||||
var fade_tween = create_tween()
|
if elapsed_time >= LIFETIME:
|
||||||
fade_tween.tween_property($Sprite2D, "modulate:a", 0.0, fade_duration) # Fade to transparent
|
|
||||||
await fade_tween.finished
|
|
||||||
queue_free()
|
queue_free()
|
||||||
pass
|
|
||||||
|
|
||||||
func _on_damage_area_body_entered(body: Node2D) -> void:
|
func _on_damage_area_body_entered(body: Node2D) -> void:
|
||||||
if body.get_parent() == initiated_by or body == initiated_by:
|
if body == player_owner:
|
||||||
return
|
return
|
||||||
if body.get_parent() is CharacterBody2D and body.get_parent().stats.is_invulnerable == false and body.get_parent().stats.hp > 0: # hit an enemy
|
if body in hit_targets:
|
||||||
$MeleeImpact.play()
|
|
||||||
body.take_damage(self, initiated_by)
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
$MeleeImpactWall.play()
|
|
||||||
pass
|
|
||||||
pass # Replace with function body.
|
|
||||||
|
|
||||||
|
|
||||||
func _on_damage_area_area_entered(body: Area2D) -> void:
|
|
||||||
if body.get_parent() == initiated_by:
|
|
||||||
return
|
return
|
||||||
if body.get_parent() is CharacterBody2D and body.get_parent().stats.is_invulnerable == false and body.get_parent().stats.hp > 0: # hit an enemy
|
hit_targets[body] = true
|
||||||
$MeleeImpact.play()
|
|
||||||
body.get_parent().take_damage(self, initiated_by)
|
# Only authority deals damage
|
||||||
pass
|
if player_owner and not player_owner.is_multiplayer_authority():
|
||||||
|
return
|
||||||
|
|
||||||
|
var attacker_pos = player_owner.global_position if player_owner and is_instance_valid(player_owner) else global_position
|
||||||
|
|
||||||
|
# Ignore other players
|
||||||
|
if body.is_in_group("player") and body.has_method("take_damage"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Enemy: deal damage via game_world, spawn axe hit effect
|
||||||
|
if body.is_in_group("enemy") and body.has_method("rpc_take_damage"):
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
var enemy_name = body.name
|
||||||
|
var enemy_index = body.get_meta("enemy_index") if body.has_meta("enemy_index") else -1
|
||||||
|
if game_world and game_world.has_method("_request_enemy_damage"):
|
||||||
|
if multiplayer.is_server():
|
||||||
|
game_world._request_enemy_damage(enemy_name, enemy_index, damage, attacker_pos, false)
|
||||||
else:
|
else:
|
||||||
|
game_world._request_enemy_damage.rpc_id(1, enemy_name, enemy_index, damage, attacker_pos, false)
|
||||||
|
else:
|
||||||
|
var eid = body.get_multiplayer_authority()
|
||||||
|
if eid != 0:
|
||||||
|
if multiplayer.is_server() and eid == multiplayer.get_unique_id():
|
||||||
|
body.rpc_take_damage(damage, attacker_pos, false)
|
||||||
|
else:
|
||||||
|
body.rpc_take_damage.rpc_id(eid, damage, attacker_pos, false)
|
||||||
|
else:
|
||||||
|
body.rpc_take_damage.rpc(damage, attacker_pos, false)
|
||||||
|
if has_node("MeleeImpact"):
|
||||||
|
$MeleeImpact.play()
|
||||||
|
_spawn_axe_hit_effect(body.global_position)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Boxes / interactables with health
|
||||||
|
if "health" in body:
|
||||||
|
if has_node("MeleeImpact"):
|
||||||
|
$MeleeImpact.play()
|
||||||
|
body.health -= damage
|
||||||
|
if body.health <= 0 and body.has_method("_break_into_pieces"):
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and multiplayer.has_multiplayer_peer():
|
||||||
|
var obj_name = body.name
|
||||||
|
if body.has_meta("object_index"):
|
||||||
|
var idx = body.get_meta("object_index")
|
||||||
|
if idx >= 0:
|
||||||
|
obj_name = "InteractableObject_%d" % idx
|
||||||
|
if multiplayer.is_server():
|
||||||
|
game_world._rpc_to_ready_peers("_sync_object_break", [obj_name])
|
||||||
|
else:
|
||||||
|
game_world._sync_object_break.rpc_id(1, obj_name)
|
||||||
|
body._break_into_pieces()
|
||||||
|
_spawn_axe_hit_effect(body.global_position)
|
||||||
|
return
|
||||||
|
|
||||||
|
if has_node("MeleeImpactWall"):
|
||||||
$MeleeImpactWall.play()
|
$MeleeImpactWall.play()
|
||||||
pass
|
|
||||||
pass # Replace with function body.
|
# Knockback for CharacterBody2D (e.g. corpses)
|
||||||
|
if body is CharacterBody2D:
|
||||||
|
var knockback_dir = (body.global_position - global_position).normalized()
|
||||||
|
body.velocity = knockback_dir * 200.0
|
||||||
|
|
||||||
|
func _spawn_axe_hit_effect(at_position: Vector2) -> void:
|
||||||
|
if multiplayer.has_multiplayer_peer() and player_owner and player_owner.is_multiplayer_authority():
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_sync_axe_hit_effect"):
|
||||||
|
if multiplayer.is_server():
|
||||||
|
game_world._sync_axe_hit_effect(at_position.x, at_position.y)
|
||||||
|
if game_world.has_method("_rpc_to_ready_peers"):
|
||||||
|
game_world._rpc_to_ready_peers("_sync_axe_hit_effect", [at_position.x, at_position.y])
|
||||||
|
else:
|
||||||
|
game_world._request_axe_hit_effect.rpc_id(1, at_position.x, at_position.y)
|
||||||
|
return
|
||||||
|
if not damage_effect_axe_scene:
|
||||||
|
return
|
||||||
|
var effect = damage_effect_axe_scene.instantiate()
|
||||||
|
var parent = get_parent()
|
||||||
|
if parent:
|
||||||
|
parent.add_child(effect)
|
||||||
|
else:
|
||||||
|
get_tree().current_scene.add_child(effect)
|
||||||
|
effect.global_position = at_position
|
||||||
|
if effect.has_method("setup"):
|
||||||
|
effect.setup(at_position)
|
||||||
|
|||||||
@@ -434,6 +434,15 @@ func _spawn_explosion_tile_particles():
|
|||||||
spr.texture = tex
|
spr.texture = tex
|
||||||
spr.region_enabled = true
|
spr.region_enabled = true
|
||||||
spr.region_rect = regions[i]
|
spr.region_rect = regions[i]
|
||||||
|
|
||||||
|
# CRITICAL: Apply level's material and colorization to tile particles
|
||||||
|
# Get the material from the tilemap layer and duplicate it
|
||||||
|
# Duplicating ShaderMaterial copies all shader parameters (colorization, tint, ambient, etc.)
|
||||||
|
if layer.material and layer.material is ShaderMaterial:
|
||||||
|
var layer_material = layer.material as ShaderMaterial
|
||||||
|
var particle_material = layer_material.duplicate() as ShaderMaterial
|
||||||
|
spr.material = particle_material
|
||||||
|
|
||||||
p.global_position = world
|
p.global_position = world
|
||||||
var speed = randf_range(280.0, 420.0) # Much faster - fly around more
|
var speed = randf_range(280.0, 420.0) # Much faster - fly around more
|
||||||
var d = outward + Vector2(randf_range(-0.4, 0.4), randf_range(-0.4, 0.4))
|
var d = outward + Vector2(randf_range(-0.4, 0.4), randf_range(-0.4, 0.4))
|
||||||
|
|||||||
135
src/scripts/attack_punch.gd
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Unarmed punch - appears in front of player, low STR-based damage.
|
||||||
|
# Uses shade_spell_effects.png frames 108-113 for the punch, spawns damage_effect_punch on hit.
|
||||||
|
|
||||||
|
@export var damage: float = 3.0
|
||||||
|
@export var lifetime: float = 0.25
|
||||||
|
@export var punch_distance: float = 12.0 # Closer than sword (less range)
|
||||||
|
|
||||||
|
const PUNCH_FRAMES: Array = [108, 109, 110, 111, 112, 113]
|
||||||
|
const FRAME_DURATION: float = 0.04
|
||||||
|
|
||||||
|
var punch_direction: Vector2 = Vector2.RIGHT
|
||||||
|
var player_owner: Node = null
|
||||||
|
var hit_targets: Dictionary = {}
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
var damage_effect_punch_scene: PackedScene = preload("res://scenes/damage_effect_punch.tscn")
|
||||||
|
|
||||||
|
@onready var sprite: Sprite2D = $Sprite2D
|
||||||
|
@onready var hit_area: Area2D = $Area2D
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if has_node("SfxSwosh"):
|
||||||
|
$SfxSwosh.play()
|
||||||
|
if hit_area and not hit_area.body_entered.is_connected(_on_body_entered):
|
||||||
|
hit_area.body_entered.connect(_on_body_entered)
|
||||||
|
if sprite and PUNCH_FRAMES.size() > 0:
|
||||||
|
sprite.frame = PUNCH_FRAMES[0]
|
||||||
|
|
||||||
|
func setup(direction: Vector2, owner_player: Node, damage_value: float = 3.0) -> void:
|
||||||
|
punch_direction = direction.normalized()
|
||||||
|
player_owner = owner_player
|
||||||
|
damage = damage_value
|
||||||
|
rotation = punch_direction.angle()
|
||||||
|
# Position in front of player
|
||||||
|
if owner_player and is_instance_valid(owner_player):
|
||||||
|
global_position = owner_player.global_position + punch_direction * punch_distance
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
elapsed += delta
|
||||||
|
# Keep punch in front of player
|
||||||
|
if player_owner and is_instance_valid(player_owner):
|
||||||
|
global_position = player_owner.global_position + punch_direction * punch_distance
|
||||||
|
# Animate frames 108-113
|
||||||
|
if sprite and PUNCH_FRAMES.size() > 0:
|
||||||
|
var frame_idx = min(int(elapsed / FRAME_DURATION), PUNCH_FRAMES.size() - 1)
|
||||||
|
sprite.frame = PUNCH_FRAMES[frame_idx]
|
||||||
|
if elapsed >= lifetime:
|
||||||
|
hit_area.set_deferred("monitoring", false)
|
||||||
|
queue_free()
|
||||||
|
|
||||||
|
func _on_body_entered(body: Node2D) -> void:
|
||||||
|
if body == player_owner:
|
||||||
|
return
|
||||||
|
if body in hit_targets:
|
||||||
|
return
|
||||||
|
hit_targets[body] = true
|
||||||
|
|
||||||
|
# Only authority deals damage (same as sword_projectile)
|
||||||
|
if player_owner and not player_owner.is_multiplayer_authority():
|
||||||
|
return
|
||||||
|
|
||||||
|
var attacker_pos = player_owner.global_position if player_owner and is_instance_valid(player_owner) else global_position
|
||||||
|
|
||||||
|
# Ignore other players (no friendly fire) - pass through
|
||||||
|
if body.is_in_group("player") and body.has_method("rpc_take_damage"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Enemy: apply damage (enemy's take_damage/rpc_take_damage applies defence)
|
||||||
|
if body.is_in_group("enemy") and body.has_method("rpc_take_damage"):
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
var enemy_name = body.name
|
||||||
|
var enemy_index = body.get_meta("enemy_index") if body.has_meta("enemy_index") else -1
|
||||||
|
if game_world and game_world.has_method("_request_enemy_damage"):
|
||||||
|
if multiplayer.is_server():
|
||||||
|
game_world._request_enemy_damage(enemy_name, enemy_index, damage, attacker_pos, false)
|
||||||
|
else:
|
||||||
|
game_world._request_enemy_damage.rpc_id(1, enemy_name, enemy_index, damage, attacker_pos, false)
|
||||||
|
else:
|
||||||
|
var enemy_peer_id = body.get_multiplayer_authority()
|
||||||
|
if enemy_peer_id != 0:
|
||||||
|
if multiplayer.is_server() and enemy_peer_id == multiplayer.get_unique_id():
|
||||||
|
body.rpc_take_damage(damage, attacker_pos, false)
|
||||||
|
else:
|
||||||
|
body.rpc_take_damage.rpc_id(enemy_peer_id, damage, attacker_pos, false)
|
||||||
|
else:
|
||||||
|
body.rpc_take_damage.rpc(damage, attacker_pos, false)
|
||||||
|
if has_node("SfxImpact"):
|
||||||
|
$SfxImpact.play()
|
||||||
|
_spawn_damage_effect(body.global_position)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Interactables with health (boxes, etc.) - small damage
|
||||||
|
if "health" in body:
|
||||||
|
if has_node("SfxImpact"):
|
||||||
|
$SfxImpact.play()
|
||||||
|
body.health -= damage
|
||||||
|
if body.health <= 0 and body.has_method("_break_into_pieces"):
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and multiplayer.has_multiplayer_peer():
|
||||||
|
var obj_name = body.name
|
||||||
|
if body.has_meta("object_index"):
|
||||||
|
var idx = body.get_meta("object_index")
|
||||||
|
if idx >= 0:
|
||||||
|
obj_name = "InteractableObject_%d" % idx
|
||||||
|
if multiplayer.is_server():
|
||||||
|
game_world._rpc_to_ready_peers("_sync_object_break", [obj_name])
|
||||||
|
else:
|
||||||
|
game_world._sync_object_break.rpc_id(1, obj_name)
|
||||||
|
body._break_into_pieces()
|
||||||
|
_spawn_damage_effect(body.global_position)
|
||||||
|
|
||||||
|
func _spawn_damage_effect(at_position: Vector2) -> void:
|
||||||
|
if multiplayer.has_multiplayer_peer() and player_owner and player_owner.is_multiplayer_authority():
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_sync_punch_hit_effect"):
|
||||||
|
if multiplayer.is_server():
|
||||||
|
game_world._sync_punch_hit_effect(at_position.x, at_position.y)
|
||||||
|
if game_world.has_method("_rpc_to_ready_peers"):
|
||||||
|
game_world._rpc_to_ready_peers("_sync_punch_hit_effect", [at_position.x, at_position.y])
|
||||||
|
else:
|
||||||
|
game_world._request_punch_hit_effect.rpc_id(1, at_position.x, at_position.y)
|
||||||
|
return
|
||||||
|
if not damage_effect_punch_scene:
|
||||||
|
return
|
||||||
|
var effect = damage_effect_punch_scene.instantiate()
|
||||||
|
var parent = get_parent()
|
||||||
|
if parent:
|
||||||
|
parent.add_child(effect)
|
||||||
|
else:
|
||||||
|
get_tree().current_scene.add_child(effect)
|
||||||
|
effect.global_position = at_position
|
||||||
|
if effect.has_method("setup"):
|
||||||
|
effect.setup(at_position)
|
||||||
1
src/scripts/attack_punch.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ddqd1nlmsb8k6
|
||||||
@@ -9,6 +9,7 @@ extends Node2D
|
|||||||
var player_owner: Node = null
|
var player_owner: Node = null
|
||||||
var hit_targets = {} # Track what we've already hit
|
var hit_targets = {} # Track what we've already hit
|
||||||
var first_hit_targets = {} # Track targets that haven't taken initial damage yet
|
var first_hit_targets = {} # Track targets that haven't taken initial damage yet
|
||||||
|
var bodies_in_area = {} # Track bodies currently in the area (for enter/exit detection)
|
||||||
var damage_timer: float = 0.0
|
var damage_timer: float = 0.0
|
||||||
var animation_timer: float = 0.0
|
var animation_timer: float = 0.0
|
||||||
var current_frame: int = 4 # Start at frame 4 (first burning frame)
|
var current_frame: int = 4 # Start at frame 4 (first burning frame)
|
||||||
@@ -133,6 +134,7 @@ func _start_sprite_animation():
|
|||||||
func _deal_periodic_damage():
|
func _deal_periodic_damage():
|
||||||
# Get all bodies in the area
|
# Get all bodies in the area
|
||||||
var bodies = hit_area.get_overlapping_bodies()
|
var bodies = hit_area.get_overlapping_bodies()
|
||||||
|
var current_bodies = {}
|
||||||
|
|
||||||
for body in bodies:
|
for body in bodies:
|
||||||
if body == player_owner:
|
if body == player_owner:
|
||||||
@@ -142,16 +144,22 @@ func _deal_periodic_damage():
|
|||||||
if player_owner and not player_owner.is_multiplayer_authority():
|
if player_owner and not player_owner.is_multiplayer_authority():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if this is the first hit on this target (for initial damage bonus)
|
current_bodies[body] = true
|
||||||
|
|
||||||
|
# Check if this body just entered the area (not in bodies_in_area)
|
||||||
|
var just_entered = not (body in bodies_in_area)
|
||||||
|
|
||||||
|
# If body just entered, deal high damage (like initial hit)
|
||||||
|
# Also deal high damage on first hit ever
|
||||||
var is_first_hit = not (body in first_hit_targets)
|
var is_first_hit = not (body in first_hit_targets)
|
||||||
if is_first_hit:
|
if is_first_hit:
|
||||||
first_hit_targets[body] = true
|
first_hit_targets[body] = true
|
||||||
|
|
||||||
# Calculate damage - initial damage is much higher for first hit
|
# Calculate damage - high damage when entering area or first hit, regular damage otherwise
|
||||||
var final_damage = damage
|
var final_damage = damage
|
||||||
var int_bonus_damage = 0.0 # Declare outside if block for use in print statements
|
var int_bonus_damage = 0.0 # Declare outside if block for use in print statements
|
||||||
if is_first_hit:
|
if just_entered or is_first_hit:
|
||||||
# Initial damage is multiplied and gets INT bonus
|
# High damage when entering area (multiplied and gets INT bonus)
|
||||||
final_damage = damage * initial_damage_multiplier
|
final_damage = damage * initial_damage_multiplier
|
||||||
if player_owner and player_owner.character_stats:
|
if player_owner and player_owner.character_stats:
|
||||||
var int_stat = player_owner.character_stats.baseStats.int + player_owner.character_stats.get_pass("int")
|
var int_stat = player_owner.character_stats.baseStats.int + player_owner.character_stats.get_pass("int")
|
||||||
@@ -198,9 +206,9 @@ func _deal_periodic_damage():
|
|||||||
else:
|
else:
|
||||||
body.rpc_take_damage.rpc(final_damage, attacker_pos, false, false, apply_burn)
|
body.rpc_take_damage.rpc(final_damage, attacker_pos, false, false, apply_burn)
|
||||||
|
|
||||||
if is_first_hit:
|
if just_entered or is_first_hit:
|
||||||
var int_bonus = int_bonus_damage if player_owner and player_owner.character_stats else 0.0
|
var int_bonus = int_bonus_damage if player_owner and player_owner.character_stats else 0.0
|
||||||
print("Flame spell INITIAL hit enemy: ", body.name, " for ", final_damage, " damage (base: ", damage, " x ", initial_damage_multiplier, " + INT bonus: ", int_bonus, ")")
|
print("Flame spell HIGH DAMAGE hit enemy: ", body.name, " for ", final_damage, " damage (base: ", damage, " x ", initial_damage_multiplier, " + INT bonus: ", int_bonus, ")")
|
||||||
else:
|
else:
|
||||||
print("Flame spell hit enemy: ", body.name, " for ", final_damage, " damage!")
|
print("Flame spell hit enemy: ", body.name, " for ", final_damage, " damage!")
|
||||||
|
|
||||||
@@ -210,12 +218,19 @@ func _deal_periodic_damage():
|
|||||||
continue # Don't break objects while held
|
continue # Don't break objects while held
|
||||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
var attacker_pos = player_owner.global_position if player_owner else global_position
|
||||||
body.take_fire_damage(final_damage, attacker_pos)
|
body.take_fire_damage(final_damage, attacker_pos)
|
||||||
if is_first_hit:
|
if just_entered or is_first_hit:
|
||||||
|
print("Flame spell HIGH DAMAGE burning wooden object: ", body.name, " for ", final_damage, " damage!")
|
||||||
|
else:
|
||||||
print("Flame spell burning wooden object: ", body.name, " for ", final_damage, " damage!")
|
print("Flame spell burning wooden object: ", body.name, " for ", final_damage, " damage!")
|
||||||
|
|
||||||
func _on_body_entered(_body):
|
# Update bodies_in_area for next check
|
||||||
# Track bodies that enter the area (for periodic damage)
|
bodies_in_area = current_bodies
|
||||||
# Don't add to hit_targets here - we want to deal damage multiple times
|
|
||||||
|
func _on_body_entered(body):
|
||||||
|
# Track bodies that enter the area - they will get high damage on next periodic check
|
||||||
|
# The _deal_periodic_damage function will detect they just entered and deal high damage
|
||||||
|
if body and body != player_owner:
|
||||||
|
# Mark that this body just entered (will be processed in _deal_periodic_damage)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
func _on_lifetime_expired():
|
func _on_lifetime_expired():
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ func _spawn_adjacent_after_delay() -> void:
|
|||||||
await get_tree().create_timer(0.5).timeout
|
await get_tree().create_timer(0.5).timeout
|
||||||
if not is_instance_valid(self):
|
if not is_instance_valid(self):
|
||||||
return
|
return
|
||||||
|
# Round 2: play spike SFX when the 4 adjacent spikes appear
|
||||||
|
if has_node("SfxSpike"):
|
||||||
|
$SfxSpike.play()
|
||||||
var gw = get_tree().get_first_node_in_group("game_world")
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
if not gw or not gw.has_method("_get_adjacent_valid_spell_tile_centers"):
|
if not gw or not gw.has_method("_get_adjacent_valid_spell_tile_centers"):
|
||||||
_finish_center_spike()
|
_finish_center_spike()
|
||||||
@@ -62,12 +65,11 @@ func _spawn_adjacent_after_delay() -> void:
|
|||||||
await get_tree().create_timer(0.25).timeout
|
await get_tree().create_timer(0.25).timeout
|
||||||
if not is_instance_valid(self):
|
if not is_instance_valid(self):
|
||||||
return
|
return
|
||||||
# Third wave: center again, 2x scale, 2x damage (most damage) — only after 4 finish
|
# Third wave: center again, 2x scale, 2x damage (most damage). Use skip_sfx=false so the
|
||||||
|
# big spike plays SfxSpike in its _ready() — we can't play on self here because we're about to queue_free().
|
||||||
var third = scene.instantiate()
|
var third = scene.instantiate()
|
||||||
third.setup(global_position, player_owner, damage, false, 2.0, 2.0, true)
|
third.setup(global_position, player_owner, damage, false, 2.0, 2.0, false)
|
||||||
par.add_child(third)
|
par.add_child(third)
|
||||||
if has_node("SfxSpike"):
|
|
||||||
$SfxSpike.play()
|
|
||||||
_finish_center_spike()
|
_finish_center_spike()
|
||||||
|
|
||||||
func _finish_center_spike() -> void:
|
func _finish_center_spike() -> void:
|
||||||
|
|||||||
@@ -287,6 +287,10 @@ func modify_health(amount: float, allow_overheal: bool = false) -> void:
|
|||||||
hp = max(0.0, hp)
|
hp = max(0.0, hp)
|
||||||
else:
|
else:
|
||||||
hp = clamp(hp, 0.0, maxhp)
|
hp = clamp(hp, 0.0, maxhp)
|
||||||
|
# Fix floating point precision: if HP is very close to 0 (within epsilon), set it to exactly 0.0
|
||||||
|
# This ensures death checks work correctly when HP reaches exactly 0
|
||||||
|
if hp <= 0.001:
|
||||||
|
hp = 0.0
|
||||||
health_changed.emit(hp, maxhp)
|
health_changed.emit(hp, maxhp)
|
||||||
character_changed.emit(self)
|
character_changed.emit(self)
|
||||||
|
|
||||||
@@ -321,7 +325,9 @@ func take_damage(amount: float, is_magical: bool = false) -> float:
|
|||||||
# Calculate damage after DEF reduction
|
# Calculate damage after DEF reduction
|
||||||
var actual_damage = calculate_damage(amount, is_magical)
|
var actual_damage = calculate_damage(amount, is_magical)
|
||||||
modify_health(-actual_damage)
|
modify_health(-actual_damage)
|
||||||
if hp <= 0:
|
# Check if dead (use epsilon to handle floating point precision)
|
||||||
|
if hp <= 0.001:
|
||||||
|
hp = 0.0 # Ensure exactly 0
|
||||||
no_health.emit() # Emit when health reaches 0
|
no_health.emit() # Emit when health reaches 0
|
||||||
character_changed.emit(self)
|
character_changed.emit(self)
|
||||||
return actual_damage
|
return actual_damage
|
||||||
|
|||||||
27
src/scripts/damage_effect_arrow.gd
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Arrow hit effect when arrow damages enemy - frames 335-339 from shade_spell_effects.png
|
||||||
|
|
||||||
|
const DURATION: float = 0.2
|
||||||
|
const FRAMES: Array = [335, 336, 337, 338, 339]
|
||||||
|
const FRAME_DURATION: float = 0.04
|
||||||
|
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
@onready var fx_sprite: Sprite2D = $FxSprite
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if fx_sprite and FRAMES.size() > 0:
|
||||||
|
fx_sprite.frame = FRAMES[0]
|
||||||
|
|
||||||
|
func setup(_position: Vector2 = Vector2.ZERO) -> void:
|
||||||
|
if _position != Vector2.ZERO:
|
||||||
|
global_position = _position
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
elapsed += delta
|
||||||
|
if fx_sprite and FRAMES.size() > 0:
|
||||||
|
var idx = min(int(elapsed / FRAME_DURATION), FRAMES.size() - 1)
|
||||||
|
fx_sprite.frame = FRAMES[idx]
|
||||||
|
if elapsed >= DURATION:
|
||||||
|
queue_free()
|
||||||
1
src/scripts/damage_effect_arrow.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://j1ypyujarmn6
|
||||||
27
src/scripts/damage_effect_axe.gd
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Axe hit effect when axe swing damages target - frames 1158-1162 from shade_spell_effects.png
|
||||||
|
|
||||||
|
const DURATION: float = 0.2
|
||||||
|
const FRAMES: Array = [1158, 1159, 1160, 1161, 1162]
|
||||||
|
const FRAME_DURATION: float = 0.04
|
||||||
|
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
@onready var fx_sprite: Sprite2D = $FxSprite
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if fx_sprite and FRAMES.size() > 0:
|
||||||
|
fx_sprite.frame = FRAMES[0]
|
||||||
|
|
||||||
|
func setup(_position: Vector2 = Vector2.ZERO) -> void:
|
||||||
|
if _position != Vector2.ZERO:
|
||||||
|
global_position = _position
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
elapsed += delta
|
||||||
|
if fx_sprite and FRAMES.size() > 0:
|
||||||
|
var idx = min(int(elapsed / FRAME_DURATION), FRAMES.size() - 1)
|
||||||
|
fx_sprite.frame = FRAMES[idx]
|
||||||
|
if elapsed >= DURATION:
|
||||||
|
queue_free()
|
||||||
1
src/scripts/damage_effect_axe.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://brg44rb3vy1g2
|
||||||
27
src/scripts/damage_effect_bite.gd
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Bite hit effect when bat/rat damages player - frames 148-158 from shade_spell_effects.png
|
||||||
|
|
||||||
|
const DURATION: float = 0.44
|
||||||
|
const FRAMES: Array = [148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158]
|
||||||
|
const FRAME_DURATION: float = 0.04
|
||||||
|
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
@onready var fx_sprite: Sprite2D = $FxSprite
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if fx_sprite and FRAMES.size() > 0:
|
||||||
|
fx_sprite.frame = FRAMES[0]
|
||||||
|
|
||||||
|
func setup(_position: Vector2 = Vector2.ZERO) -> void:
|
||||||
|
if _position != Vector2.ZERO:
|
||||||
|
global_position = _position
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
elapsed += delta
|
||||||
|
if fx_sprite and FRAMES.size() > 0:
|
||||||
|
var idx = min(int(elapsed / FRAME_DURATION), FRAMES.size() - 1)
|
||||||
|
fx_sprite.frame = FRAMES[idx]
|
||||||
|
if elapsed >= DURATION:
|
||||||
|
queue_free()
|
||||||
1
src/scripts/damage_effect_bite.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b3l607m13h12y
|
||||||
27
src/scripts/damage_effect_punch.gd
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Temporary hit effect for punch - plays frames 451-458 from shade_spell_effects.png
|
||||||
|
|
||||||
|
const DURATION: float = 0.32
|
||||||
|
const FRAMES: Array = [451, 452, 453, 454, 455, 456, 457, 458]
|
||||||
|
const FRAME_DURATION: float = 0.04
|
||||||
|
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
@onready var fx_sprite: Sprite2D = $FxSprite
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if fx_sprite and FRAMES.size() > 0:
|
||||||
|
fx_sprite.frame = FRAMES[0]
|
||||||
|
|
||||||
|
func setup(_position: Vector2 = Vector2.ZERO) -> void:
|
||||||
|
if _position != Vector2.ZERO:
|
||||||
|
global_position = _position
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
elapsed += delta
|
||||||
|
if fx_sprite and FRAMES.size() > 0:
|
||||||
|
var idx = min(int(elapsed / FRAME_DURATION), FRAMES.size() - 1)
|
||||||
|
fx_sprite.frame = FRAMES[idx]
|
||||||
|
if elapsed >= DURATION:
|
||||||
|
queue_free()
|
||||||
1
src/scripts/damage_effect_punch.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bqq0nj858gglm
|
||||||
27
src/scripts/damage_effect_slash.gd
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Slash hit effect when sword projectile damages enemy - frames 948-952 from shade_spell_effects.png
|
||||||
|
|
||||||
|
const DURATION: float = 0.2
|
||||||
|
const FRAMES: Array = [948, 949, 950, 951, 952]
|
||||||
|
const FRAME_DURATION: float = 0.04
|
||||||
|
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
@onready var fx_sprite: Sprite2D = $FxSprite
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if fx_sprite and FRAMES.size() > 0:
|
||||||
|
fx_sprite.frame = FRAMES[0]
|
||||||
|
|
||||||
|
func setup(_position: Vector2 = Vector2.ZERO) -> void:
|
||||||
|
if _position != Vector2.ZERO:
|
||||||
|
global_position = _position
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
elapsed += delta
|
||||||
|
if fx_sprite and FRAMES.size() > 0:
|
||||||
|
var idx = min(int(elapsed / FRAME_DURATION), FRAMES.size() - 1)
|
||||||
|
fx_sprite.frame = FRAMES[idx]
|
||||||
|
if elapsed >= DURATION:
|
||||||
|
queue_free()
|
||||||
1
src/scripts/damage_effect_slash.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bs77pdmfdbnwb
|
||||||
@@ -164,7 +164,10 @@ func _physics_process(delta):
|
|||||||
var actual_damage = old_hp - character_stats.hp
|
var actual_damage = old_hp - character_stats.hp
|
||||||
LogManager.log(str(name) + " takes " + str(actual_damage) + " burn damage! Health: " + str(current_health) + "/" + str(character_stats.maxhp), LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " takes " + str(actual_damage) + " burn damage! Health: " + str(current_health) + "/" + str(character_stats.maxhp), LogManager.CATEGORY_ENEMY)
|
||||||
# Show damage number for burn damage
|
# Show damage number for burn damage
|
||||||
_show_damage_number(actual_damage, global_position, false, false, false) # Show burn damage number
|
# Use a position slightly offset from global_position to ensure proper direction calculation
|
||||||
|
# (if from_position equals global_position, direction becomes Vector2.ZERO which causes issues)
|
||||||
|
var burn_damage_source_pos = global_position + Vector2(randf_range(-10, 10), -20) # Slight random offset above enemy
|
||||||
|
_show_damage_number(actual_damage, burn_damage_source_pos, false, false, false) # Show burn damage number
|
||||||
# Sync burn damage visual to clients
|
# Sync burn damage visual to clients
|
||||||
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
||||||
var enemy_name = name
|
var enemy_name = name
|
||||||
@@ -303,6 +306,14 @@ func _attack_player(player):
|
|||||||
player.rpc_take_damage.rpc(damage, global_position)
|
player.rpc_take_damage.rpc(damage, global_position)
|
||||||
attack_timer = attack_cooldown
|
attack_timer = attack_cooldown
|
||||||
LogManager.log(str(name) + " attacked " + str(player.name) + " (peer: " + str(player_peer_id) + ", server: " + str(multiplayer.get_unique_id()) + ")", LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " attacked " + str(player.name) + " (peer: " + str(player_peer_id) + ", server: " + str(multiplayer.get_unique_id()) + ")", LogManager.CATEGORY_ENEMY)
|
||||||
|
# Bite effect when bat or rat hits player
|
||||||
|
var script_path = get_script().resource_path if get_script() else ""
|
||||||
|
if "enemy_bat" in script_path or "enemy_rat" in script_path:
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_sync_bite_effect"):
|
||||||
|
game_world._sync_bite_effect(player.name)
|
||||||
|
if game_world.has_method("_rpc_to_ready_peers"):
|
||||||
|
game_world._rpc_to_ready_peers("_sync_bite_effect", [player.name])
|
||||||
|
|
||||||
func _find_nearest_player() -> Node:
|
func _find_nearest_player() -> Node:
|
||||||
var players = get_tree().get_nodes_in_group("player")
|
var players = get_tree().get_nodes_in_group("player")
|
||||||
@@ -471,7 +482,6 @@ func rpc_heal_enemy(amount: float, allow_overheal: bool = false):
|
|||||||
func _show_damage_number(amount: float, from_position: Vector2, is_critical: bool = false, is_miss: bool = false, is_dodged: bool = false):
|
func _show_damage_number(amount: float, from_position: Vector2, is_critical: bool = false, is_miss: bool = false, is_dodged: bool = false):
|
||||||
# Show damage number (red/orange for crits, cyan for dodge, gray for miss, using dmg_numbers.png font) above enemy
|
# Show damage number (red/orange for crits, cyan for dodge, gray for miss, using dmg_numbers.png font) above enemy
|
||||||
# Show even if amount is 0 for MISS/DODGED
|
# Show even if amount is 0 for MISS/DODGED
|
||||||
|
|
||||||
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
||||||
if not damage_number_scene:
|
if not damage_number_scene:
|
||||||
return
|
return
|
||||||
@@ -512,6 +522,38 @@ func _show_damage_number(amount: float, from_position: Vector2, is_critical: boo
|
|||||||
get_tree().current_scene.add_child(damage_label)
|
get_tree().current_scene.add_child(damage_label)
|
||||||
damage_label.global_position = global_position + Vector2(0, -16)
|
damage_label.global_position = global_position + Vector2(0, -16)
|
||||||
|
|
||||||
|
func _show_exp_number(amount: float, exp_pos: Vector2):
|
||||||
|
# Show EXP number (green/yellow, using dmg_numbers.png font) at position
|
||||||
|
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
||||||
|
if not damage_number_scene:
|
||||||
|
return
|
||||||
|
|
||||||
|
var exp_label = damage_number_scene.instantiate()
|
||||||
|
if not exp_label:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set text and color for EXP (green/yellow)
|
||||||
|
exp_label.label = "+" + str(int(amount)) + " EXP"
|
||||||
|
exp_label.color = Color(0.4, 1.0, 0.4) # Bright green
|
||||||
|
exp_label.z_index = 5
|
||||||
|
|
||||||
|
# Direction is straight up
|
||||||
|
exp_label.direction = Vector2(0, -1)
|
||||||
|
|
||||||
|
# Position at the specified location
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world:
|
||||||
|
var entities_node = game_world.get_node_or_null("Entities")
|
||||||
|
if entities_node:
|
||||||
|
entities_node.add_child(exp_label)
|
||||||
|
exp_label.global_position = exp_pos + Vector2(0, -20) # Slightly above
|
||||||
|
else:
|
||||||
|
get_tree().current_scene.add_child(exp_label)
|
||||||
|
exp_label.global_position = exp_pos + Vector2(0, -20)
|
||||||
|
else:
|
||||||
|
get_tree().current_scene.add_child(exp_label)
|
||||||
|
exp_label.global_position = exp_pos + Vector2(0, -20)
|
||||||
|
|
||||||
func _flash_damage():
|
func _flash_damage():
|
||||||
# Flash red visual effect
|
# Flash red visual effect
|
||||||
if sprite:
|
if sprite:
|
||||||
@@ -661,16 +703,11 @@ func _die():
|
|||||||
game_world.defeated_enemies[enemy_index] = true
|
game_world.defeated_enemies[enemy_index] = true
|
||||||
LogManager.log("Enemy: Tracked defeated enemy with index " + str(enemy_index), LogManager.CATEGORY_ENEMY)
|
LogManager.log("Enemy: Tracked defeated enemy with index " + str(enemy_index), LogManager.CATEGORY_ENEMY)
|
||||||
|
|
||||||
# Credit kill and grant EXP to the player who dealt the fatal damage
|
# Credit kill to the player who dealt the fatal damage
|
||||||
if killer_player and is_instance_valid(killer_player) and killer_player.character_stats:
|
if killer_player and is_instance_valid(killer_player) and killer_player.character_stats:
|
||||||
killer_player.character_stats.kills += 1
|
killer_player.character_stats.kills += 1
|
||||||
LogManager.log(str(name) + " kill credited to " + str(killer_player.name) + " (total kills: " + str(killer_player.character_stats.kills) + ")", LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " kill credited to " + str(killer_player.name) + " (total kills: " + str(killer_player.character_stats.kills) + ")", LogManager.CATEGORY_ENEMY)
|
||||||
|
|
||||||
# Grant EXP to the killer
|
|
||||||
if exp_reward > 0:
|
|
||||||
killer_player.character_stats.add_xp(exp_reward)
|
|
||||||
LogManager.log(str(name) + " granted " + str(exp_reward) + " EXP to " + str(killer_player.name), LogManager.CATEGORY_ENEMY)
|
|
||||||
|
|
||||||
# Sync kill update to client if this player belongs to a client
|
# Sync kill update to client if this player belongs to a client
|
||||||
# Only sync if we're on the server and the killer is a client's player
|
# Only sync if we're on the server and the killer is a client's player
|
||||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
|
||||||
@@ -679,8 +716,41 @@ func _die():
|
|||||||
if killer_peer_id != 0 and killer_peer_id != multiplayer.get_unique_id() and killer_player.has_method("_sync_stats_update"):
|
if killer_peer_id != 0 and killer_peer_id != multiplayer.get_unique_id() and killer_player.has_method("_sync_stats_update"):
|
||||||
# Server is updating a client's player stats - sync to the client
|
# Server is updating a client's player stats - sync to the client
|
||||||
var coins = killer_player.character_stats.coin if "coin" in killer_player.character_stats else 0
|
var coins = killer_player.character_stats.coin if "coin" in killer_player.character_stats else 0
|
||||||
LogManager.log(str(name) + " syncing kill stats to client peer_id=" + str(killer_peer_id) + " kills=" + str(killer_player.character_stats.kills) + " coins=" + str(coins), LogManager.CATEGORY_ENEMY)
|
var xp = killer_player.character_stats.xp if "xp" in killer_player.character_stats else 0.0
|
||||||
killer_player._sync_stats_update.rpc_id(killer_peer_id, killer_player.character_stats.kills, coins)
|
LogManager.log(str(name) + " syncing kill stats to client peer_id=" + str(killer_peer_id) + " kills=" + str(killer_player.character_stats.kills) + " coins=" + str(coins) + " xp=" + str(xp), LogManager.CATEGORY_ENEMY)
|
||||||
|
killer_player._sync_stats_update.rpc_id(killer_peer_id, killer_player.character_stats.kills, coins, xp)
|
||||||
|
|
||||||
|
# Grant EXP to all players (split evenly among all players)
|
||||||
|
if exp_reward > 0:
|
||||||
|
var all_players = get_tree().get_nodes_in_group("player")
|
||||||
|
var valid_players = []
|
||||||
|
for player in all_players:
|
||||||
|
if is_instance_valid(player) and player.character_stats:
|
||||||
|
valid_players.append(player)
|
||||||
|
|
||||||
|
if valid_players.size() > 0:
|
||||||
|
# Split EXP evenly among all players
|
||||||
|
var exp_per_player = exp_reward / valid_players.size()
|
||||||
|
for player in valid_players:
|
||||||
|
player.character_stats.add_xp(exp_per_player)
|
||||||
|
LogManager.log(str(name) + " granted " + str(exp_per_player) + " EXP to " + str(player.name) + " (shared from " + str(exp_reward) + " total)", LogManager.CATEGORY_ENEMY)
|
||||||
|
|
||||||
|
# Sync EXP to client if this player belongs to a client
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
|
||||||
|
var player_peer_id = player.get_multiplayer_authority()
|
||||||
|
if player_peer_id != 0 and player_peer_id != multiplayer.get_unique_id() and player.has_method("_sync_stats_update"):
|
||||||
|
var coins = player.character_stats.coin if "coin" in player.character_stats else 0
|
||||||
|
var xp = player.character_stats.xp if "xp" in player.character_stats else 0.0
|
||||||
|
player._sync_stats_update.rpc_id(player_peer_id, player.character_stats.kills, coins, xp)
|
||||||
|
|
||||||
|
# Show floating EXP text at enemy position and sync to all clients
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
# Show locally first
|
||||||
|
_show_exp_number(exp_per_player, global_position)
|
||||||
|
# Sync to all clients via game_world
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_sync_exp_text_at_position") and multiplayer.has_multiplayer_peer():
|
||||||
|
game_world._sync_exp_text_at_position.rpc(exp_per_player, global_position)
|
||||||
|
|
||||||
# Spawn loot immediately (before death animation)
|
# Spawn loot immediately (before death animation)
|
||||||
_spawn_loot()
|
_spawn_loot()
|
||||||
@@ -771,10 +841,11 @@ func _spawn_loot():
|
|||||||
var lck_bonus_item = min(killer_lck * 0.01, 0.2) # Up to +20% item chance at LCK 20+
|
var lck_bonus_item = min(killer_lck * 0.01, 0.2) # Up to +20% item chance at LCK 20+
|
||||||
var lck_penalty_coin = min(killer_lck * 0.005, 0.15) # Up to -15% coin chance at LCK 30+
|
var lck_penalty_coin = min(killer_lck * 0.005, 0.15) # Up to -15% coin chance at LCK 30+
|
||||||
|
|
||||||
# Base probabilities: 50% coin, 20% food, 30% item
|
# Base probabilities: 70% coin, 15% food, 15% item (reduced from 30%)
|
||||||
var coin_chance = 0.5 - lck_penalty_coin
|
# Items are further split: 80% consumables, 20% equipment (making equipment very rare)
|
||||||
var food_chance = 0.2
|
var coin_chance = 0.7 - lck_penalty_coin
|
||||||
var item_chance = 0.3 + lck_bonus_item
|
var food_chance = 0.15
|
||||||
|
var item_chance = 0.15 + lck_bonus_item # Reduced from 0.3
|
||||||
|
|
||||||
# Normalize probabilities
|
# Normalize probabilities
|
||||||
var total = coin_chance + food_chance + item_chance
|
var total = coin_chance + food_chance + item_chance
|
||||||
@@ -844,42 +915,22 @@ func _spawn_loot():
|
|||||||
safe_spawn_pos = game_world._find_nearby_safe_spawn_position(safe_spawn_pos, 32.0)
|
safe_spawn_pos = game_world._find_nearby_safe_spawn_position(safe_spawn_pos, 32.0)
|
||||||
|
|
||||||
if drop_item:
|
if drop_item:
|
||||||
# Spawn Item instance as loot - LCK influences rarity
|
# Spawn Item instance as loot - prioritize consumables over equipment
|
||||||
|
# 80% consumables (arrows, bombs, restoration), 20% equipment
|
||||||
|
var item_type_roll = randf()
|
||||||
var item = null
|
var item = null
|
||||||
if item_rarity_boost:
|
|
||||||
# High LCK: use chest rarity weights (better loot) instead of enemy drop weights
|
|
||||||
# Roll for rarity with LCK bonus: each 5 LCK above 15 increases rare/epic chance
|
|
||||||
var rarity_roll = randf()
|
|
||||||
var lck_rarity_bonus = min((killer_lck - 15.0) * 0.02, 0.15) # Up to +15% rare/epic chance
|
|
||||||
|
|
||||||
# Clamp values to prevent going below 0 or above 1
|
if item_type_roll < 0.8:
|
||||||
var common_threshold = max(0.0, 0.3 - lck_rarity_bonus)
|
# Consumable drop (arrows, bombs, restoration items)
|
||||||
var uncommon_threshold = max(common_threshold, 0.65 - (lck_rarity_bonus * 0.5))
|
item = ItemDatabase.get_random_consumable_drop()
|
||||||
var rare_threshold = min(1.0, 0.90 + (lck_rarity_bonus * 2.0))
|
|
||||||
|
|
||||||
if rarity_roll < common_threshold:
|
|
||||||
# Common (reduced by LCK)
|
|
||||||
item = ItemDatabase.get_random_item_by_rarity(ItemDatabase.ItemRarity.COMMON)
|
|
||||||
elif rarity_roll < uncommon_threshold:
|
|
||||||
# Uncommon (slightly reduced)
|
|
||||||
item = ItemDatabase.get_random_item_by_rarity(ItemDatabase.ItemRarity.UNCOMMON)
|
|
||||||
elif rarity_roll < rare_threshold:
|
|
||||||
# Rare (increased by LCK)
|
|
||||||
item = ItemDatabase.get_random_item_by_rarity(ItemDatabase.ItemRarity.RARE)
|
|
||||||
else:
|
else:
|
||||||
# Epic/Consumable (greatly increased by LCK)
|
# Equipment drop (much rarer - only 20% of item drops, which is 20% of 15% = 3% total)
|
||||||
var epic_roll = randf()
|
# LCK boost still applies - higher LCK makes equipment drops more likely to be better quality
|
||||||
if epic_roll < 0.5:
|
item = ItemDatabase.get_random_equipment_drop()
|
||||||
item = ItemDatabase.get_random_item_by_rarity(ItemDatabase.ItemRarity.EPIC)
|
|
||||||
else:
|
|
||||||
item = ItemDatabase.get_random_item_by_rarity(ItemDatabase.ItemRarity.CONSUMABLE)
|
|
||||||
else:
|
|
||||||
# Normal LCK: use standard enemy drop weights
|
|
||||||
item = ItemDatabase.get_random_enemy_drop()
|
|
||||||
|
|
||||||
if item:
|
if item:
|
||||||
ItemLootHelper.spawn_item_loot(item, safe_spawn_pos, entities_node, game_world)
|
ItemLootHelper.spawn_item_loot(item, safe_spawn_pos, entities_node, game_world)
|
||||||
LogManager.log(str(name) + " ✓ dropped item #" + str(i+1) + ": " + str(item.item_name) + " at " + str(safe_spawn_pos) + " (LCK boost: " + str(item_rarity_boost) + ")", LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " ✓ dropped item #" + str(i + 1) + ": " + str(item.item_name) + " at " + str(safe_spawn_pos) + " (LCK boost: " + str(item_rarity_boost) + ")", LogManager.CATEGORY_ENEMY)
|
||||||
else:
|
else:
|
||||||
# Spawn regular loot (coin or food)
|
# Spawn regular loot (coin or food)
|
||||||
var loot = loot_scene.instantiate()
|
var loot = loot_scene.instantiate()
|
||||||
@@ -891,7 +942,7 @@ func _spawn_loot():
|
|||||||
loot.velocity_z = random_velocity_z
|
loot.velocity_z = random_velocity_z
|
||||||
loot.velocity_set_by_spawner = true
|
loot.velocity_set_by_spawner = true
|
||||||
loot.is_airborne = true
|
loot.is_airborne = true
|
||||||
LogManager.log(str(name) + " ✓ dropped loot #" + str(i+1) + ": " + str(loot_type) + " at " + str(safe_spawn_pos), LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " ✓ dropped loot #" + str(i + 1) + ": " + str(loot_type) + " at " + str(safe_spawn_pos), LogManager.CATEGORY_ENEMY)
|
||||||
|
|
||||||
# Sync loot spawn to all clients (use safe position)
|
# Sync loot spawn to all clients (use safe position)
|
||||||
if multiplayer.has_multiplayer_peer():
|
if multiplayer.has_multiplayer_peer():
|
||||||
@@ -922,7 +973,7 @@ func _spawn_loot():
|
|||||||
loot.set_meta("loot_id", loot_id)
|
loot.set_meta("loot_id", loot_id)
|
||||||
# Sync to clients with ID
|
# Sync to clients with ID
|
||||||
game_world._rpc_to_ready_peers("_sync_loot_spawn", [safe_spawn_pos, loot_type, initial_velocity, random_velocity_z, loot_id])
|
game_world._rpc_to_ready_peers("_sync_loot_spawn", [safe_spawn_pos, loot_type, initial_velocity, random_velocity_z, loot_id])
|
||||||
LogManager.log(str(name) + " ✓ synced loot #" + str(i+1) + " spawn to clients", LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " ✓ synced loot #" + str(i + 1) + " spawn to clients", LogManager.CATEGORY_ENEMY)
|
||||||
else:
|
else:
|
||||||
LogManager.log_error(str(name) + " ERROR: game_world not found for loot sync!", LogManager.CATEGORY_ENEMY)
|
LogManager.log_error(str(name) + " ERROR: game_world not found for loot sync!", LogManager.CATEGORY_ENEMY)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ extends "res://scripts/enemy_base.gd"
|
|||||||
# Moves toward player if in PlayerInterestArea, else random. Collides with walls.
|
# Moves toward player if in PlayerInterestArea, else random. Collides with walls.
|
||||||
# If player enters GrabPlayerArea and alive: grab, lock player, snatch anim, deal damage, release + knockback.
|
# If player enters GrabPlayerArea and alive: grab, lock player, snatch anim, deal damage, release + knockback.
|
||||||
|
|
||||||
enum HandState { HIDDEN, EMERGING, IDLE, GRABBING }
|
enum HandState {HIDDEN, EMERGING, IDLE, GRABBING}
|
||||||
|
|
||||||
var state: HandState = HandState.HIDDEN
|
var state: HandState = HandState.HIDDEN
|
||||||
var players_in_interest: Array[Node] = []
|
var players_in_interest: Array[Node] = []
|
||||||
@@ -31,7 +31,7 @@ func _ready() -> void:
|
|||||||
super._ready()
|
super._ready()
|
||||||
max_health = 25.0
|
max_health = 25.0
|
||||||
current_health = max_health
|
current_health = max_health
|
||||||
move_speed = 28.0 # Reduced from 55.0 - much slower
|
move_speed = 16.8 # 60% of 28.0 - slower chase/random movement
|
||||||
damage = SNATCH_DAMAGE
|
damage = SNATCH_DAMAGE
|
||||||
exp_reward = 8.0
|
exp_reward = 8.0
|
||||||
collision_layer = 2
|
collision_layer = 2
|
||||||
|
|||||||
@@ -601,7 +601,6 @@ func _get_body_texture_for_type(type: HumanoidType) -> String:
|
|||||||
func _load_random_equipment():
|
func _load_random_equipment():
|
||||||
# Load random equipment (shoes, clothes, gloves, headgear)
|
# Load random equipment (shoes, clothes, gloves, headgear)
|
||||||
# Equipment is optional - chance to have each piece
|
# Equipment is optional - chance to have each piece
|
||||||
|
|
||||||
# Random shoes (Layer 1 - Shoes)
|
# Random shoes (Layer 1 - Shoes)
|
||||||
if appearance_rng.randf() < 0.8: # 80% chance to have shoes
|
if appearance_rng.randf() < 0.8: # 80% chance to have shoes
|
||||||
_load_random_shoes()
|
_load_random_shoes()
|
||||||
@@ -1339,7 +1338,7 @@ func _chasing_behavior(delta_arg):
|
|||||||
# Move slightly away if too close, or maintain distance
|
# Move slightly away if too close, or maintain distance
|
||||||
if dist < desired_distance * 0.8:
|
if dist < desired_distance * 0.8:
|
||||||
# Too close - back away slightly
|
# Too close - back away slightly
|
||||||
velocity = -to_player * move_speed * 0.3 * speed_mult
|
velocity = - to_player * move_speed * 0.3 * speed_mult
|
||||||
else:
|
else:
|
||||||
# Good distance - just face player (or move slowly if blocking)
|
# Good distance - just face player (or move slowly if blocking)
|
||||||
if is_blocking:
|
if is_blocking:
|
||||||
@@ -1632,6 +1631,12 @@ func _throw_held_bomb():
|
|||||||
par.add_child(bomb)
|
par.add_child(bomb)
|
||||||
bomb.global_position = global_position + Vector2(0, -20) # From above head
|
bomb.global_position = global_position + Vector2(0, -20) # From above head
|
||||||
bomb.setup(bomb.global_position, self, fallback_throw_force, true)
|
bomb.setup(bomb.global_position, self, fallback_throw_force, true)
|
||||||
|
# Sync fallback bomb to joiners
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_rpc_to_ready_peers") and multiplayer.has_multiplayer_peer():
|
||||||
|
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
|
||||||
|
var bomb_name = "EnemyBomb_" + name + "_" + str(Time.get_ticks_msec())
|
||||||
|
game_world._rpc_to_ready_peers("_sync_enemy_throw_bomb", [bomb_name, name, enemy_index, bomb.global_position, fallback_throw_force])
|
||||||
return
|
return
|
||||||
|
|
||||||
if bombs_left <= 0:
|
if bombs_left <= 0:
|
||||||
|
|||||||
9
src/scripts/game_state.gd
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
extends Node
|
||||||
|
# Persists selected race across scene change (main menu -> select class -> game world).
|
||||||
|
# Player reads this in _setup_player_appearance() and randomizes character for that race.
|
||||||
|
# Set skip_race_select = true (e.g. via --race=elf) to go straight to game without race select UI.
|
||||||
|
# race_chosen_before_connect: joiner picked race on main menu before Join; skip race select after connect.
|
||||||
|
|
||||||
|
var selected_race: String = "Dwarf"
|
||||||
|
var skip_race_select: bool = false
|
||||||
|
var race_chosen_before_connect: bool = false
|
||||||
1
src/scripts/game_state.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://6dgu8mbartys
|
||||||
@@ -5,6 +5,7 @@ extends CanvasLayer
|
|||||||
@onready var main_menu = $Control/MainMenu
|
@onready var main_menu = $Control/MainMenu
|
||||||
@onready var host_button = $Control/MainMenu/VBoxContainer/HostButton
|
@onready var host_button = $Control/MainMenu/VBoxContainer/HostButton
|
||||||
@onready var join_button = $Control/MainMenu/VBoxContainer/JoinButton
|
@onready var join_button = $Control/MainMenu/VBoxContainer/JoinButton
|
||||||
|
@onready var select_race_button = $Control/MainMenu/VBoxContainer/SelectRaceButton
|
||||||
@onready var network_mode_option = $Control/MainMenu/VBoxContainer/NetworkModeContainer/NetworkModeOption
|
@onready var network_mode_option = $Control/MainMenu/VBoxContainer/NetworkModeContainer/NetworkModeOption
|
||||||
@onready var network_mode_container = $Control/MainMenu/VBoxContainer/NetworkModeContainer
|
@onready var network_mode_container = $Control/MainMenu/VBoxContainer/NetworkModeContainer
|
||||||
@onready var local_players_spinbox = $Control/MainMenu/VBoxContainer/LocalPlayersContainer/SpinBox
|
@onready var local_players_spinbox = $Control/MainMenu/VBoxContainer/LocalPlayersContainer/SpinBox
|
||||||
@@ -16,6 +17,11 @@ extends CanvasLayer
|
|||||||
|
|
||||||
@onready var network_manager = $"/root/NetworkManager"
|
@onready var network_manager = $"/root/NetworkManager"
|
||||||
|
|
||||||
|
var select_class_scene: PackedScene = preload("res://scenes/select_class.tscn")
|
||||||
|
var select_class_instance: Node = null # Race select overlay (before dungeon)
|
||||||
|
var _race_select_standalone: bool = false # True when joiner picks race before Join (don't start game on confirm)
|
||||||
|
var pending_auto_join_after_race_select: bool = false # --join --webrtc with no --race=: show race select first, then auto-join
|
||||||
|
|
||||||
var connection_error_label: Label = null
|
var connection_error_label: Label = null
|
||||||
var connection_error_shown: bool = false # Prevent spamming error messages
|
var connection_error_shown: bool = false # Prevent spamming error messages
|
||||||
var is_joining_attempt: bool = false
|
var is_joining_attempt: bool = false
|
||||||
@@ -49,6 +55,8 @@ func _ready():
|
|||||||
# Connect buttons
|
# Connect buttons
|
||||||
host_button.pressed.connect(_on_host_pressed)
|
host_button.pressed.connect(_on_host_pressed)
|
||||||
join_button.pressed.connect(_on_join_pressed)
|
join_button.pressed.connect(_on_join_pressed)
|
||||||
|
if select_race_button:
|
||||||
|
select_race_button.pressed.connect(_on_select_race_pressed)
|
||||||
|
|
||||||
# Setup network mode dropdown
|
# Setup network mode dropdown
|
||||||
if network_mode_option:
|
if network_mode_option:
|
||||||
@@ -119,6 +127,24 @@ func _check_command_line_args():
|
|||||||
join_address = arg.split("=")[1]
|
join_address = arg.split("=")[1]
|
||||||
elif arg.begins_with("--players="):
|
elif arg.begins_with("--players="):
|
||||||
local_count = int(arg.split("=")[1])
|
local_count = int(arg.split("=")[1])
|
||||||
|
elif arg.begins_with("--race="):
|
||||||
|
var race_arg = arg.split("=")[1].strip_edges().to_lower()
|
||||||
|
var gs = get_node_or_null("/root/GameState")
|
||||||
|
if gs:
|
||||||
|
if race_arg == "dwarf":
|
||||||
|
gs.selected_race = "Dwarf"
|
||||||
|
gs.skip_race_select = true
|
||||||
|
LogManager.log("GameUI: Race set from argument: Dwarf (skip race select)", LogManager.CATEGORY_UI)
|
||||||
|
elif race_arg == "elf":
|
||||||
|
gs.selected_race = "Elf"
|
||||||
|
gs.skip_race_select = true
|
||||||
|
LogManager.log("GameUI: Race set from argument: Elf (skip race select)", LogManager.CATEGORY_UI)
|
||||||
|
elif race_arg == "human":
|
||||||
|
gs.selected_race = "Human"
|
||||||
|
gs.skip_race_select = true
|
||||||
|
LogManager.log("GameUI: Race set from argument: Human (skip race select)", LogManager.CATEGORY_UI)
|
||||||
|
else:
|
||||||
|
LogManager.log("GameUI: Ignoring invalid --race=" + race_arg + " (use dwarf, elf, or human)", LogManager.CATEGORY_UI)
|
||||||
|
|
||||||
LogManager.log("GameUI: Parsed flags - should_host: " + str(should_host) + ", should_join: " + str(should_join) + ", force_webrtc: " + str(force_webrtc) + ", should_debug: " + str(should_debug), LogManager.CATEGORY_UI)
|
LogManager.log("GameUI: Parsed flags - should_host: " + str(should_host) + ", should_join: " + str(should_join) + ", force_webrtc: " + str(force_webrtc) + ", should_debug: " + str(should_debug), LogManager.CATEGORY_UI)
|
||||||
|
|
||||||
@@ -146,29 +172,35 @@ func _check_command_line_args():
|
|||||||
LogManager.log("Auto-hosting due to --host argument", LogManager.CATEGORY_UI)
|
LogManager.log("Auto-hosting due to --host argument", LogManager.CATEGORY_UI)
|
||||||
network_manager.set_local_player_count(local_count)
|
network_manager.set_local_player_count(local_count)
|
||||||
if network_manager.host_game():
|
if network_manager.host_game():
|
||||||
_start_game()
|
call_deferred("_show_race_select")
|
||||||
elif should_join:
|
elif should_join:
|
||||||
# Check network mode after it's been set
|
# Check network mode after it's been set
|
||||||
var current_mode = network_manager.network_mode
|
var current_mode = network_manager.network_mode
|
||||||
if join_address.is_empty() and (current_mode == 1 or current_mode == 2):
|
if join_address.is_empty() and (current_mode == 1 or current_mode == 2):
|
||||||
# No address provided, and using WebRTC or WebSocket - fetch and auto-join first available room
|
# WebRTC/WebSocket with no address: auto-join first room. If no --race=, show race select first.
|
||||||
LogManager.log("Auto-joining: No address provided, fetching available rooms (mode: " + str(current_mode) + ")...", LogManager.CATEGORY_UI)
|
var gs = get_node_or_null("/root/GameState")
|
||||||
|
if gs and gs.skip_race_select:
|
||||||
|
# --race= was passed: skip race select and auto-join immediately
|
||||||
|
LogManager.log("Auto-joining: No address provided, fetching available rooms (mode: " + str(current_mode) + ", race: " + gs.selected_race + ")...", LogManager.CATEGORY_UI)
|
||||||
network_manager.set_local_player_count(local_count)
|
network_manager.set_local_player_count(local_count)
|
||||||
is_auto_joining = true
|
is_auto_joining = true
|
||||||
is_joining_attempt = true # Mark as joining attempt so connection failure handler works
|
is_joining_attempt = true
|
||||||
# Create timer for retrying room fetches
|
|
||||||
room_fetch_timer = Timer.new()
|
room_fetch_timer = Timer.new()
|
||||||
room_fetch_timer.name = "RoomFetchTimer"
|
room_fetch_timer.name = "RoomFetchTimer"
|
||||||
room_fetch_timer.wait_time = 2.0 # Retry every 2 seconds
|
room_fetch_timer.wait_time = 2.0
|
||||||
room_fetch_timer.timeout.connect(_retry_room_fetch)
|
room_fetch_timer.timeout.connect(_retry_room_fetch)
|
||||||
room_fetch_timer.autostart = false
|
room_fetch_timer.autostart = false
|
||||||
add_child(room_fetch_timer)
|
add_child(room_fetch_timer)
|
||||||
# Connect to rooms_fetched signal (not one-shot, so we can keep retrying)
|
|
||||||
if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join):
|
if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join):
|
||||||
network_manager.rooms_fetched.connect(_on_rooms_fetched_auto_join)
|
network_manager.rooms_fetched.connect(_on_rooms_fetched_auto_join)
|
||||||
# Show room fetch status UI and start fetching
|
|
||||||
_show_room_fetch_status()
|
_show_room_fetch_status()
|
||||||
_start_room_fetch()
|
_start_room_fetch()
|
||||||
|
else:
|
||||||
|
# No --race=: show race select first; after they confirm we start auto-join
|
||||||
|
LogManager.log("Auto-join with WebRTC/WebSocket: choose race first (no --race= passed)", LogManager.CATEGORY_UI)
|
||||||
|
pending_auto_join_after_race_select = true
|
||||||
|
_race_select_standalone = true
|
||||||
|
call_deferred("_show_race_select_ui")
|
||||||
elif not join_address.is_empty():
|
elif not join_address.is_empty():
|
||||||
LogManager.log("Auto-joining to " + join_address + " due to --join argument", LogManager.CATEGORY_UI)
|
LogManager.log("Auto-joining to " + join_address + " due to --join argument", LogManager.CATEGORY_UI)
|
||||||
address_input.text = join_address
|
address_input.text = join_address
|
||||||
@@ -291,6 +323,26 @@ func _on_rooms_fetched_auto_join(rooms: Array):
|
|||||||
_show_loading_indicator()
|
_show_loading_indicator()
|
||||||
_start_room_fetch()
|
_start_room_fetch()
|
||||||
|
|
||||||
|
func _start_pending_auto_join():
|
||||||
|
"""Start auto-join flow after user chose race (--join --webrtc with no --race=)."""
|
||||||
|
if main_menu:
|
||||||
|
main_menu.visible = true # Room fetch status lives inside MainMenu
|
||||||
|
var local_count = int(local_players_spinbox.value) if local_players_spinbox else 1
|
||||||
|
network_manager.set_local_player_count(local_count)
|
||||||
|
is_auto_joining = true
|
||||||
|
is_joining_attempt = true
|
||||||
|
if not room_fetch_timer:
|
||||||
|
room_fetch_timer = Timer.new()
|
||||||
|
room_fetch_timer.name = "RoomFetchTimer"
|
||||||
|
room_fetch_timer.wait_time = 2.0
|
||||||
|
room_fetch_timer.timeout.connect(_retry_room_fetch)
|
||||||
|
room_fetch_timer.autostart = false
|
||||||
|
add_child(room_fetch_timer)
|
||||||
|
if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join):
|
||||||
|
network_manager.rooms_fetched.connect(_on_rooms_fetched_auto_join)
|
||||||
|
_show_room_fetch_status()
|
||||||
|
_start_room_fetch()
|
||||||
|
|
||||||
func _retry_room_fetch():
|
func _retry_room_fetch():
|
||||||
"""Retry fetching available rooms"""
|
"""Retry fetching available rooms"""
|
||||||
if not is_auto_joining:
|
if not is_auto_joining:
|
||||||
@@ -632,7 +684,7 @@ func _on_host_pressed():
|
|||||||
print("Share this code with players!")
|
print("Share this code with players!")
|
||||||
else:
|
else:
|
||||||
print("Hosting ENet game with ", local_count, " local players")
|
print("Hosting ENet game with ", local_count, " local players")
|
||||||
_start_game()
|
_show_race_select()
|
||||||
|
|
||||||
func _on_join_pressed():
|
func _on_join_pressed():
|
||||||
# Reset error state when attempting new connection
|
# Reset error state when attempting new connection
|
||||||
@@ -678,7 +730,16 @@ func _on_connection_succeeded():
|
|||||||
if not is_inside_tree():
|
if not is_inside_tree():
|
||||||
LogManager.log_error("GameUI: Cannot start game - node not in tree", LogManager.CATEGORY_UI)
|
LogManager.log_error("GameUI: Cannot start game - node not in tree", LogManager.CATEGORY_UI)
|
||||||
return
|
return
|
||||||
# Use call_deferred to ensure we're in a safe state to change scenes
|
# Joiner must load game_world immediately so host's spawn RPCs can be processed. Use race from
|
||||||
|
# GameState (set in _on_race_selected when they picked; do not overwrite). No race select after connect.
|
||||||
|
var gs = get_node_or_null("/root/GameState")
|
||||||
|
if gs:
|
||||||
|
gs.race_chosen_before_connect = false
|
||||||
|
# Only default to Dwarf if nothing was ever set (e.g. --join --webrtc --race=elf skips UI)
|
||||||
|
if gs.selected_race.is_empty():
|
||||||
|
gs.selected_race = "Dwarf"
|
||||||
|
print("GameUI: Connection succeeded, starting game - GameState.selected_race = '", gs.selected_race if gs else "Dwarf", "' (joiner will use this for their player)")
|
||||||
|
LogManager.log("GameUI: Connection succeeded, starting game (race: " + (gs.selected_race if gs else "Dwarf") + ")", LogManager.CATEGORY_UI)
|
||||||
call_deferred("_start_game")
|
call_deferred("_start_game")
|
||||||
|
|
||||||
func _on_connection_failed():
|
func _on_connection_failed():
|
||||||
@@ -769,15 +830,83 @@ func _hide_connection_error():
|
|||||||
connection_error_label.queue_free()
|
connection_error_label.queue_free()
|
||||||
connection_error_label = null
|
connection_error_label = null
|
||||||
|
|
||||||
|
func _on_select_race_pressed():
|
||||||
|
"""Joiner picks race before connecting. Show race select; on confirm we just store choice and return to menu."""
|
||||||
|
_race_select_standalone = true
|
||||||
|
_show_race_select_ui()
|
||||||
|
|
||||||
|
func _show_race_select():
|
||||||
|
"""Show race select before dungeon (after Host or before Join). User picks race; on confirm we start game."""
|
||||||
|
if not is_inside_tree():
|
||||||
|
return
|
||||||
|
var gs = get_node_or_null("/root/GameState")
|
||||||
|
if gs and gs.skip_race_select:
|
||||||
|
LogManager.log("GameUI: Skipping race select (race from args: " + gs.selected_race + ")", LogManager.CATEGORY_UI)
|
||||||
|
_start_game()
|
||||||
|
return
|
||||||
|
_race_select_standalone = false
|
||||||
|
_show_race_select_ui()
|
||||||
|
|
||||||
|
func _show_race_select_ui():
|
||||||
|
"""Show race select overlay. _race_select_standalone: true = joiner picking before Join (don't start game)."""
|
||||||
|
if not is_inside_tree():
|
||||||
|
return
|
||||||
|
if select_class_instance and is_instance_valid(select_class_instance):
|
||||||
|
return
|
||||||
|
if main_menu:
|
||||||
|
main_menu.visible = false
|
||||||
|
var sel = select_class_scene.instantiate()
|
||||||
|
add_child(sel)
|
||||||
|
select_class_instance = sel
|
||||||
|
if sel.has_signal("race_selected"):
|
||||||
|
sel.race_selected.connect(_on_race_selected)
|
||||||
|
LogManager.log("GameUI: Race select shown" + (" (choose before Join)" if _race_select_standalone else ""), LogManager.CATEGORY_UI)
|
||||||
|
|
||||||
|
func _on_race_selected(race_name: String):
|
||||||
|
"""User confirmed race. Standalone = joiner chose before Join: store and return to menu. Else start game."""
|
||||||
|
if select_class_instance and is_instance_valid(select_class_instance):
|
||||||
|
select_class_instance.queue_free()
|
||||||
|
select_class_instance = null
|
||||||
|
# Always persist chosen race to GameState (select_class sets it too; this guarantees it from signal)
|
||||||
|
var gs = get_node_or_null("/root/GameState")
|
||||||
|
if gs and not race_name.is_empty():
|
||||||
|
gs.selected_race = race_name
|
||||||
|
LogManager.log("GameUI: JOINER PICKED race='" + race_name + "', GameState.selected_race set to '" + gs.selected_race + "'", LogManager.CATEGORY_UI)
|
||||||
|
if _race_select_standalone:
|
||||||
|
_race_select_standalone = false
|
||||||
|
if gs:
|
||||||
|
gs.race_chosen_before_connect = true
|
||||||
|
if pending_auto_join_after_race_select:
|
||||||
|
# --join --webrtc with no --race=: they just chose race, now start auto-join
|
||||||
|
pending_auto_join_after_race_select = false
|
||||||
|
_start_pending_auto_join()
|
||||||
|
LogManager.log("GameUI: Race chosen (" + race_name + "), fetching rooms to auto-join...", LogManager.CATEGORY_UI)
|
||||||
|
else:
|
||||||
|
if main_menu:
|
||||||
|
main_menu.visible = true
|
||||||
|
LogManager.log("GameUI: Race chosen for joining (" + race_name + "). Press Join when ready.", LogManager.CATEGORY_UI)
|
||||||
|
return
|
||||||
|
_start_game()
|
||||||
|
|
||||||
func _start_game():
|
func _start_game():
|
||||||
# Check if node is still in the tree before trying to access get_tree()
|
# Check if node is still in the tree before trying to access get_tree()
|
||||||
if not is_inside_tree():
|
if not is_inside_tree():
|
||||||
LogManager.log_error("GameUI: Cannot change scene - node is not in tree", LogManager.CATEGORY_UI)
|
LogManager.log_error("GameUI: Cannot change scene - node is not in tree", LogManager.CATEGORY_UI)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Hide menu
|
# Disconnect network callbacks so we don't get signals on freed game_ui after scene change
|
||||||
|
if network_manager:
|
||||||
|
if network_manager.connection_succeeded.is_connected(_on_connection_succeeded):
|
||||||
|
network_manager.connection_succeeded.disconnect(_on_connection_succeeded)
|
||||||
|
if network_manager.connection_failed.is_connected(_on_connection_failed):
|
||||||
|
network_manager.connection_failed.disconnect(_on_connection_failed)
|
||||||
|
|
||||||
|
# Hide menu (and race select if still present)
|
||||||
if main_menu:
|
if main_menu:
|
||||||
main_menu.visible = false
|
main_menu.visible = false
|
||||||
|
if select_class_instance and is_instance_valid(select_class_instance):
|
||||||
|
select_class_instance.queue_free()
|
||||||
|
select_class_instance = null
|
||||||
|
|
||||||
# Load the game scene
|
# Load the game scene
|
||||||
var tree = get_tree()
|
var tree = get_tree()
|
||||||
|
|||||||
@@ -442,6 +442,12 @@ func _send_initial_client_sync_with_retry(peer_id: int, local_count: int, retry_
|
|||||||
if is_inside_tree():
|
if is_inside_tree():
|
||||||
_rpc_to_ready_peers("_sync_spawn_player", [peer_id, local_count])
|
_rpc_to_ready_peers("_sync_spawn_player", [peer_id, local_count])
|
||||||
)
|
)
|
||||||
|
# Push existing players' state (equipment, inventory, race, appearance) to the new joiner
|
||||||
|
# so they see the host's Wizard inventory (tomes, etc.). Delay so joiner has spawned player nodes first.
|
||||||
|
get_tree().create_timer(0.4).timeout.connect(func():
|
||||||
|
if is_inside_tree():
|
||||||
|
_push_existing_players_state_to_client(peer_id)
|
||||||
|
)
|
||||||
|
|
||||||
# Sync broken interactable objects to the new client
|
# Sync broken interactable objects to the new client
|
||||||
# Wait a bit after dungeon sync to ensure objects are spawned first
|
# Wait a bit after dungeon sync to ensure objects are spawned first
|
||||||
@@ -499,6 +505,21 @@ func _send_initial_client_sync_with_retry(peer_id: int, local_count: int, retry_
|
|||||||
_sync_interactable_object_positions_to_client(peer_id)
|
_sync_interactable_object_positions_to_client(peer_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func _push_existing_players_state_to_client(peer_id: int) -> void:
|
||||||
|
# Server: push each server-authority player's full state (equipment, inventory, race, appearance) to the new joiner.
|
||||||
|
# Joiner otherwise never receives host's inventory because _sync_inventory was sent before joiner was "ready".
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if not entities_node:
|
||||||
|
return
|
||||||
|
var server_id = multiplayer.get_unique_id()
|
||||||
|
for child in entities_node.get_children():
|
||||||
|
if child.is_in_group("player") and child.get_multiplayer_authority() == server_id:
|
||||||
|
if child.has_method("_push_full_state_to_peer"):
|
||||||
|
child._push_full_state_to_peer(peer_id)
|
||||||
|
LogManager.log("GameWorld: Pushed existing players state to client " + str(peer_id), LogManager.CATEGORY_NETWORK)
|
||||||
|
|
||||||
func _rpc_to_ready_peers(method: String, args: Array = []):
|
func _rpc_to_ready_peers(method: String, args: Array = []):
|
||||||
# Send RPC to all clients whose GameWorld is ready
|
# Send RPC to all clients whose GameWorld is ready
|
||||||
if not multiplayer.has_multiplayer_peer() or not multiplayer.is_server():
|
if not multiplayer.has_multiplayer_peer() or not multiplayer.is_server():
|
||||||
@@ -1388,6 +1409,38 @@ func _sync_enemy_attack(enemy_name: String, enemy_index: int, direction: int, at
|
|||||||
# This is okay, just log it
|
# This is okay, just log it
|
||||||
print("GameWorld: Could not find enemy for attack sync: name=", enemy_name, " index=", enemy_index)
|
print("GameWorld: Could not find enemy for attack sync: name=", enemy_name, " index=", enemy_index)
|
||||||
|
|
||||||
|
@rpc("authority", "reliable")
|
||||||
|
func _sync_enemy_throw_bomb(bomb_name: String, enemy_name: String, enemy_index: int, bomb_pos: Vector2, throw_force: Vector2):
|
||||||
|
# Clients spawn bomb when server tells them an enemy threw one
|
||||||
|
if multiplayer.is_server():
|
||||||
|
return # Server ignores this (it's the sender)
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if not entities_node:
|
||||||
|
return
|
||||||
|
var enemy = null
|
||||||
|
for child in entities_node.get_children():
|
||||||
|
if child.is_in_group("enemy"):
|
||||||
|
if child.name == enemy_name:
|
||||||
|
enemy = child
|
||||||
|
break
|
||||||
|
elif child.has_meta("enemy_index") and child.get_meta("enemy_index") == enemy_index:
|
||||||
|
enemy = child
|
||||||
|
break
|
||||||
|
if not enemy:
|
||||||
|
return
|
||||||
|
var attack_bomb_scene = load("res://scenes/attack_bomb.tscn")
|
||||||
|
if not attack_bomb_scene:
|
||||||
|
push_error("GameWorld: Could not load attack_bomb scene for enemy throw sync!")
|
||||||
|
return
|
||||||
|
var bomb = attack_bomb_scene.instantiate()
|
||||||
|
bomb.name = bomb_name
|
||||||
|
entities_node.add_child(bomb)
|
||||||
|
bomb.global_position = bomb_pos
|
||||||
|
bomb.setup(bomb_pos, enemy, throw_force, true) # true = is_thrown
|
||||||
|
if bomb.has_node("Sprite2D"):
|
||||||
|
bomb.get_node("Sprite2D").visible = true
|
||||||
|
print("GameWorld: (client) synced enemy bomb: ", bomb_name, " at ", bomb_pos)
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _request_item_drop(item_data: Dictionary, drop_position: Vector2, player_peer_id: int):
|
func _request_item_drop(item_data: Dictionary, drop_position: Vector2, player_peer_id: int):
|
||||||
# Server receives item drop request from client
|
# Server receives item drop request from client
|
||||||
@@ -1468,6 +1521,111 @@ func _request_enemy_damage(enemy_name: String, enemy_index: int, damage: float,
|
|||||||
# Enemy not found - might already be freed
|
# Enemy not found - might already be freed
|
||||||
print("GameWorld: Could not find enemy for damage request: name=", enemy_name, " index=", enemy_index)
|
print("GameWorld: Could not find enemy for damage request: name=", enemy_name, " index=", enemy_index)
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _request_punch_hit_effect(pos_x: float, pos_y: float):
|
||||||
|
# Server receives request from client (punch hit); broadcast effect to all peers
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
_sync_punch_hit_effect(pos_x, pos_y)
|
||||||
|
if has_method("_rpc_to_ready_peers"):
|
||||||
|
_rpc_to_ready_peers("_sync_punch_hit_effect", [pos_x, pos_y])
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_punch_hit_effect(pos_x: float, pos_y: float):
|
||||||
|
# Spawn punch hit effect on this peer (called by server or when server broadcasts)
|
||||||
|
var scene = load("res://scenes/damage_effect_punch.tscn") as PackedScene
|
||||||
|
if not scene:
|
||||||
|
return
|
||||||
|
var effect = scene.instantiate()
|
||||||
|
add_child(effect)
|
||||||
|
effect.global_position = Vector2(pos_x, pos_y)
|
||||||
|
if effect.has_method("setup"):
|
||||||
|
effect.setup(Vector2(pos_x, pos_y))
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _request_slash_hit_effect(pos_x: float, pos_y: float):
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
_sync_slash_hit_effect(pos_x, pos_y)
|
||||||
|
if has_method("_rpc_to_ready_peers"):
|
||||||
|
_rpc_to_ready_peers("_sync_slash_hit_effect", [pos_x, pos_y])
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_slash_hit_effect(pos_x: float, pos_y: float):
|
||||||
|
var scene = load("res://scenes/damage_effect_slash.tscn") as PackedScene
|
||||||
|
if not scene:
|
||||||
|
return
|
||||||
|
var effect = scene.instantiate()
|
||||||
|
add_child(effect)
|
||||||
|
effect.global_position = Vector2(pos_x, pos_y)
|
||||||
|
if effect.has_method("setup"):
|
||||||
|
effect.setup(Vector2(pos_x, pos_y))
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _request_arrow_hit_effect(pos_x: float, pos_y: float):
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
_sync_arrow_hit_effect(pos_x, pos_y)
|
||||||
|
if has_method("_rpc_to_ready_peers"):
|
||||||
|
_rpc_to_ready_peers("_sync_arrow_hit_effect", [pos_x, pos_y])
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_arrow_hit_effect(pos_x: float, pos_y: float):
|
||||||
|
var scene = load("res://scenes/damage_effect_arrow.tscn") as PackedScene
|
||||||
|
if not scene:
|
||||||
|
return
|
||||||
|
var effect = scene.instantiate()
|
||||||
|
add_child(effect)
|
||||||
|
effect.global_position = Vector2(pos_x, pos_y)
|
||||||
|
if effect.has_method("setup"):
|
||||||
|
effect.setup(Vector2(pos_x, pos_y))
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _request_axe_hit_effect(pos_x: float, pos_y: float):
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
_sync_axe_hit_effect(pos_x, pos_y)
|
||||||
|
if has_method("_rpc_to_ready_peers"):
|
||||||
|
_rpc_to_ready_peers("_sync_axe_hit_effect", [pos_x, pos_y])
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_axe_hit_effect(pos_x: float, pos_y: float):
|
||||||
|
var scene = load("res://scenes/damage_effect_axe.tscn") as PackedScene
|
||||||
|
if not scene:
|
||||||
|
return
|
||||||
|
var effect = scene.instantiate()
|
||||||
|
add_child(effect)
|
||||||
|
effect.global_position = Vector2(pos_x, pos_y)
|
||||||
|
if effect.has_method("setup"):
|
||||||
|
effect.setup(Vector2(pos_x, pos_y))
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _request_bite_effect(player_name: String):
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
_sync_bite_effect(player_name)
|
||||||
|
if has_method("_rpc_to_ready_peers"):
|
||||||
|
_rpc_to_ready_peers("_sync_bite_effect", [player_name])
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_bite_effect(player_name: String):
|
||||||
|
var player = player_manager.get_node_or_null(player_name) if player_manager else null
|
||||||
|
if not player and is_instance_valid(self):
|
||||||
|
for p in get_tree().get_nodes_in_group("player"):
|
||||||
|
if p.name == player_name:
|
||||||
|
player = p
|
||||||
|
break
|
||||||
|
if not player or not is_instance_valid(player):
|
||||||
|
return
|
||||||
|
var scene = load("res://scenes/damage_effect_bite.tscn") as PackedScene
|
||||||
|
if not scene:
|
||||||
|
return
|
||||||
|
var effect = scene.instantiate()
|
||||||
|
player.add_child(effect)
|
||||||
|
effect.position = Vector2.ZERO
|
||||||
|
if effect.has_method("setup"):
|
||||||
|
effect.setup(Vector2.ZERO)
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _request_loot_pickup(loot_id: int, loot_position: Vector2, player_peer_id: int):
|
func _request_loot_pickup(loot_id: int, loot_position: Vector2, player_peer_id: int):
|
||||||
# Server receives loot pickup request from client
|
# Server receives loot pickup request from client
|
||||||
@@ -1575,6 +1733,19 @@ func _sync_hide_level_complete():
|
|||||||
if level_complete_ui:
|
if level_complete_ui:
|
||||||
level_complete_ui.visible = false
|
level_complete_ui.visible = false
|
||||||
|
|
||||||
|
@rpc("authority", "reliable")
|
||||||
|
func _sync_disable_stairs():
|
||||||
|
# Clients receive stairs disable sync from server
|
||||||
|
if multiplayer.is_server():
|
||||||
|
return # Server ignores this (it's the sender)
|
||||||
|
|
||||||
|
# Disable stairs Area2D on clients so no player can activate it
|
||||||
|
var stairs_area = get_node_or_null("StairsArea")
|
||||||
|
if stairs_area:
|
||||||
|
stairs_area.set_deferred("monitoring", false)
|
||||||
|
stairs_area.set_deferred("monitorable", false)
|
||||||
|
LogManager.log("GameWorld: Client disabled stairs Area2D", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
@rpc("authority", "reliable")
|
@rpc("authority", "reliable")
|
||||||
func _sync_restore_player_controls():
|
func _sync_restore_player_controls():
|
||||||
# Clients receive restore controls/collision sync from server
|
# Clients receive restore controls/collision sync from server
|
||||||
@@ -2860,6 +3031,20 @@ func _generate_dungeon():
|
|||||||
dead_players.clear()
|
dead_players.clear()
|
||||||
respawn_all_check_running = false
|
respawn_all_check_running = false
|
||||||
|
|
||||||
|
# CRITICAL: Reset all players' death state flags when transitioning to a new level
|
||||||
|
# This prevents GAME OVER from showing incorrectly when players are alive in the new level
|
||||||
|
var players = get_tree().get_nodes_in_group("player")
|
||||||
|
for player in players:
|
||||||
|
if is_instance_valid(player):
|
||||||
|
if "is_dead" in player:
|
||||||
|
player.is_dead = false
|
||||||
|
if "is_processing_death" in player:
|
||||||
|
player.is_processing_death = false
|
||||||
|
# Reset exit notification flag for new level
|
||||||
|
if "has_seen_exit_this_level" in player:
|
||||||
|
player.has_seen_exit_this_level = false
|
||||||
|
LogManager.log("GameWorld: Reset death state for player " + str(player.name) + " on level transition", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
# Hide game over UI if it exists
|
# Hide game over UI if it exists
|
||||||
var game_over_ui = get_node_or_null("GameOverUI")
|
var game_over_ui = get_node_or_null("GameOverUI")
|
||||||
if game_over_ui:
|
if game_over_ui:
|
||||||
@@ -2968,19 +3153,19 @@ func _generate_dungeon():
|
|||||||
|
|
||||||
# Dungeon shader color replacement: 13 original colors (wall x6, ground x5, fallout x2)
|
# Dungeon shader color replacement: 13 original colors (wall x6, ground x5, fallout x2)
|
||||||
const _DUNGEON_ORIGINALS: Array = [
|
const _DUNGEON_ORIGINALS: Array = [
|
||||||
Color(24/255.0, 59/255.0, 255/255.0), # 0 wall
|
Color(24 / 255.0, 59 / 255.0, 255 / 255.0), # 0 wall
|
||||||
Color(33/255.0, 50/255.0, 195/255.0), # 1 wall
|
Color(33 / 255.0, 50 / 255.0, 195 / 255.0), # 1 wall
|
||||||
Color(98/255.0, 29/255.0, 93/255.0), # 2 wall
|
Color(98 / 255.0, 29 / 255.0, 93 / 255.0), # 2 wall
|
||||||
Color(66/255.0, 13/255.0, 52/255.0), # 3 wall
|
Color(66 / 255.0, 13 / 255.0, 52 / 255.0), # 3 wall
|
||||||
Color(74/255.0, 33/255.0, 134/255.0), # 4 wall
|
Color(74 / 255.0, 33 / 255.0, 134 / 255.0), # 4 wall
|
||||||
Color(50/255.0, 12/255.0, 23/255.0), # 5 wall
|
Color(50 / 255.0, 12 / 255.0, 23 / 255.0), # 5 wall
|
||||||
Color(149/255.0, 79/255.0, 111/255.0), # 6 ground
|
Color(149 / 255.0, 79 / 255.0, 111 / 255.0), # 6 ground
|
||||||
Color(192/255.0, 95/255.0, 193/255.0), # 7 ground
|
Color(192 / 255.0, 95 / 255.0, 193 / 255.0), # 7 ground
|
||||||
Color(48/255.0, 38/255.0, 20/255.0), # 8 ground
|
Color(48 / 255.0, 38 / 255.0, 20 / 255.0), # 8 ground
|
||||||
Color(143/255.0, 71/255.0, 112/255.0), # 9 ground
|
Color(143 / 255.0, 71 / 255.0, 112 / 255.0), # 9 ground
|
||||||
Color(106/255.0, 62/255.0, 57/255.0), # 10 ground
|
Color(106 / 255.0, 62 / 255.0, 57 / 255.0), # 10 ground
|
||||||
Color(69/255.0, 42/255.0, 31/255.0), # 11 fallout
|
Color(69 / 255.0, 42 / 255.0, 31 / 255.0), # 11 fallout
|
||||||
Color(53/255.0, 46/255.0, 26/255.0), # 12 fallout
|
Color(53 / 255.0, 46 / 255.0, 26 / 255.0), # 12 fallout
|
||||||
]
|
]
|
||||||
|
|
||||||
# Original wall slots ordered light→dark (by luminance). Tile art uses these for highlight/mid/shadow.
|
# Original wall slots ordered light→dark (by luminance). Tile art uses these for highlight/mid/shadow.
|
||||||
@@ -3018,8 +3203,8 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
|||||||
match scheme_index:
|
match scheme_index:
|
||||||
0: # 1️⃣ Arcane Blue (magic / night / mana)
|
0: # 1️⃣ Arcane Blue (magic / night / mana)
|
||||||
walls = [
|
walls = [
|
||||||
Color(24/255.0, 59/255.0, 255/255.0), Color(80/255.0, 120/255.0, 255/255.0), Color(140/255.0, 180/255.0, 255/255.0),
|
Color(24 / 255.0, 59 / 255.0, 255 / 255.0), Color(80 / 255.0, 120 / 255.0, 255 / 255.0), Color(140 / 255.0, 180 / 255.0, 255 / 255.0),
|
||||||
Color(10/255.0, 30/255.0, 120/255.0), Color(180/255.0, 200/255.0, 255/255.0), Color(220/255.0, 230/255.0, 255/255.0),
|
Color(10 / 255.0, 30 / 255.0, 120 / 255.0), Color(180 / 255.0, 200 / 255.0, 255 / 255.0), Color(220 / 255.0, 230 / 255.0, 255 / 255.0),
|
||||||
]
|
]
|
||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.78, 0.48, 0.24), Color(0.84, 0.54, 0.30), Color(0.58, 0.36, 0.16),
|
Color(0.78, 0.48, 0.24), Color(0.84, 0.54, 0.30), Color(0.58, 0.36, 0.16),
|
||||||
@@ -3028,8 +3213,8 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
|||||||
]
|
]
|
||||||
1: # 2️⃣ Crimson Void (blood / corruption / danger)
|
1: # 2️⃣ Crimson Void (blood / corruption / danger)
|
||||||
walls = [
|
walls = [
|
||||||
Color(120/255.0, 20/255.0, 40/255.0), Color(180/255.0, 40/255.0, 60/255.0), Color(220/255.0, 90/255.0, 110/255.0),
|
Color(120 / 255.0, 20 / 255.0, 40 / 255.0), Color(180 / 255.0, 40 / 255.0, 60 / 255.0), Color(220 / 255.0, 90 / 255.0, 110 / 255.0),
|
||||||
Color(60/255.0, 5/255.0, 20/255.0), Color(255/255.0, 140/255.0, 160/255.0), Color(90/255.0, 10/255.0, 30/255.0),
|
Color(60 / 255.0, 5 / 255.0, 20 / 255.0), Color(255 / 255.0, 140 / 255.0, 160 / 255.0), Color(90 / 255.0, 10 / 255.0, 30 / 255.0),
|
||||||
]
|
]
|
||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.22, 0.58, 0.62), Color(0.28, 0.64, 0.66), Color(0.18, 0.48, 0.52),
|
Color(0.22, 0.58, 0.62), Color(0.28, 0.64, 0.66), Color(0.18, 0.48, 0.52),
|
||||||
@@ -3038,8 +3223,8 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
|||||||
]
|
]
|
||||||
2: # 3️⃣ Toxic Green (poison / nature / alchemy)
|
2: # 3️⃣ Toxic Green (poison / nature / alchemy)
|
||||||
walls = [
|
walls = [
|
||||||
Color(20/255.0, 120/255.0, 40/255.0), Color(60/255.0, 180/255.0, 90/255.0), Color(120/255.0, 220/255.0, 160/255.0),
|
Color(20 / 255.0, 120 / 255.0, 40 / 255.0), Color(60 / 255.0, 180 / 255.0, 90 / 255.0), Color(120 / 255.0, 220 / 255.0, 160 / 255.0),
|
||||||
Color(10/255.0, 60/255.0, 25/255.0), Color(180/255.0, 255/255.0, 210/255.0), Color(40/255.0, 90/255.0, 55/255.0),
|
Color(10 / 255.0, 60 / 255.0, 25 / 255.0), Color(180 / 255.0, 255 / 255.0, 210 / 255.0), Color(40 / 255.0, 90 / 255.0, 55 / 255.0),
|
||||||
]
|
]
|
||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.64, 0.36, 0.72), Color(0.70, 0.42, 0.78), Color(0.48, 0.26, 0.56),
|
Color(0.64, 0.36, 0.72), Color(0.70, 0.42, 0.78), Color(0.48, 0.26, 0.56),
|
||||||
@@ -3048,8 +3233,8 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
|||||||
]
|
]
|
||||||
3: # 4️⃣ Stone Grey (industrial / ruins / UI neutral)
|
3: # 4️⃣ Stone Grey (industrial / ruins / UI neutral)
|
||||||
walls = [
|
walls = [
|
||||||
Color(40/255.0, 40/255.0, 45/255.0), Color(80/255.0, 80/255.0, 85/255.0), Color(130/255.0, 130/255.0, 135/255.0),
|
Color(40 / 255.0, 40 / 255.0, 45 / 255.0), Color(80 / 255.0, 80 / 255.0, 85 / 255.0), Color(130 / 255.0, 130 / 255.0, 135 / 255.0),
|
||||||
Color(20/255.0, 20/255.0, 25/255.0), Color(180/255.0, 180/255.0, 185/255.0), Color(220/255.0, 220/255.0, 225/255.0),
|
Color(20 / 255.0, 20 / 255.0, 25 / 255.0), Color(180 / 255.0, 180 / 255.0, 185 / 255.0), Color(220 / 255.0, 220 / 255.0, 225 / 255.0),
|
||||||
]
|
]
|
||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.50, 0.50, 0.52), Color(0.55, 0.55, 0.57), Color(0.35, 0.35, 0.38),
|
Color(0.50, 0.50, 0.52), Color(0.55, 0.55, 0.57), Color(0.35, 0.35, 0.38),
|
||||||
@@ -3058,8 +3243,8 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
|||||||
]
|
]
|
||||||
4: # 5️⃣ Royal Purple (arcane royalty / bosses)
|
4: # 5️⃣ Royal Purple (arcane royalty / bosses)
|
||||||
walls = [
|
walls = [
|
||||||
Color(80/255.0, 30/255.0, 130/255.0), Color(130/255.0, 70/255.0, 180/255.0), Color(180/255.0, 130/255.0, 220/255.0),
|
Color(80 / 255.0, 30 / 255.0, 130 / 255.0), Color(130 / 255.0, 70 / 255.0, 180 / 255.0), Color(180 / 255.0, 130 / 255.0, 220 / 255.0),
|
||||||
Color(40/255.0, 10/255.0, 80/255.0), Color(220/255.0, 180/255.0, 255/255.0), Color(100/255.0, 60/255.0, 150/255.0),
|
Color(40 / 255.0, 10 / 255.0, 80 / 255.0), Color(220 / 255.0, 180 / 255.0, 255 / 255.0), Color(100 / 255.0, 60 / 255.0, 150 / 255.0),
|
||||||
]
|
]
|
||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.90, 0.68, 0.22), Color(0.94, 0.74, 0.28), Color(0.72, 0.52, 0.14),
|
Color(0.90, 0.68, 0.22), Color(0.94, 0.74, 0.28), Color(0.72, 0.52, 0.14),
|
||||||
@@ -3068,28 +3253,28 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
|||||||
]
|
]
|
||||||
5: # 6️⃣ Desert Gold (sand / temples / sunlight)
|
5: # 6️⃣ Desert Gold (sand / temples / sunlight)
|
||||||
walls = [
|
walls = [
|
||||||
Color(150/255.0, 110/255.0, 40/255.0), Color(200/255.0, 160/255.0, 80/255.0), Color(240/255.0, 210/255.0, 140/255.0),
|
Color(150 / 255.0, 110 / 255.0, 40 / 255.0), Color(200 / 255.0, 160 / 255.0, 80 / 255.0), Color(240 / 255.0, 210 / 255.0, 140 / 255.0),
|
||||||
Color(90/255.0, 60/255.0, 15/255.0), Color(255/255.0, 230/255.0, 170/255.0), Color(170/255.0, 130/255.0, 60/255.0),
|
Color(90 / 255.0, 60 / 255.0, 15 / 255.0), Color(255 / 255.0, 230 / 255.0, 170 / 255.0), Color(170 / 255.0, 130 / 255.0, 60 / 255.0),
|
||||||
]
|
]
|
||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.22, 0.58, 0.62), Color(0.28, 0.64, 0.66), Color(0.18, 0.48, 0.52),
|
Color(0.22, 0.58, 0.62), Color(0.28, 0.64, 0.66), Color(0.18, 0.48, 0.52),
|
||||||
Color(0.20, 0.54, 0.58), Color(0.20, 0.50, 0.54), Color(0.26, 0.38, 0.34),
|
Color(0.20, 0.54, 0.58), Color(0.20, 0.50, 0.54), Color(0.26, 0.38, 0.34),
|
||||||
Color(0.22, 0.34, 0.30),
|
Color(0.22, 0.34, 0.30),
|
||||||
]
|
]
|
||||||
6: # 7️⃣ Neon Cyber (sci-fi / UI / hacking)
|
6: # 7️⃣ Ancient Stone (medieval / castles / ruins)
|
||||||
walls = [
|
walls = [
|
||||||
Color(20/255.0, 240/255.0, 220/255.0), Color(240/255.0, 60/255.0, 220/255.0), Color(120/255.0, 120/255.0, 255/255.0),
|
Color(120 / 255.0, 110 / 255.0, 100 / 255.0), Color(160 / 255.0, 150 / 255.0, 140 / 255.0), Color(200 / 255.0, 190 / 255.0, 180 / 255.0),
|
||||||
Color(10/255.0, 20/255.0, 40/255.0), Color(255/255.0, 255/255.0, 255/255.0), Color(80/255.0, 255/255.0, 180/255.0),
|
Color(60 / 255.0, 55 / 255.0, 50 / 255.0), Color(220 / 255.0, 210 / 255.0, 200 / 255.0), Color(90 / 255.0, 85 / 255.0, 75 / 255.0),
|
||||||
]
|
]
|
||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.45, 0.28, 0.55), Color(0.52, 0.35, 0.62), Color(0.38, 0.22, 0.48),
|
Color(0.35, 0.28, 0.22), Color(0.40, 0.32, 0.26), Color(0.28, 0.22, 0.18),
|
||||||
Color(0.48, 0.32, 0.58), Color(0.42, 0.26, 0.52), Color(0.28, 0.18, 0.38),
|
Color(0.38, 0.30, 0.24), Color(0.32, 0.26, 0.20), Color(0.24, 0.20, 0.16),
|
||||||
Color(0.22, 0.14, 0.32),
|
Color(0.20, 0.16, 0.14),
|
||||||
]
|
]
|
||||||
7: # 8️⃣ Infernal Lava (hell / bosses / damage)
|
7: # 8️⃣ Infernal Lava (hell / bosses / damage)
|
||||||
walls = [
|
walls = [
|
||||||
Color(180/255.0, 40/255.0, 20/255.0), Color(240/255.0, 90/255.0, 30/255.0), Color(255/255.0, 160/255.0, 80/255.0),
|
Color(180 / 255.0, 40 / 255.0, 20 / 255.0), Color(240 / 255.0, 90 / 255.0, 30 / 255.0), Color(255 / 255.0, 160 / 255.0, 80 / 255.0),
|
||||||
Color(90/255.0, 10/255.0, 5/255.0), Color(255/255.0, 210/255.0, 160/255.0), Color(140/255.0, 30/255.0, 15/255.0),
|
Color(90 / 255.0, 10 / 255.0, 5 / 255.0), Color(255 / 255.0, 210 / 255.0, 160 / 255.0), Color(140 / 255.0, 30 / 255.0, 15 / 255.0),
|
||||||
]
|
]
|
||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.32, 0.68, 0.48), Color(0.38, 0.74, 0.54), Color(0.22, 0.52, 0.36),
|
Color(0.32, 0.68, 0.48), Color(0.38, 0.74, 0.54), Color(0.22, 0.52, 0.36),
|
||||||
@@ -4356,6 +4541,22 @@ func _sync_dungeon_blob_metadata(seed_value: int, level: int, map_size_sync: Vec
|
|||||||
# Clear any ongoing syncs
|
# Clear any ongoing syncs
|
||||||
dungeon_sync_in_progress.clear()
|
dungeon_sync_in_progress.clear()
|
||||||
|
|
||||||
|
# CRITICAL: Reset all players' death state flags when transitioning to a new level
|
||||||
|
# This prevents GAME OVER from showing incorrectly when players are alive in the new level
|
||||||
|
var players = get_tree().get_nodes_in_group("player")
|
||||||
|
for player in players:
|
||||||
|
if is_instance_valid(player):
|
||||||
|
if "is_dead" in player:
|
||||||
|
player.is_dead = false
|
||||||
|
if "is_processing_death" in player:
|
||||||
|
player.is_processing_death = false
|
||||||
|
# Reset exit notification flag for new level
|
||||||
|
if "has_seen_exit_this_level" in player:
|
||||||
|
player.has_seen_exit_this_level = false
|
||||||
|
# Also reset game over flag on client
|
||||||
|
game_over_triggered = false
|
||||||
|
LogManager.log("GameWorld: Client - Reset death state for all players on level transition", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
# CRITICAL: Store defeated enemies, broken objects, opened chests, and door states in temporary variables
|
# CRITICAL: Store defeated enemies, broken objects, opened chests, and door states in temporary variables
|
||||||
# because _clear_level() will clear them, but we need them for the new level
|
# because _clear_level() will clear them, but we need them for the new level
|
||||||
var temp_defeated_enemies = defeated_enemies.duplicate()
|
var temp_defeated_enemies = defeated_enemies.duplicate()
|
||||||
@@ -5631,6 +5832,39 @@ func _apply_trap_state_by_name(trap_name: String, is_detected: bool, is_disarmed
|
|||||||
if "activation_area" in trap and trap.activation_area:
|
if "activation_area" in trap and trap.activation_area:
|
||||||
trap.activation_area.monitoring = false
|
trap.activation_area.monitoring = false
|
||||||
|
|
||||||
|
# Grant EXP to all players when trap is disarmed (only on server)
|
||||||
|
if multiplayer.is_server():
|
||||||
|
var trap_exp_reward = 8.0 # EXP reward for disarming a trap
|
||||||
|
var all_players = get_tree().get_nodes_in_group("player")
|
||||||
|
var valid_players = []
|
||||||
|
for player in all_players:
|
||||||
|
if is_instance_valid(player) and player.character_stats:
|
||||||
|
valid_players.append(player)
|
||||||
|
|
||||||
|
if valid_players.size() > 0:
|
||||||
|
# Split EXP evenly among all players
|
||||||
|
var exp_per_player = trap_exp_reward / valid_players.size()
|
||||||
|
for player in valid_players:
|
||||||
|
player.character_stats.add_xp(exp_per_player)
|
||||||
|
LogManager.log("Trap disarmed: granted " + str(exp_per_player) + " EXP to " + str(player.name) + " (shared from " + str(trap_exp_reward) + " total)", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
|
# Sync EXP to client if this player belongs to a client
|
||||||
|
var player_peer_id = player.get_multiplayer_authority()
|
||||||
|
if player_peer_id != 0 and player_peer_id != multiplayer.get_unique_id() and player.has_method("_sync_stats_update"):
|
||||||
|
var coins = player.character_stats.coin if "coin" in player.character_stats else 0
|
||||||
|
var xp = player.character_stats.xp if "xp" in player.character_stats else 0.0
|
||||||
|
player._sync_stats_update.rpc_id(player_peer_id, player.character_stats.kills, coins, xp)
|
||||||
|
|
||||||
|
# Show floating EXP text at trap position and sync to all clients
|
||||||
|
if is_instance_valid(trap) and "global_position" in trap:
|
||||||
|
# Show locally first
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_show_exp_number_at_position"):
|
||||||
|
game_world._show_exp_number_at_position(exp_per_player, trap.global_position)
|
||||||
|
# Sync to all clients via game_world
|
||||||
|
if game_world and game_world.has_method("_sync_exp_text_at_position") and multiplayer.has_multiplayer_peer():
|
||||||
|
game_world._sync_exp_text_at_position.rpc(exp_per_player, trap.global_position)
|
||||||
|
|
||||||
@rpc("authority", "reliable")
|
@rpc("authority", "reliable")
|
||||||
func _sync_trap_state_by_name(trap_name: String, is_detected: bool, is_disarmed: bool):
|
func _sync_trap_state_by_name(trap_name: String, is_detected: bool, is_disarmed: bool):
|
||||||
# Client receives trap state sync by name (avoids node path RPC errors)
|
# Client receives trap state sync by name (avoids node path RPC errors)
|
||||||
@@ -6480,11 +6714,34 @@ func _position_new_joiner_and_sync_positions(new_peer_id: int):
|
|||||||
if player.peer_id < new_peer_id:
|
if player.peer_id < new_peer_id:
|
||||||
used_spawn_index += 1
|
used_spawn_index += 1
|
||||||
|
|
||||||
|
# Check if entrance exists (level > 1) - joiners should use entrance if available
|
||||||
|
var has_entrance = not dungeon_data.is_empty() and dungeon_data.has("entrance") and not dungeon_data.entrance.is_empty()
|
||||||
|
|
||||||
|
if has_entrance and current_level > 1:
|
||||||
|
# Position new joiner in entrance and start walk-out sequence (same as level transition)
|
||||||
|
# CRITICAL: Disable falling cutscene for joiners in entrance (they should walk out, not fall)
|
||||||
|
for player in new_joiner_players:
|
||||||
|
if is_instance_valid(player):
|
||||||
|
player.spawn_landing = false
|
||||||
|
player.position_z = 0.0
|
||||||
|
player.velocity_z = 0.0
|
||||||
|
player.is_airborne = false
|
||||||
|
_position_players_in_entrance(new_joiner_players)
|
||||||
|
else:
|
||||||
|
# Normal spawn positioning (level 1 or no entrance)
|
||||||
# Assign spawn points to new joiner's players (starting from the next available spawn point)
|
# Assign spawn points to new joiner's players (starting from the next available spawn point)
|
||||||
var spawn_index = used_spawn_index
|
var spawn_index = used_spawn_index
|
||||||
for player in new_joiner_players:
|
for player in new_joiner_players:
|
||||||
if spawn_index < player_manager.spawn_points.size():
|
if spawn_index < player_manager.spawn_points.size():
|
||||||
var new_pos = player_manager.spawn_points[spawn_index]
|
var new_pos = player_manager.spawn_points[spawn_index]
|
||||||
|
|
||||||
|
# CRITICAL: Verify spawn position is safe (on floor, not in wall)
|
||||||
|
if not _is_safe_spawn_position(new_pos):
|
||||||
|
# Spawn position is not safe, find a nearby safe position
|
||||||
|
var safe_pos = _find_nearby_safe_spawn_position(new_pos, 128.0)
|
||||||
|
LogManager.log("GameWorld: WARNING - Spawn position " + str(new_pos) + " for new joiner " + player.name + " was unsafe, using safe position: " + str(safe_pos), LogManager.CATEGORY_GAMEPLAY)
|
||||||
|
new_pos = safe_pos
|
||||||
|
|
||||||
player.global_position = new_pos
|
player.global_position = new_pos
|
||||||
LogManager.log("GameWorld: Positioned new joiner player " + player.name + " (peer_id: " + str(player.peer_id) + ") at spawn index " + str(spawn_index) + " position " + str(new_pos), LogManager.CATEGORY_GAMEPLAY)
|
LogManager.log("GameWorld: Positioned new joiner player " + player.name + " (peer_id: " + str(player.peer_id) + ") at spawn index " + str(spawn_index) + " position " + str(new_pos), LogManager.CATEGORY_GAMEPLAY)
|
||||||
spawn_index += 1
|
spawn_index += 1
|
||||||
@@ -6493,11 +6750,25 @@ func _position_new_joiner_and_sync_positions(new_peer_id: int):
|
|||||||
var room_center_x = (start_room.x + start_room.w / 2.0) * 16
|
var room_center_x = (start_room.x + start_room.w / 2.0) * 16
|
||||||
var room_center_y = (start_room.y + start_room.h / 2.0) * 16
|
var room_center_y = (start_room.y + start_room.h / 2.0) * 16
|
||||||
var fallback_pos = Vector2(room_center_x, room_center_y)
|
var fallback_pos = Vector2(room_center_x, room_center_y)
|
||||||
|
|
||||||
|
# CRITICAL: Verify fallback position is safe
|
||||||
|
if not _is_safe_spawn_position(fallback_pos):
|
||||||
|
fallback_pos = _find_nearby_safe_spawn_position(fallback_pos, 128.0)
|
||||||
|
LogManager.log("GameWorld: WARNING - Fallback spawn position for new joiner " + player.name + " was unsafe, using safe position: " + str(fallback_pos), LogManager.CATEGORY_GAMEPLAY)
|
||||||
|
|
||||||
player.global_position = fallback_pos
|
player.global_position = fallback_pos
|
||||||
LogManager.log("GameWorld: Positioned new joiner player " + player.name + " (peer_id: " + str(player.peer_id) + ") at start room center " + str(fallback_pos), LogManager.CATEGORY_GAMEPLAY)
|
LogManager.log("GameWorld: Positioned new joiner player " + player.name + " (peer_id: " + str(player.peer_id) + ") at start room center " + str(fallback_pos), LogManager.CATEGORY_GAMEPLAY)
|
||||||
|
|
||||||
# Host never receives _sync_player_position_by_name; ensure joiner is visible on host
|
# Host never receives _sync_player_position_by_name; ensure joiner is visible on host
|
||||||
player.visible = true
|
player.visible = true
|
||||||
|
|
||||||
|
# CRITICAL: Remove black fade overlay for new joiner (in case it was left from previous level)
|
||||||
|
# This prevents joiners from seeing a black screen
|
||||||
|
# Remove on server (for host's view) and sync to joiner's client
|
||||||
|
_remove_black_fade_overlay()
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
_sync_remove_black_fade.rpc_id(new_peer_id)
|
||||||
|
|
||||||
# Send ALL current player positions to the new joiner (so they see everyone correctly)
|
# Send ALL current player positions to the new joiner (so they see everyone correctly)
|
||||||
# Wait longer to ensure the client has fully loaded the game scene and all player nodes are spawned
|
# Wait longer to ensure the client has fully loaded the game scene and all player nodes are spawned
|
||||||
await get_tree().create_timer(0.3).timeout # Wait 0.3 seconds after positioning
|
await get_tree().create_timer(0.3).timeout # Wait 0.3 seconds after positioning
|
||||||
@@ -7111,6 +7382,17 @@ func _on_player_reached_stairs(player: Node):
|
|||||||
# Mark as triggered to prevent re-triggering
|
# Mark as triggered to prevent re-triggering
|
||||||
level_complete_triggered = true
|
level_complete_triggered = true
|
||||||
|
|
||||||
|
# CRITICAL: Disable stairs so no other player can activate it
|
||||||
|
# Use set_deferred() because we're in a signal callback (body_entered)
|
||||||
|
var stairs_area = get_node_or_null("StairsArea")
|
||||||
|
if stairs_area:
|
||||||
|
stairs_area.set_deferred("monitoring", false)
|
||||||
|
stairs_area.set_deferred("monitorable", false)
|
||||||
|
LogManager.log("GameWorld: Disabled stairs Area2D to prevent other players from activating", LogManager.CATEGORY_DUNGEON)
|
||||||
|
# Sync stairs disable to all clients
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
_rpc_to_ready_peers("_sync_disable_stairs", [])
|
||||||
|
|
||||||
# Disable controls and collision for the player who reached stairs
|
# Disable controls and collision for the player who reached stairs
|
||||||
var player_peer_id = player.get_multiplayer_authority() if player.has_method("get_multiplayer_authority") else 0
|
var player_peer_id = player.get_multiplayer_authority() if player.has_method("get_multiplayer_authority") else 0
|
||||||
player.controls_disabled = true
|
player.controls_disabled = true
|
||||||
@@ -7149,6 +7431,38 @@ func _on_player_reached_stairs(player: Node):
|
|||||||
# Stop background music when level completes
|
# Stop background music when level completes
|
||||||
_stop_bg_music()
|
_stop_bg_music()
|
||||||
|
|
||||||
|
# Grant EXP to all players for completing the level
|
||||||
|
var level_completion_exp = 15.0 # EXP reward for completing a level
|
||||||
|
var all_players = get_tree().get_nodes_in_group("player")
|
||||||
|
var valid_players = []
|
||||||
|
for p in all_players:
|
||||||
|
if is_instance_valid(p) and p.character_stats:
|
||||||
|
valid_players.append(p)
|
||||||
|
|
||||||
|
if valid_players.size() > 0:
|
||||||
|
# Split EXP evenly among all players
|
||||||
|
var exp_per_player = level_completion_exp / valid_players.size()
|
||||||
|
for p in valid_players:
|
||||||
|
p.character_stats.add_xp(exp_per_player)
|
||||||
|
LogManager.log("Level complete: granted " + str(exp_per_player) + " EXP to " + str(p.name) + " (shared from " + str(level_completion_exp) + " total)", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
|
# Sync EXP to client if this player belongs to a client
|
||||||
|
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||||
|
var p_peer_id = p.get_multiplayer_authority()
|
||||||
|
if p_peer_id != 0 and p_peer_id != multiplayer.get_unique_id() and p.has_method("_sync_stats_update"):
|
||||||
|
var coins = p.character_stats.coin if "coin" in p.character_stats else 0
|
||||||
|
var xp = p.character_stats.xp if "xp" in p.character_stats else 0.0
|
||||||
|
p._sync_stats_update.rpc_id(p_peer_id, p.character_stats.kills, coins, xp)
|
||||||
|
|
||||||
|
# Show floating EXP text at player position and sync to all clients
|
||||||
|
if multiplayer.is_server() and is_instance_valid(p):
|
||||||
|
# Show locally first
|
||||||
|
_show_exp_number_at_player(exp_per_player, p)
|
||||||
|
# Sync to all clients via RPC
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
var p_peer_id = p.get_multiplayer_authority()
|
||||||
|
_sync_exp_text_at_player.rpc(exp_per_player, p_peer_id)
|
||||||
|
|
||||||
# Show level complete UI (server and clients) with per-player stats
|
# Show level complete UI (server and clients) with per-player stats
|
||||||
_show_level_complete_ui(level_time)
|
_show_level_complete_ui(level_time)
|
||||||
# Sync to all clients (each client will show their own local player's stats)
|
# Sync to all clients (each client will show their own local player's stats)
|
||||||
@@ -7598,6 +7912,78 @@ func _load_floating_text_layer() -> void:
|
|||||||
layer.follow_viewport_enabled = true
|
layer.follow_viewport_enabled = true
|
||||||
add_child(layer)
|
add_child(layer)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func _sync_exp_text_at_position(amount: float, exp_pos: Vector2):
|
||||||
|
# Sync EXP text display to all clients at a specific position
|
||||||
|
_show_exp_number_at_position(amount, exp_pos)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func _sync_exp_text_at_player(amount: float, player_peer_id: int):
|
||||||
|
# Sync EXP text display to all clients at a player's position
|
||||||
|
var player = null
|
||||||
|
var players = get_tree().get_nodes_in_group("player")
|
||||||
|
for p in players:
|
||||||
|
if p.get_multiplayer_authority() == player_peer_id:
|
||||||
|
player = p
|
||||||
|
break
|
||||||
|
|
||||||
|
if player and is_instance_valid(player):
|
||||||
|
_show_exp_number_at_player(amount, player)
|
||||||
|
|
||||||
|
func _show_exp_number_at_position(amount: float, exp_pos: Vector2):
|
||||||
|
# Show EXP number (green, using dmg_numbers.png font) at a specific position
|
||||||
|
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
||||||
|
if not damage_number_scene:
|
||||||
|
return
|
||||||
|
|
||||||
|
var exp_label = damage_number_scene.instantiate()
|
||||||
|
if not exp_label:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set text and color for EXP (green)
|
||||||
|
exp_label.label = "+" + str(int(amount)) + " EXP"
|
||||||
|
exp_label.color = Color(0.4, 1.0, 0.4) # Bright green
|
||||||
|
exp_label.z_index = 5
|
||||||
|
|
||||||
|
# Direction is straight up
|
||||||
|
exp_label.direction = Vector2(0, -1)
|
||||||
|
|
||||||
|
# Position at specified location
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if entities_node:
|
||||||
|
entities_node.add_child(exp_label)
|
||||||
|
exp_label.global_position = exp_pos + Vector2(0, -20) # Slightly above
|
||||||
|
else:
|
||||||
|
get_tree().current_scene.add_child(exp_label)
|
||||||
|
exp_label.global_position = exp_pos + Vector2(0, -20)
|
||||||
|
|
||||||
|
func _show_exp_number_at_player(amount: float, player: Node):
|
||||||
|
# Show EXP number (green, using dmg_numbers.png font) at player position
|
||||||
|
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
||||||
|
if not damage_number_scene or not is_instance_valid(player):
|
||||||
|
return
|
||||||
|
|
||||||
|
var exp_label = damage_number_scene.instantiate()
|
||||||
|
if not exp_label:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set text and color for EXP (green)
|
||||||
|
exp_label.label = "+" + str(int(amount)) + " EXP"
|
||||||
|
exp_label.color = Color(0.4, 1.0, 0.4) # Bright green
|
||||||
|
exp_label.z_index = 5
|
||||||
|
|
||||||
|
# Direction is straight up
|
||||||
|
exp_label.direction = Vector2(0, -1)
|
||||||
|
|
||||||
|
# Position at player location
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if entities_node:
|
||||||
|
entities_node.add_child(exp_label)
|
||||||
|
exp_label.global_position = player.global_position + Vector2(0, -20) # Above player
|
||||||
|
else:
|
||||||
|
get_tree().current_scene.add_child(exp_label)
|
||||||
|
exp_label.global_position = player.global_position + Vector2(0, -20)
|
||||||
|
|
||||||
func world_to_screen(world_pos: Vector2) -> Vector2:
|
func world_to_screen(world_pos: Vector2) -> Vector2:
|
||||||
var vp = get_viewport()
|
var vp = get_viewport()
|
||||||
if not vp:
|
if not vp:
|
||||||
@@ -7844,6 +8230,46 @@ func _request_register_player_died(player_peer_id: int):
|
|||||||
_run_respawn_all_check()
|
_run_respawn_all_check()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
func _unregister_player_died(player: Node):
|
||||||
|
# Remove player from dead_players dictionary when they're revived
|
||||||
|
# Authority runs this; if client, we RPC server so server has full picture.
|
||||||
|
if not is_inside_tree():
|
||||||
|
return
|
||||||
|
var peer_id: int = 0
|
||||||
|
if player and player.has_method("get_multiplayer_authority"):
|
||||||
|
peer_id = player.get_multiplayer_authority()
|
||||||
|
var n = str(player.name) if player else ""
|
||||||
|
if n.is_empty():
|
||||||
|
return
|
||||||
|
if multiplayer.is_server() and multiplayer.has_multiplayer_peer():
|
||||||
|
if dead_players.has(n):
|
||||||
|
dead_players.erase(n)
|
||||||
|
LogManager.log("GameWorld: Unregistered player " + n + " from dead_players (revived)", LogManager.CATEGORY_DUNGEON)
|
||||||
|
elif multiplayer.has_multiplayer_peer():
|
||||||
|
_request_unregister_player_died.rpc_id(1, peer_id)
|
||||||
|
else:
|
||||||
|
# Single-player: we are "server"
|
||||||
|
if dead_players.has(n):
|
||||||
|
dead_players.erase(n)
|
||||||
|
LogManager.log("GameWorld: Unregistered player " + n + " from dead_players (revived)", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _request_unregister_player_died(player_peer_id: int):
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
var pm = get_node_or_null("PlayerManager")
|
||||||
|
if not pm or not pm.has_method("get_all_players"):
|
||||||
|
return
|
||||||
|
for p in pm.get_all_players():
|
||||||
|
if not is_instance_valid(p):
|
||||||
|
continue
|
||||||
|
if p.has_method("get_multiplayer_authority") and p.get_multiplayer_authority() == player_peer_id:
|
||||||
|
var n = str(p.name)
|
||||||
|
if not n.is_empty() and dead_players.has(n):
|
||||||
|
dead_players.erase(n)
|
||||||
|
LogManager.log("GameWorld: Unregistered player " + n + " from dead_players (revived)", LogManager.CATEGORY_DUNGEON)
|
||||||
|
return
|
||||||
|
|
||||||
func _are_all_players_dead_server() -> bool:
|
func _are_all_players_dead_server() -> bool:
|
||||||
var pm = get_node_or_null("PlayerManager")
|
var pm = get_node_or_null("PlayerManager")
|
||||||
if not pm or not pm.has_method("get_all_players"):
|
if not pm or not pm.has_method("get_all_players"):
|
||||||
@@ -7851,13 +8277,49 @@ func _are_all_players_dead_server() -> bool:
|
|||||||
var all_p = pm.get_all_players()
|
var all_p = pm.get_all_players()
|
||||||
if all_p.is_empty():
|
if all_p.is_empty():
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
# Get list of connected peer IDs (exclude disconnected players)
|
||||||
|
var connected_peer_ids: Array[int] = []
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
# Server's own peer ID (1) is always connected
|
||||||
|
connected_peer_ids.append(1)
|
||||||
|
# Add all other connected peers
|
||||||
|
for peer_id in multiplayer.get_peers():
|
||||||
|
connected_peer_ids.append(peer_id)
|
||||||
|
else:
|
||||||
|
# Single-player: only check local players
|
||||||
|
connected_peer_ids.append(1)
|
||||||
|
|
||||||
|
# Only check players that are connected (not disconnected)
|
||||||
|
var has_connected_players = false
|
||||||
for p in all_p:
|
for p in all_p:
|
||||||
if not is_instance_valid(p):
|
if not is_instance_valid(p):
|
||||||
return false
|
continue
|
||||||
|
|
||||||
|
# Get player's peer ID
|
||||||
|
var player_peer_id = 0
|
||||||
|
if p.has_method("get_multiplayer_authority"):
|
||||||
|
player_peer_id = p.get_multiplayer_authority()
|
||||||
|
elif "peer_id" in p:
|
||||||
|
player_peer_id = p.peer_id
|
||||||
|
|
||||||
|
# Skip disconnected players (not in connected_peer_ids)
|
||||||
|
if player_peer_id > 0 and player_peer_id not in connected_peer_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# This is a connected player - check if they're dead
|
||||||
|
has_connected_players = true
|
||||||
var in_dead = dead_players.has(str(p.name))
|
var in_dead = dead_players.has(str(p.name))
|
||||||
var node_dead = "is_dead" in p and p.is_dead
|
var node_dead = "is_dead" in p and p.is_dead
|
||||||
if not in_dead and not node_dead:
|
if not in_dead and not node_dead:
|
||||||
|
# Found a connected player who is not dead
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
# If no connected players found, return false (shouldn't happen, but safety check)
|
||||||
|
if not has_connected_players:
|
||||||
|
return false
|
||||||
|
|
||||||
|
# All connected players are dead
|
||||||
return true
|
return true
|
||||||
|
|
||||||
func _run_respawn_all_check():
|
func _run_respawn_all_check():
|
||||||
@@ -7870,6 +8332,14 @@ func _run_respawn_all_check():
|
|||||||
respawn_all_check_running = false
|
respawn_all_check_running = false
|
||||||
if not is_inside_tree():
|
if not is_inside_tree():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# CRITICAL: Double-check that all players are still dead before showing game over
|
||||||
|
# This prevents showing game over if level transition happened during the wait
|
||||||
|
if not _are_all_players_dead_server():
|
||||||
|
# Players are no longer dead (likely level transition happened), abort
|
||||||
|
dead_players.clear()
|
||||||
|
return
|
||||||
|
|
||||||
if game_over_triggered:
|
if game_over_triggered:
|
||||||
pass # Already shown (e.g. by earlier logic)
|
pass # Already shown (e.g. by earlier logic)
|
||||||
else:
|
else:
|
||||||
@@ -7880,6 +8350,18 @@ func _run_respawn_all_check():
|
|||||||
await get_tree().create_timer(0.5).timeout
|
await get_tree().create_timer(0.5).timeout
|
||||||
if not is_inside_tree():
|
if not is_inside_tree():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# CRITICAL: Check again after the wait - level transition might have happened
|
||||||
|
if not _are_all_players_dead_server():
|
||||||
|
# Players are no longer dead (level transition happened), abort and reset
|
||||||
|
game_over_triggered = false
|
||||||
|
dead_players.clear()
|
||||||
|
# Hide game over UI if it was shown
|
||||||
|
var game_over_ui = get_node_or_null("GameOverUI")
|
||||||
|
if game_over_ui:
|
||||||
|
game_over_ui.visible = false
|
||||||
|
return
|
||||||
|
|
||||||
dead_players.clear()
|
dead_players.clear()
|
||||||
respawn_all_ready.emit()
|
respawn_all_ready.emit()
|
||||||
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
||||||
@@ -7929,7 +8411,7 @@ func _show_game_over_local():
|
|||||||
# Center the VBoxContainer
|
# Center the VBoxContainer
|
||||||
var screen_size = get_viewport().get_visible_rect().size
|
var screen_size = get_viewport().get_visible_rect().size
|
||||||
vbox.set_anchors_preset(Control.PRESET_CENTER)
|
vbox.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
vbox.offset_left = -screen_size.x / 2
|
vbox.offset_left = - screen_size.x / 2
|
||||||
vbox.offset_right = screen_size.x / 2
|
vbox.offset_right = screen_size.x / 2
|
||||||
vbox.offset_top = -100
|
vbox.offset_top = -100
|
||||||
vbox.offset_bottom = screen_size.y / 2 - 100
|
vbox.offset_bottom = screen_size.y / 2 - 100
|
||||||
@@ -8051,6 +8533,120 @@ func _sync_arrow_collected(arrow_name: String):
|
|||||||
if arrow and is_instance_valid(arrow):
|
if arrow and is_instance_valid(arrow):
|
||||||
arrow.call_deferred("queue_free")
|
arrow.call_deferred("queue_free")
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func _sync_arrow_hit(arrow_name: String, target_name: String):
|
||||||
|
# Route arrow hit sync through game_world to avoid node path issues
|
||||||
|
# Find arrow by name (may be in Entities or as child of target)
|
||||||
|
if not is_inside_tree() or arrow_name.is_empty() or target_name.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var arrow = _find_arrow_by_name(arrow_name)
|
||||||
|
if not arrow or not is_instance_valid(arrow):
|
||||||
|
LogManager.log("GameWorld: Arrow not found for hit sync: " + arrow_name, LogManager.CATEGORY_DUNGEON)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find target by name in Entities node
|
||||||
|
var target = null
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if entities_node:
|
||||||
|
target = entities_node.get_node_or_null(target_name)
|
||||||
|
# Also check if target is a child of Entities (arrow might be stuck to it)
|
||||||
|
if not target:
|
||||||
|
for child in entities_node.get_children():
|
||||||
|
if child.name == target_name:
|
||||||
|
target = child
|
||||||
|
break
|
||||||
|
|
||||||
|
if not target:
|
||||||
|
LogManager.log("GameWorld: Arrow hit target not found: " + target_name, LogManager.CATEGORY_DUNGEON)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Call the arrow's internal sync method
|
||||||
|
if arrow.has_method("_process_arrow_hit_sync"):
|
||||||
|
arrow._process_arrow_hit_sync(target)
|
||||||
|
else:
|
||||||
|
# Fallback: directly stick to target
|
||||||
|
if arrow.has_method("_stick_to_target") and target not in arrow.hit_targets:
|
||||||
|
arrow.hit_targets[target] = true
|
||||||
|
if arrow.has_method("play_impact"):
|
||||||
|
arrow.play_impact()
|
||||||
|
arrow._stick_to_target(target)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func _sync_arrow_miss(arrow_name: String, target_name: String):
|
||||||
|
# Route arrow miss sync through game_world to avoid node path issues
|
||||||
|
if not is_inside_tree() or arrow_name.is_empty() or target_name.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var arrow = _find_arrow_by_name(arrow_name)
|
||||||
|
if not arrow or not is_instance_valid(arrow):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find target by name in Entities node
|
||||||
|
var target = null
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if entities_node:
|
||||||
|
target = entities_node.get_node_or_null(target_name)
|
||||||
|
|
||||||
|
if target and target not in arrow.hit_targets:
|
||||||
|
arrow.hit_targets[target] = true
|
||||||
|
LogManager.log("GameWorld: Arrow synced as MISS - continuing through: " + (target.name if target else "unknown"), LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func _sync_arrow_dodge(arrow_name: String, target_name: String):
|
||||||
|
# Route arrow dodge sync through game_world to avoid node path issues
|
||||||
|
if not is_inside_tree() or arrow_name.is_empty() or target_name.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var arrow = _find_arrow_by_name(arrow_name)
|
||||||
|
if not arrow or not is_instance_valid(arrow):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find target by name in Entities node
|
||||||
|
var target = null
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if entities_node:
|
||||||
|
target = entities_node.get_node_or_null(target_name)
|
||||||
|
|
||||||
|
if target and target not in arrow.hit_targets:
|
||||||
|
arrow.hit_targets[target] = true
|
||||||
|
LogManager.log("GameWorld: Arrow synced as DODGE - continuing through: " + (target.name if target else "unknown"), LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
|
func _find_arrow_by_name(arrow_name: String) -> Node:
|
||||||
|
# Find arrow by name - check Entities first, then check children of entities (arrows stuck to enemies)
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if not entities_node:
|
||||||
|
return null
|
||||||
|
|
||||||
|
# First check direct children of Entities
|
||||||
|
var arrow = entities_node.get_node_or_null(arrow_name)
|
||||||
|
if arrow and is_instance_valid(arrow):
|
||||||
|
return arrow
|
||||||
|
|
||||||
|
# Recursively search children of Entities' children (arrows stuck to enemies/players)
|
||||||
|
# Arrows can be nested deeper when stuck to targets
|
||||||
|
for child in entities_node.get_children():
|
||||||
|
var found = _find_node_recursive(child, arrow_name)
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
func _find_node_recursive(node: Node, target_name: String) -> Node:
|
||||||
|
# Recursively search for a node by name
|
||||||
|
if not is_instance_valid(node):
|
||||||
|
return null
|
||||||
|
|
||||||
|
if node.name == target_name:
|
||||||
|
return node
|
||||||
|
|
||||||
|
for child in node.get_children():
|
||||||
|
var found = _find_node_recursive(child, target_name)
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
func _create_level_complete_ui_programmatically() -> Node:
|
func _create_level_complete_ui_programmatically() -> Node:
|
||||||
# Create level complete UI programmatically
|
# Create level complete UI programmatically
|
||||||
var canvas_layer = CanvasLayer.new()
|
var canvas_layer = CanvasLayer.new()
|
||||||
@@ -8170,7 +8766,7 @@ func _create_level_text_ui_programmatically() -> Node:
|
|||||||
|
|
||||||
# Center horizontally and position higher up
|
# Center horizontally and position higher up
|
||||||
var screen_size = get_viewport().get_visible_rect().size
|
var screen_size = get_viewport().get_visible_rect().size
|
||||||
vbox.offset_left = -screen_size.x / 2
|
vbox.offset_left = - screen_size.x / 2
|
||||||
vbox.offset_right = screen_size.x / 2
|
vbox.offset_right = screen_size.x / 2
|
||||||
vbox.offset_top = -250 # Position higher up from center
|
vbox.offset_top = -250 # Position higher up from center
|
||||||
vbox.offset_bottom = screen_size.y / 2 - 250
|
vbox.offset_bottom = screen_size.y / 2 - 250
|
||||||
|
|||||||
@@ -233,12 +233,12 @@ func _handle_air_collision():
|
|||||||
|
|
||||||
_break_into_pieces()
|
_break_into_pieces()
|
||||||
|
|
||||||
# Damage and knockback player using RPC (pots deal less damage than boxes)
|
# Damage and knockback player using RPC (any thrown object deals max 1 damage to other players)
|
||||||
# Player's take_damage() already handles defense calculation
|
# Player's take_damage() already handles defense calculation
|
||||||
# Pass the thrower's position for accurate direction
|
# Pass the thrower's position for accurate direction
|
||||||
if collider.has_method("rpc_take_damage"):
|
if collider.has_method("rpc_take_damage"):
|
||||||
var attacker_pos = thrown_by_player.global_position if thrown_by_player and is_instance_valid(thrown_by_player) else global_position
|
var attacker_pos = thrown_by_player.global_position if thrown_by_player and is_instance_valid(thrown_by_player) else global_position
|
||||||
var base_damage = 7.0 if object_type == "Pot" else 10.0
|
var base_damage = 1.0 # Max 1 damage to other players for any thrown object
|
||||||
var player_peer_id = collider.get_multiplayer_authority()
|
var player_peer_id = collider.get_multiplayer_authority()
|
||||||
if player_peer_id != 0:
|
if player_peer_id != 0:
|
||||||
# If target peer is the same as server (us), call directly
|
# If target peer is the same as server (us), call directly
|
||||||
@@ -568,9 +568,18 @@ func _convert_to_bomb_projectile(by_player, force: Vector2):
|
|||||||
if bomb.has_node("Sprite2D"):
|
if bomb.has_node("Sprite2D"):
|
||||||
bomb.get_node("Sprite2D").visible = true
|
bomb.get_node("Sprite2D").visible = true
|
||||||
|
|
||||||
# Sync bomb throw to other clients (pass our name so they can free the lifted bomb)
|
# Sync bomb throw to other clients
|
||||||
if multiplayer.has_multiplayer_peer() and by_player.is_multiplayer_authority() and by_player.has_method("_rpc_to_ready_peers") and by_player.is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and by_player.is_multiplayer_authority():
|
||||||
|
if by_player.has_method("_rpc_to_ready_peers") and by_player.is_inside_tree():
|
||||||
|
# Player threw: sync via player RPC (pass our name so they can free the lifted bomb)
|
||||||
by_player._rpc_to_ready_peers("_sync_throw_bomb", [name, current_pos, force])
|
by_player._rpc_to_ready_peers("_sync_throw_bomb", [name, current_pos, force])
|
||||||
|
elif by_player.is_in_group("enemy"):
|
||||||
|
# Enemy threw: sync via game_world so joiners see the bomb
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_rpc_to_ready_peers"):
|
||||||
|
var enemy_index = by_player.get_meta("enemy_index") if by_player.has_meta("enemy_index") else -1
|
||||||
|
var bomb_name = "EnemyBomb_" + by_player.name + "_" + str(Time.get_ticks_msec())
|
||||||
|
game_world._rpc_to_ready_peers("_sync_enemy_throw_bomb", [bomb_name, by_player.name, enemy_index, current_pos, force])
|
||||||
|
|
||||||
# Remove the interactable object
|
# Remove the interactable object
|
||||||
queue_free()
|
queue_free()
|
||||||
|
|||||||
@@ -207,10 +207,18 @@ func _find_local_player():
|
|||||||
if local_players.size() > 0:
|
if local_players.size() > 0:
|
||||||
local_player = local_players[0]
|
local_player = local_players[0]
|
||||||
if local_player and local_player.character_stats:
|
if local_player and local_player.character_stats:
|
||||||
# Connect to character_changed signal
|
# Connect to character_changed signal (for inventory/equipment changes)
|
||||||
if local_player.character_stats.character_changed.is_connected(_on_character_changed):
|
if local_player.character_stats.character_changed.is_connected(_on_character_changed):
|
||||||
local_player.character_stats.character_changed.disconnect(_on_character_changed)
|
local_player.character_stats.character_changed.disconnect(_on_character_changed)
|
||||||
local_player.character_stats.character_changed.connect(_on_character_changed)
|
local_player.character_stats.character_changed.connect(_on_character_changed)
|
||||||
|
# Connect to mana_changed signal (for mana updates only - don't rebuild UI)
|
||||||
|
if local_player.character_stats.mana_changed.is_connected(_on_mana_changed):
|
||||||
|
local_player.character_stats.mana_changed.disconnect(_on_mana_changed)
|
||||||
|
local_player.character_stats.mana_changed.connect(_on_mana_changed)
|
||||||
|
# Connect to health_changed signal (for HP updates only - don't rebuild UI)
|
||||||
|
if local_player.character_stats.health_changed.is_connected(_on_health_changed):
|
||||||
|
local_player.character_stats.health_changed.disconnect(_on_health_changed)
|
||||||
|
local_player.character_stats.health_changed.connect(_on_health_changed)
|
||||||
# Initial update
|
# Initial update
|
||||||
_update_ui()
|
_update_ui()
|
||||||
_update_stats()
|
_update_stats()
|
||||||
@@ -1396,6 +1404,20 @@ func _on_inventory_item_gui_input(event: InputEvent, item: Item):
|
|||||||
# Use the same logic as E key to drop
|
# Use the same logic as E key to drop
|
||||||
_handle_e_key()
|
_handle_e_key()
|
||||||
|
|
||||||
|
func _on_mana_changed(new_mana: float, max_mana: float):
|
||||||
|
# Update only MP bar and label when mana changes (don't rebuild entire UI)
|
||||||
|
if mp_progress_bar and mp_value_label:
|
||||||
|
mp_progress_bar.max_value = max(1.0, max_mana)
|
||||||
|
mp_progress_bar.value = new_mana
|
||||||
|
mp_value_label.text = str(int(new_mana)) + "/" + str(int(max_mana))
|
||||||
|
|
||||||
|
func _on_health_changed(new_hp: float, max_hp: float):
|
||||||
|
# Update only HP bar and label when health changes (don't rebuild entire UI)
|
||||||
|
if hp_progress_bar and hp_value_label:
|
||||||
|
hp_progress_bar.max_value = max(1.0, max_hp)
|
||||||
|
hp_progress_bar.value = new_hp
|
||||||
|
hp_value_label.text = str(int(new_hp)) + "/" + str(int(max_hp))
|
||||||
|
|
||||||
func _on_character_changed(_char: CharacterStats):
|
func _on_character_changed(_char: CharacterStats):
|
||||||
# Always update stats when character changes (even if inventory is closed)
|
# Always update stats when character changes (even if inventory is closed)
|
||||||
# Equipment changes affect max HP/MP which should be reflected everywhere
|
# Equipment changes affect max HP/MP which should be reflected everywhere
|
||||||
|
|||||||
@@ -165,17 +165,17 @@ static func _load_all_items():
|
|||||||
|
|
||||||
# HEADGEAR items
|
# HEADGEAR items
|
||||||
# MageHatRed (frame 9) variants
|
# MageHatRed (frame 9) variants
|
||||||
var _mhr_o = [Color(255/255.0,39/255.0,44/255.0), Color(182/255.0,0,0), Color(118/255.0,1/255.0,0), Color(72/255.0,0,12/255.0)]
|
var _mhr_o = [Color(255 / 255.0, 39 / 255.0, 44 / 255.0), Color(182 / 255.0, 0, 0), Color(118 / 255.0, 1 / 255.0, 0), Color(72 / 255.0, 0, 12 / 255.0)]
|
||||||
var _mhr_brown = [Color(139/255.0,90/255.0,43/255.0), Color(101/255.0,67/255.0,33/255.0), Color(80/255.0,50/255.0,20/255.0), Color(50/255.0,30/255.0,10/255.0)]
|
var _mhr_brown = [Color(139 / 255.0, 90 / 255.0, 43 / 255.0), Color(101 / 255.0, 67 / 255.0, 33 / 255.0), Color(80 / 255.0, 50 / 255.0, 20 / 255.0), Color(50 / 255.0, 30 / 255.0, 10 / 255.0)]
|
||||||
var _mhr_blue = [Color(30/255.0,80/255.0,180/255.0), Color(20/255.0,50/255.0,120/255.0), Color(10/255.0,30/255.0,80/255.0), Color(5/255.0,15/255.0,50/255.0)]
|
var _mhr_blue = [Color(30 / 255.0, 80 / 255.0, 180 / 255.0), Color(20 / 255.0, 50 / 255.0, 120 / 255.0), Color(10 / 255.0, 30 / 255.0, 80 / 255.0), Color(5 / 255.0, 15 / 255.0, 50 / 255.0)]
|
||||||
var _mhr_lightblue = [Color(170/255.0,220/255.0,1.0), Color(130/255.0,190/255.0,1.0), Color(90/255.0,150/255.0,220/255.0), Color(50/255.0,100/255.0,180/255.0)]
|
var _mhr_lightblue = [Color(170 / 255.0, 220 / 255.0, 1.0), Color(130 / 255.0, 190 / 255.0, 1.0), Color(90 / 255.0, 150 / 255.0, 220 / 255.0), Color(50 / 255.0, 100 / 255.0, 180 / 255.0)]
|
||||||
var _mhr_white = [Color(250/255.0,250/255.0,250/255.0), Color(220/255.0,220/255.0,220/255.0), Color(190/255.0,190/255.0,190/255.0), Color(150/255.0,150/255.0,150/255.0)]
|
var _mhr_white = [Color(250 / 255.0, 250 / 255.0, 250 / 255.0), Color(220 / 255.0, 220 / 255.0, 220 / 255.0), Color(190 / 255.0, 190 / 255.0, 190 / 255.0), Color(150 / 255.0, 150 / 255.0, 150 / 255.0)]
|
||||||
var _shc_o = [Color(0,180/255.0,157/255.0), Color(0,121/255.0,102/255.0), Color(0,79/255.0,67/255.0), Color(0,46/255.0,93/255.0)]
|
var _shc_o = [Color(0, 180 / 255.0, 157 / 255.0), Color(0, 121 / 255.0, 102 / 255.0), Color(0, 79 / 255.0, 67 / 255.0), Color(0, 46 / 255.0, 93 / 255.0)]
|
||||||
var _shc_red = [Color(180/255.0,40/255.0,40/255.0), Color(130/255.0,0,0), Color(90/255.0,0,0), Color(60/255.0,0,0)]
|
var _shc_red = [Color(180 / 255.0, 40 / 255.0, 40 / 255.0), Color(130 / 255.0, 0, 0), Color(90 / 255.0, 0, 0), Color(60 / 255.0, 0, 0)]
|
||||||
var _shc_green = [Color(40/255.0,160/255.0,40/255.0), Color(0,120/255.0,0), Color(0,80/255.0,0), Color(0,50/255.0,0)]
|
var _shc_green = [Color(40 / 255.0, 160 / 255.0, 40 / 255.0), Color(0, 120 / 255.0, 0), Color(0, 80 / 255.0, 0), Color(0, 50 / 255.0, 0)]
|
||||||
var _sb_o = [Color(248/255.0,219/255.0,108/255.0), Color(225/255.0,159/255.0,57/255.0), Color(199/255.0,115/255.0,29/255.0), Color(151/255.0,73/255.0,9/255.0), Color(108/255.0,43/255.0,0), Color(58/255.0,23/255.0,11/255.0), Color(0,76/255.0,218/255.0), Color(9/255.0,35/255.0,105/255.0), Color(0,14/255.0,62/255.0)]
|
var _sb_o = [Color(248 / 255.0, 219 / 255.0, 108 / 255.0), Color(225 / 255.0, 159 / 255.0, 57 / 255.0), Color(199 / 255.0, 115 / 255.0, 29 / 255.0), Color(151 / 255.0, 73 / 255.0, 9 / 255.0), Color(108 / 255.0, 43 / 255.0, 0), Color(58 / 255.0, 23 / 255.0, 11 / 255.0), Color(0, 76 / 255.0, 218 / 255.0), Color(9 / 255.0, 35 / 255.0, 105 / 255.0), Color(0, 14 / 255.0, 62 / 255.0)]
|
||||||
var _sb_iron = [Color(190/255.0,187/255.0,181/255.0), Color(162/255.0,158/255.0,150/255.0), Color(125/255.0,123/255.0,118/255.0), Color(77/255.0,76/255.0,75/255.0), Color(54/255.0,54/255.0,54/255.0), Color(30/255.0,30/255.0,30/255.0), Color(0,76/255.0,218/255.0), Color(9/255.0,35/255.0,105/255.0), Color(0,14/255.0,62/255.0)]
|
var _sb_iron = [Color(190 / 255.0, 187 / 255.0, 181 / 255.0), Color(162 / 255.0, 158 / 255.0, 150 / 255.0), Color(125 / 255.0, 123 / 255.0, 118 / 255.0), Color(77 / 255.0, 76 / 255.0, 75 / 255.0), Color(54 / 255.0, 54 / 255.0, 54 / 255.0), Color(30 / 255.0, 30 / 255.0, 30 / 255.0), Color(0, 76 / 255.0, 218 / 255.0), Color(9 / 255.0, 35 / 255.0, 105 / 255.0), Color(0, 14 / 255.0, 62 / 255.0)]
|
||||||
var _sb_steel = [Color(227/255.0,227/255.0,227/255.0), Color(183/255.0,183/255.0,183/255.0), Color(116/255.0,116/255.0,116/255.0), Color(77/255.0,76/255.0,75/255.0), Color(54/255.0,54/255.0,54/255.0), Color(30/255.0,30/255.0,30/255.0), Color(0,76/255.0,218/255.0), Color(9/255.0,35/255.0,105/255.0), Color(0,14/255.0,62/255.0)]
|
var _sb_steel = [Color(227 / 255.0, 227 / 255.0, 227 / 255.0), Color(183 / 255.0, 183 / 255.0, 183 / 255.0), Color(116 / 255.0, 116 / 255.0, 116 / 255.0), Color(77 / 255.0, 76 / 255.0, 75 / 255.0), Color(54 / 255.0, 54 / 255.0, 54 / 255.0), Color(30 / 255.0, 30 / 255.0, 30 / 255.0), Color(0, 76 / 255.0, 218 / 255.0), Color(9 / 255.0, 35 / 255.0, 105 / 255.0), Color(0, 14 / 255.0, 62 / 255.0)]
|
||||||
|
|
||||||
_register_item("hat", {
|
_register_item("hat", {
|
||||||
"item_name": "Hat",
|
"item_name": "Hat",
|
||||||
@@ -189,7 +189,7 @@ static func _load_all_items():
|
|||||||
"buy_cost": 20,
|
"buy_cost": 20,
|
||||||
"sell_worth": 6,
|
"sell_worth": 6,
|
||||||
"rarity": ItemRarity.COMMON,
|
"rarity": ItemRarity.COMMON,
|
||||||
"colorReplacements": [{"original": _mhr_o[0], "replace": _mhr_brown[0]}, {"original": _mhr_o[1], "replace": _mhr_brown[1]}, {"original": _mhr_o[2], "replace": _mhr_brown[2]}, {"original": _mhr_o[3], "replace": _mhr_brown[3]}]
|
"colorReplacements": [ {"original": _mhr_o[0], "replace": _mhr_brown[0]}, {"original": _mhr_o[1], "replace": _mhr_brown[1]}, {"original": _mhr_o[2], "replace": _mhr_brown[2]}, {"original": _mhr_o[3], "replace": _mhr_brown[3]}]
|
||||||
})
|
})
|
||||||
_register_item("red_hat", {
|
_register_item("red_hat", {
|
||||||
"item_name": "Red hat",
|
"item_name": "Red hat",
|
||||||
@@ -216,7 +216,7 @@ static func _load_all_items():
|
|||||||
"buy_cost": 28,
|
"buy_cost": 28,
|
||||||
"sell_worth": 9,
|
"sell_worth": 9,
|
||||||
"rarity": ItemRarity.COMMON,
|
"rarity": ItemRarity.COMMON,
|
||||||
"colorReplacements": [{"original": _mhr_o[0], "replace": _mhr_blue[0]}, {"original": _mhr_o[1], "replace": _mhr_blue[1]}, {"original": _mhr_o[2], "replace": _mhr_blue[2]}, {"original": _mhr_o[3], "replace": _mhr_blue[3]}]
|
"colorReplacements": [ {"original": _mhr_o[0], "replace": _mhr_blue[0]}, {"original": _mhr_o[1], "replace": _mhr_blue[1]}, {"original": _mhr_o[2], "replace": _mhr_blue[2]}, {"original": _mhr_o[3], "replace": _mhr_blue[3]}]
|
||||||
})
|
})
|
||||||
_register_item("wizards_hat", {
|
_register_item("wizards_hat", {
|
||||||
"item_name": "Wizard's hat",
|
"item_name": "Wizard's hat",
|
||||||
@@ -230,7 +230,7 @@ static func _load_all_items():
|
|||||||
"buy_cost": 55,
|
"buy_cost": 55,
|
||||||
"sell_worth": 18,
|
"sell_worth": 18,
|
||||||
"rarity": ItemRarity.UNCOMMON,
|
"rarity": ItemRarity.UNCOMMON,
|
||||||
"colorReplacements": [{"original": _mhr_o[0], "replace": _mhr_lightblue[0]}, {"original": _mhr_o[1], "replace": _mhr_lightblue[1]}, {"original": _mhr_o[2], "replace": _mhr_lightblue[2]}, {"original": _mhr_o[3], "replace": _mhr_lightblue[3]}]
|
"colorReplacements": [ {"original": _mhr_o[0], "replace": _mhr_lightblue[0]}, {"original": _mhr_o[1], "replace": _mhr_lightblue[1]}, {"original": _mhr_o[2], "replace": _mhr_lightblue[2]}, {"original": _mhr_o[3], "replace": _mhr_lightblue[3]}]
|
||||||
})
|
})
|
||||||
_register_item("gandolfs_hat", {
|
_register_item("gandolfs_hat", {
|
||||||
"item_name": "Gandolf's Hat",
|
"item_name": "Gandolf's Hat",
|
||||||
@@ -244,7 +244,7 @@ static func _load_all_items():
|
|||||||
"buy_cost": 60,
|
"buy_cost": 60,
|
||||||
"sell_worth": 20,
|
"sell_worth": 20,
|
||||||
"rarity": ItemRarity.UNCOMMON,
|
"rarity": ItemRarity.UNCOMMON,
|
||||||
"colorReplacements": [{"original": _mhr_o[0], "replace": _mhr_white[0]}, {"original": _mhr_o[1], "replace": _mhr_white[1]}, {"original": _mhr_o[2], "replace": _mhr_white[2]}, {"original": _mhr_o[3], "replace": _mhr_white[3]}]
|
"colorReplacements": [ {"original": _mhr_o[0], "replace": _mhr_white[0]}, {"original": _mhr_o[1], "replace": _mhr_white[1]}, {"original": _mhr_o[2], "replace": _mhr_white[2]}, {"original": _mhr_o[3], "replace": _mhr_white[3]}]
|
||||||
})
|
})
|
||||||
|
|
||||||
_register_item("sorcerors_hood", {
|
_register_item("sorcerors_hood", {
|
||||||
@@ -272,7 +272,7 @@ static func _load_all_items():
|
|||||||
"buy_cost": 45,
|
"buy_cost": 45,
|
||||||
"sell_worth": 14,
|
"sell_worth": 14,
|
||||||
"rarity": ItemRarity.COMMON,
|
"rarity": ItemRarity.COMMON,
|
||||||
"colorReplacements": [{"original": _shc_o[0], "replace": _shc_red[0]}, {"original": _shc_o[1], "replace": _shc_red[1]}, {"original": _shc_o[2], "replace": _shc_red[2]}, {"original": _shc_o[3], "replace": _shc_red[3]}]
|
"colorReplacements": [ {"original": _shc_o[0], "replace": _shc_red[0]}, {"original": _shc_o[1], "replace": _shc_red[1]}, {"original": _shc_o[2], "replace": _shc_red[2]}, {"original": _shc_o[3], "replace": _shc_red[3]}]
|
||||||
})
|
})
|
||||||
_register_item("green_hood", {
|
_register_item("green_hood", {
|
||||||
"item_name": "Green Hood",
|
"item_name": "Green Hood",
|
||||||
@@ -286,7 +286,7 @@ static func _load_all_items():
|
|||||||
"buy_cost": 52,
|
"buy_cost": 52,
|
||||||
"sell_worth": 17,
|
"sell_worth": 17,
|
||||||
"rarity": ItemRarity.COMMON,
|
"rarity": ItemRarity.COMMON,
|
||||||
"colorReplacements": [{"original": _shc_o[0], "replace": _shc_green[0]}, {"original": _shc_o[1], "replace": _shc_green[1]}, {"original": _shc_o[2], "replace": _shc_green[2]}, {"original": _shc_o[3], "replace": _shc_green[3]}]
|
"colorReplacements": [ {"original": _shc_o[0], "replace": _shc_green[0]}, {"original": _shc_o[1], "replace": _shc_green[1]}, {"original": _shc_o[2], "replace": _shc_green[2]}, {"original": _shc_o[3], "replace": _shc_green[3]}]
|
||||||
})
|
})
|
||||||
|
|
||||||
_register_item("high_mage_hat", {
|
_register_item("high_mage_hat", {
|
||||||
@@ -723,7 +723,7 @@ static func _load_all_items():
|
|||||||
"buy_cost": 110,
|
"buy_cost": 110,
|
||||||
"sell_worth": 34,
|
"sell_worth": 34,
|
||||||
"rarity": ItemRarity.UNCOMMON,
|
"rarity": ItemRarity.UNCOMMON,
|
||||||
"colorReplacements": [{"original": _sb_o[0], "replace": _sb_iron[0]}, {"original": _sb_o[1], "replace": _sb_iron[1]}, {"original": _sb_o[2], "replace": _sb_iron[2]}, {"original": _sb_o[3], "replace": _sb_iron[3]}, {"original": _sb_o[4], "replace": _sb_iron[4]}, {"original": _sb_o[5], "replace": _sb_iron[5]}, {"original": _sb_o[6], "replace": _sb_iron[6]}, {"original": _sb_o[7], "replace": _sb_iron[7]}, {"original": _sb_o[8], "replace": _sb_iron[8]}]
|
"colorReplacements": [ {"original": _sb_o[0], "replace": _sb_iron[0]}, {"original": _sb_o[1], "replace": _sb_iron[1]}, {"original": _sb_o[2], "replace": _sb_iron[2]}, {"original": _sb_o[3], "replace": _sb_iron[3]}, {"original": _sb_o[4], "replace": _sb_iron[4]}, {"original": _sb_o[5], "replace": _sb_iron[5]}, {"original": _sb_o[6], "replace": _sb_iron[6]}, {"original": _sb_o[7], "replace": _sb_iron[7]}, {"original": _sb_o[8], "replace": _sb_iron[8]}]
|
||||||
})
|
})
|
||||||
_register_item("soldier_steel_helm", {
|
_register_item("soldier_steel_helm", {
|
||||||
"item_name": "Soldier Steel Helm",
|
"item_name": "Soldier Steel Helm",
|
||||||
@@ -737,7 +737,7 @@ static func _load_all_items():
|
|||||||
"buy_cost": 125,
|
"buy_cost": 125,
|
||||||
"sell_worth": 38,
|
"sell_worth": 38,
|
||||||
"rarity": ItemRarity.UNCOMMON,
|
"rarity": ItemRarity.UNCOMMON,
|
||||||
"colorReplacements": [{"original": _sb_o[0], "replace": _sb_steel[0]}, {"original": _sb_o[1], "replace": _sb_steel[1]}, {"original": _sb_o[2], "replace": _sb_steel[2]}, {"original": _sb_o[3], "replace": _sb_steel[3]}, {"original": _sb_o[4], "replace": _sb_steel[4]}, {"original": _sb_o[5], "replace": _sb_steel[5]}, {"original": _sb_o[6], "replace": _sb_steel[6]}, {"original": _sb_o[7], "replace": _sb_steel[7]}, {"original": _sb_o[8], "replace": _sb_steel[8]}]
|
"colorReplacements": [ {"original": _sb_o[0], "replace": _sb_steel[0]}, {"original": _sb_o[1], "replace": _sb_steel[1]}, {"original": _sb_o[2], "replace": _sb_steel[2]}, {"original": _sb_o[3], "replace": _sb_steel[3]}, {"original": _sb_o[4], "replace": _sb_steel[4]}, {"original": _sb_o[5], "replace": _sb_steel[5]}, {"original": _sb_o[6], "replace": _sb_steel[6]}, {"original": _sb_o[7], "replace": _sb_steel[7]}, {"original": _sb_o[8], "replace": _sb_steel[8]}]
|
||||||
})
|
})
|
||||||
|
|
||||||
_register_item("assassin_bandana", {
|
_register_item("assassin_bandana", {
|
||||||
@@ -1664,6 +1664,45 @@ static func _load_all_items():
|
|||||||
"rarity": ItemRarity.COMMON
|
"rarity": ItemRarity.COMMON
|
||||||
})
|
})
|
||||||
|
|
||||||
|
_register_item("apple", {
|
||||||
|
"item_name": "Apple",
|
||||||
|
"description": "Restores 20 HP",
|
||||||
|
"item_type": Item.ItemType.Restoration,
|
||||||
|
"equipment_type": Item.EquipmentType.NONE,
|
||||||
|
"weapon_type": Item.WeaponType.NONE,
|
||||||
|
"spriteFrame": 8 * 20 + 10, # 10,8
|
||||||
|
"modifiers": {"hp": 20},
|
||||||
|
"buy_cost": 15,
|
||||||
|
"sell_worth": 5,
|
||||||
|
"rarity": ItemRarity.COMMON
|
||||||
|
})
|
||||||
|
|
||||||
|
_register_item("banana", {
|
||||||
|
"item_name": "Banana",
|
||||||
|
"description": "Restores 20 HP",
|
||||||
|
"item_type": Item.ItemType.Restoration,
|
||||||
|
"equipment_type": Item.EquipmentType.NONE,
|
||||||
|
"weapon_type": Item.WeaponType.NONE,
|
||||||
|
"spriteFrame": 8 * 20 + 11, # 11,8
|
||||||
|
"modifiers": {"hp": 20},
|
||||||
|
"buy_cost": 15,
|
||||||
|
"sell_worth": 5,
|
||||||
|
"rarity": ItemRarity.COMMON
|
||||||
|
})
|
||||||
|
|
||||||
|
_register_item("cherry", {
|
||||||
|
"item_name": "Cherry",
|
||||||
|
"description": "Restores 20 HP",
|
||||||
|
"item_type": Item.ItemType.Restoration,
|
||||||
|
"equipment_type": Item.EquipmentType.NONE,
|
||||||
|
"weapon_type": Item.WeaponType.NONE,
|
||||||
|
"spriteFrame": 8 * 20 + 12, # 12,8
|
||||||
|
"modifiers": {"hp": 20},
|
||||||
|
"buy_cost": 15,
|
||||||
|
"sell_worth": 5,
|
||||||
|
"rarity": ItemRarity.COMMON
|
||||||
|
})
|
||||||
|
|
||||||
_register_item("healing_potion", {
|
_register_item("healing_potion", {
|
||||||
"item_name": "Healing Potion",
|
"item_name": "Healing Potion",
|
||||||
"description": "Restores 50 HP",
|
"description": "Restores 50 HP",
|
||||||
@@ -1739,11 +1778,11 @@ static func _load_all_items():
|
|||||||
# SPELLBOOKS (row 11, columns 13-14)
|
# SPELLBOOKS (row 11, columns 13-14)
|
||||||
# Sprite 233 = 11 * 20 + 13 — same base as Tome of Healing, blue colorReplacements
|
# Sprite 233 = 11 * 20 + 13 — same base as Tome of Healing, blue colorReplacements
|
||||||
var _tf_o = [
|
var _tf_o = [
|
||||||
Color(225.0/255.0, 130.0/255.0, 137.0/255.0),
|
Color(225.0 / 255.0, 130.0 / 255.0, 137.0 / 255.0),
|
||||||
Color(174.0/255.0, 108.0/255.0, 55.0/255.0),
|
Color(174.0 / 255.0, 108.0 / 255.0, 55.0 / 255.0),
|
||||||
Color(245.0/255.0, 183.0/255.0, 132.0/255.0),
|
Color(245.0 / 255.0, 183.0 / 255.0, 132.0 / 255.0),
|
||||||
Color(130.0/255.0, 60.0/255.0, 61.0/255.0),
|
Color(130.0 / 255.0, 60.0 / 255.0, 61.0 / 255.0),
|
||||||
Color(197.0/255.0, 151.0/255.0, 130.0/255.0)
|
Color(197.0 / 255.0, 151.0 / 255.0, 130.0 / 255.0)
|
||||||
]
|
]
|
||||||
var _tf_blue = [
|
var _tf_blue = [
|
||||||
Color(0.35, 0.6, 0.95),
|
Color(0.35, 0.6, 0.95),
|
||||||
@@ -1793,11 +1832,11 @@ static func _load_all_items():
|
|||||||
|
|
||||||
# Tome of Healing - frame 233 (11*20+13), green colorReplacements
|
# Tome of Healing - frame 233 (11*20+13), green colorReplacements
|
||||||
var _th_o = [
|
var _th_o = [
|
||||||
Color(225.0/255.0, 130.0/255.0, 137.0/255.0),
|
Color(225.0 / 255.0, 130.0 / 255.0, 137.0 / 255.0),
|
||||||
Color(174.0/255.0, 108.0/255.0, 55.0/255.0),
|
Color(174.0 / 255.0, 108.0 / 255.0, 55.0 / 255.0),
|
||||||
Color(245.0/255.0, 183.0/255.0, 132.0/255.0),
|
Color(245.0 / 255.0, 183.0 / 255.0, 132.0 / 255.0),
|
||||||
Color(130.0/255.0, 60.0/255.0, 61.0/255.0),
|
Color(130.0 / 255.0, 60.0 / 255.0, 61.0 / 255.0),
|
||||||
Color(197.0/255.0, 151.0/255.0, 130.0/255.0)
|
Color(197.0 / 255.0, 151.0 / 255.0, 130.0 / 255.0)
|
||||||
]
|
]
|
||||||
var _th_green = [
|
var _th_green = [
|
||||||
Color(0.35, 0.85, 0.4),
|
Color(0.35, 0.85, 0.4),
|
||||||
@@ -1945,6 +1984,106 @@ static func get_random_item() -> Item:
|
|||||||
|
|
||||||
return get_random_item_by_rarity(rarity)
|
return get_random_item_by_rarity(rarity)
|
||||||
|
|
||||||
|
# Get random consumable items (arrows, bombs, restoration items) for enemy drops
|
||||||
|
static func get_random_consumable_drop() -> Item:
|
||||||
|
_initialize()
|
||||||
|
|
||||||
|
# Prioritize consumables: 70% CONSUMABLE rarity, 30% Restoration items
|
||||||
|
var roll = randf()
|
||||||
|
|
||||||
|
if roll < 0.7:
|
||||||
|
# CONSUMABLE rarity items (arrows, bombs, etc.)
|
||||||
|
return get_random_item_by_rarity(ItemRarity.CONSUMABLE)
|
||||||
|
else:
|
||||||
|
# Restoration items (food) - get from COMMON rarity with Restoration type
|
||||||
|
var candidates = []
|
||||||
|
var weights = []
|
||||||
|
var total_weight = 0.0
|
||||||
|
|
||||||
|
for item_id in item_definitions.keys():
|
||||||
|
var item_data = item_definitions[item_id]
|
||||||
|
if item_data.has("rarity") and item_data["rarity"] == ItemRarity.COMMON:
|
||||||
|
if item_data.has("item_type") and item_data["item_type"] == Item.ItemType.Restoration:
|
||||||
|
candidates.append(item_id)
|
||||||
|
var drop_chance = item_data.get("drop_chance", 1.0)
|
||||||
|
weights.append(drop_chance)
|
||||||
|
total_weight += drop_chance
|
||||||
|
|
||||||
|
if candidates.is_empty():
|
||||||
|
# Fallback to CONSUMABLE rarity
|
||||||
|
return get_random_item_by_rarity(ItemRarity.CONSUMABLE)
|
||||||
|
|
||||||
|
# Weighted random selection
|
||||||
|
var weight_roll = randf() * total_weight
|
||||||
|
var cumulative = 0.0
|
||||||
|
for i in range(candidates.size()):
|
||||||
|
cumulative += weights[i]
|
||||||
|
if weight_roll <= cumulative:
|
||||||
|
return create_item(candidates[i])
|
||||||
|
|
||||||
|
# Fallback
|
||||||
|
return create_item(candidates[candidates.size() - 1])
|
||||||
|
|
||||||
|
# Get random equipment items (much rarer than consumables)
|
||||||
|
static func get_random_equipment_drop() -> Item:
|
||||||
|
_initialize()
|
||||||
|
|
||||||
|
# Equipment: 70% common, 25% uncommon, 4% rare, 1% epic
|
||||||
|
var roll = randf()
|
||||||
|
var rarity: ItemRarity
|
||||||
|
|
||||||
|
if roll < 0.7:
|
||||||
|
rarity = ItemRarity.COMMON
|
||||||
|
elif roll < 0.95:
|
||||||
|
rarity = ItemRarity.UNCOMMON
|
||||||
|
elif roll < 0.99:
|
||||||
|
rarity = ItemRarity.RARE
|
||||||
|
else:
|
||||||
|
rarity = ItemRarity.EPIC
|
||||||
|
|
||||||
|
# Filter to only Equippable items of the selected rarity
|
||||||
|
var candidates = []
|
||||||
|
var weights = []
|
||||||
|
var total_weight = 0.0
|
||||||
|
|
||||||
|
for item_id in item_definitions.keys():
|
||||||
|
var item_data = item_definitions[item_id]
|
||||||
|
if item_data.has("rarity") and item_data["rarity"] == rarity:
|
||||||
|
# Only include Equippable items (not consumables or restoration)
|
||||||
|
if item_data.has("item_type") and item_data["item_type"] == Item.ItemType.Equippable:
|
||||||
|
candidates.append(item_id)
|
||||||
|
var drop_chance = item_data.get("drop_chance", 1.0)
|
||||||
|
weights.append(drop_chance)
|
||||||
|
total_weight += drop_chance
|
||||||
|
|
||||||
|
if candidates.is_empty():
|
||||||
|
# Fallback: try lower rarity equipment
|
||||||
|
if rarity != ItemRarity.COMMON:
|
||||||
|
# Try common equipment as fallback
|
||||||
|
for item_id in item_definitions.keys():
|
||||||
|
var item_data = item_definitions[item_id]
|
||||||
|
if item_data.has("rarity") and item_data["rarity"] == ItemRarity.COMMON:
|
||||||
|
if item_data.has("item_type") and item_data["item_type"] == Item.ItemType.Equippable:
|
||||||
|
candidates.append(item_id)
|
||||||
|
var drop_chance = item_data.get("drop_chance", 1.0)
|
||||||
|
weights.append(drop_chance)
|
||||||
|
total_weight += drop_chance
|
||||||
|
|
||||||
|
if candidates.is_empty():
|
||||||
|
# Last resort: return any common item
|
||||||
|
return get_random_item_by_rarity(ItemRarity.COMMON)
|
||||||
|
|
||||||
|
# Weighted random selection
|
||||||
|
var weight_roll = randf() * total_weight
|
||||||
|
var cumulative = 0.0
|
||||||
|
for i in range(candidates.size()):
|
||||||
|
cumulative += weights[i]
|
||||||
|
if weight_roll <= cumulative:
|
||||||
|
return create_item(candidates[i])
|
||||||
|
|
||||||
|
# Fallback
|
||||||
|
return create_item(candidates[candidates.size() - 1])
|
||||||
|
|
||||||
# Get random items for enemies (weighted towards common/uncommon)
|
# Get random items for enemies (weighted towards common/uncommon)
|
||||||
static func get_random_enemy_drop() -> Item:
|
static func get_random_enemy_drop() -> Item:
|
||||||
_initialize()
|
_initialize()
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ func _ready():
|
|||||||
# Setup sprite based on loot type (call after all properties are set)
|
# Setup sprite based on loot type (call after all properties are set)
|
||||||
call_deferred("_setup_sprite")
|
call_deferred("_setup_sprite")
|
||||||
|
|
||||||
|
# CRITICAL: Duplicate sprite material after setup so it isn't shared between loot instances
|
||||||
|
# This must be called after _setup_sprite because materials may be applied there (e.g., ItemDatabase.apply_item_colors_to_sprite)
|
||||||
|
call_deferred("_duplicate_sprite_material")
|
||||||
|
|
||||||
# Setup collision shape based on loot type
|
# Setup collision shape based on loot type
|
||||||
call_deferred("_setup_collision_shape")
|
call_deferred("_setup_collision_shape")
|
||||||
|
|
||||||
@@ -187,6 +191,12 @@ func _setup_collision_shape():
|
|||||||
|
|
||||||
collision_shape.shape = circle_shape
|
collision_shape.shape = circle_shape
|
||||||
|
|
||||||
|
func _duplicate_sprite_material():
|
||||||
|
# Duplicate sprite material so it isn't shared between loot instances
|
||||||
|
# This prevents material state from being shared (e.g., colorization, tint effects)
|
||||||
|
if sprite and sprite.material:
|
||||||
|
sprite.material = sprite.material.duplicate()
|
||||||
|
|
||||||
func _create_quantity_badge(quantity: int):
|
func _create_quantity_badge(quantity: int):
|
||||||
# Create a label to show the quantity
|
# Create a label to show the quantity
|
||||||
quantity_badge = Label.new()
|
quantity_badge = Label.new()
|
||||||
@@ -515,19 +525,40 @@ func _process_pickup_on_server(player: Node):
|
|||||||
LootType.APPLE:
|
LootType.APPLE:
|
||||||
if sfx_potion_collect:
|
if sfx_potion_collect:
|
||||||
sfx_potion_collect.play()
|
sfx_potion_collect.play()
|
||||||
# Heal player
|
|
||||||
var actual_heal = 0.0
|
# Create Item instance and add to inventory instead of directly healing
|
||||||
if player.has_method("heal"):
|
var apple_item = ItemDatabase.create_item("apple")
|
||||||
actual_heal = heal_amount
|
if apple_item and player.character_stats:
|
||||||
player.heal(heal_amount)
|
var was_encumbered = player.character_stats.is_over_encumbered()
|
||||||
# Show floating text with item graphic and heal amount
|
player.character_stats.add_item(apple_item)
|
||||||
|
if not was_encumbered and player.character_stats.is_over_encumbered():
|
||||||
|
if player.has_method("show_floating_status"):
|
||||||
|
player.show_floating_status("Encumbered!", Color(1.0, 0.5, 0.2))
|
||||||
|
|
||||||
|
# Sync inventory+equipment to joiner (server added to their copy; joiner's client must apply)
|
||||||
|
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||||
|
var owner_id = player.get_multiplayer_authority()
|
||||||
|
if owner_id != 1 and owner_id != multiplayer.get_unique_id():
|
||||||
|
var inv_data: Array = []
|
||||||
|
for inv_item in player.character_stats.inventory:
|
||||||
|
inv_data.append(inv_item.save() if inv_item else null)
|
||||||
|
var equip_data: Dictionary = {}
|
||||||
|
for slot_name in player.character_stats.equipment.keys():
|
||||||
|
var eq = player.character_stats.equipment[slot_name]
|
||||||
|
equip_data[slot_name] = eq.save() if eq else null
|
||||||
|
if player.has_method("_apply_inventory_and_equipment_from_server"):
|
||||||
|
player._apply_inventory_and_equipment_from_server.rpc_id(owner_id, inv_data, equip_data)
|
||||||
|
|
||||||
|
# Show floating text with item name (uppercase)
|
||||||
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
_show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 10)
|
var display_text = "APPLE"
|
||||||
|
var text_color = Color.GREEN
|
||||||
|
_show_floating_text(player, display_text, text_color, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 10)
|
||||||
# Sync floating text to client via GameWorld to avoid loot node path RPCs
|
# Sync floating text to client via GameWorld to avoid loot node path RPCs
|
||||||
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
|
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
if game_world and game_world.has_method("_sync_loot_floating_text"):
|
if game_world and game_world.has_method("_sync_loot_floating_text"):
|
||||||
game_world._sync_loot_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(int(actual_heal)) + " HP", Color.GREEN, (8 * 20) + 10, player.get_multiplayer_authority())
|
game_world._sync_loot_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, display_text, text_color, (8 * 20) + 10, player.get_multiplayer_authority())
|
||||||
|
|
||||||
self.visible = false
|
self.visible = false
|
||||||
|
|
||||||
@@ -538,19 +569,40 @@ func _process_pickup_on_server(player: Node):
|
|||||||
LootType.BANANA:
|
LootType.BANANA:
|
||||||
if sfx_banana_collect:
|
if sfx_banana_collect:
|
||||||
sfx_banana_collect.play()
|
sfx_banana_collect.play()
|
||||||
# Heal player
|
|
||||||
var actual_heal = 0.0
|
# Create Item instance and add to inventory instead of directly healing
|
||||||
if player.has_method("heal"):
|
var banana_item = ItemDatabase.create_item("banana")
|
||||||
actual_heal = heal_amount
|
if banana_item and player.character_stats:
|
||||||
player.heal(heal_amount)
|
var was_encumbered = player.character_stats.is_over_encumbered()
|
||||||
# Show floating text with item graphic and heal amount
|
player.character_stats.add_item(banana_item)
|
||||||
|
if not was_encumbered and player.character_stats.is_over_encumbered():
|
||||||
|
if player.has_method("show_floating_status"):
|
||||||
|
player.show_floating_status("Encumbered!", Color(1.0, 0.5, 0.2))
|
||||||
|
|
||||||
|
# Sync inventory+equipment to joiner (server added to their copy; joiner's client must apply)
|
||||||
|
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||||
|
var owner_id = player.get_multiplayer_authority()
|
||||||
|
if owner_id != 1 and owner_id != multiplayer.get_unique_id():
|
||||||
|
var inv_data: Array = []
|
||||||
|
for inv_item in player.character_stats.inventory:
|
||||||
|
inv_data.append(inv_item.save() if inv_item else null)
|
||||||
|
var equip_data: Dictionary = {}
|
||||||
|
for slot_name in player.character_stats.equipment.keys():
|
||||||
|
var eq = player.character_stats.equipment[slot_name]
|
||||||
|
equip_data[slot_name] = eq.save() if eq else null
|
||||||
|
if player.has_method("_apply_inventory_and_equipment_from_server"):
|
||||||
|
player._apply_inventory_and_equipment_from_server.rpc_id(owner_id, inv_data, equip_data)
|
||||||
|
|
||||||
|
# Show floating text with item name (uppercase)
|
||||||
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
_show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 11)
|
var display_text = "BANANA"
|
||||||
|
var text_color = Color.GREEN
|
||||||
|
_show_floating_text(player, display_text, text_color, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 11)
|
||||||
# Sync floating text to client via GameWorld to avoid loot node path RPCs
|
# Sync floating text to client via GameWorld to avoid loot node path RPCs
|
||||||
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
|
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
if game_world and game_world.has_method("_sync_loot_floating_text"):
|
if game_world and game_world.has_method("_sync_loot_floating_text"):
|
||||||
game_world._sync_loot_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(int(actual_heal)) + " HP", Color.GREEN, (8 * 20) + 11, player.get_multiplayer_authority())
|
game_world._sync_loot_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, display_text, text_color, (8 * 20) + 11, player.get_multiplayer_authority())
|
||||||
|
|
||||||
self.visible = false
|
self.visible = false
|
||||||
|
|
||||||
@@ -561,19 +613,40 @@ func _process_pickup_on_server(player: Node):
|
|||||||
LootType.CHERRY:
|
LootType.CHERRY:
|
||||||
if sfx_banana_collect:
|
if sfx_banana_collect:
|
||||||
sfx_banana_collect.play()
|
sfx_banana_collect.play()
|
||||||
# Heal player
|
|
||||||
var actual_heal = 0.0
|
# Create Item instance and add to inventory instead of directly healing
|
||||||
if player.has_method("heal"):
|
var cherry_item = ItemDatabase.create_item("cherry")
|
||||||
actual_heal = heal_amount
|
if cherry_item and player.character_stats:
|
||||||
player.heal(heal_amount)
|
var was_encumbered = player.character_stats.is_over_encumbered()
|
||||||
# Show floating text with item graphic and heal amount
|
player.character_stats.add_item(cherry_item)
|
||||||
|
if not was_encumbered and player.character_stats.is_over_encumbered():
|
||||||
|
if player.has_method("show_floating_status"):
|
||||||
|
player.show_floating_status("Encumbered!", Color(1.0, 0.5, 0.2))
|
||||||
|
|
||||||
|
# Sync inventory+equipment to joiner (server added to their copy; joiner's client must apply)
|
||||||
|
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||||
|
var owner_id = player.get_multiplayer_authority()
|
||||||
|
if owner_id != 1 and owner_id != multiplayer.get_unique_id():
|
||||||
|
var inv_data: Array = []
|
||||||
|
for inv_item in player.character_stats.inventory:
|
||||||
|
inv_data.append(inv_item.save() if inv_item else null)
|
||||||
|
var equip_data: Dictionary = {}
|
||||||
|
for slot_name in player.character_stats.equipment.keys():
|
||||||
|
var eq = player.character_stats.equipment[slot_name]
|
||||||
|
equip_data[slot_name] = eq.save() if eq else null
|
||||||
|
if player.has_method("_apply_inventory_and_equipment_from_server"):
|
||||||
|
player._apply_inventory_and_equipment_from_server.rpc_id(owner_id, inv_data, equip_data)
|
||||||
|
|
||||||
|
# Show floating text with item name (uppercase)
|
||||||
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
_show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 12)
|
var display_text = "CHERRY"
|
||||||
|
var text_color = Color.GREEN
|
||||||
|
_show_floating_text(player, display_text, text_color, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 12)
|
||||||
# Sync floating text to client via GameWorld to avoid loot node path RPCs
|
# Sync floating text to client via GameWorld to avoid loot node path RPCs
|
||||||
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
|
if multiplayer.has_multiplayer_peer() and player.get_multiplayer_authority() != 1:
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
if game_world and game_world.has_method("_sync_loot_floating_text"):
|
if game_world and game_world.has_method("_sync_loot_floating_text"):
|
||||||
game_world._sync_loot_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, "+" + str(int(actual_heal)) + " HP", Color.GREEN, (8 * 20) + 12, player.get_multiplayer_authority())
|
game_world._sync_loot_floating_text.rpc_id(player.get_multiplayer_authority(), loot_type, display_text, text_color, (8 * 20) + 12, player.get_multiplayer_authority())
|
||||||
|
|
||||||
self.visible = false
|
self.visible = false
|
||||||
|
|
||||||
|
|||||||
@@ -354,6 +354,20 @@ func _handle_new_peer(uuid: String):
|
|||||||
# Just store the UUID for now, peer ID will come via Signal message
|
# Just store the UUID for now, peer ID will come via Signal message
|
||||||
log_print("MatchboxClient: Client received NewPeer for UUID: " + uuid + " (waiting for host to assign peer ID)")
|
log_print("MatchboxClient: Client received NewPeer for UUID: " + uuid + " (waiting for host to assign peer ID)")
|
||||||
|
|
||||||
|
# If we're in reconnection mode and don't have a host connection yet, this might be the host reconnecting
|
||||||
|
# Check if we previously had a host (peer ID 1) but lost it
|
||||||
|
var had_host = false
|
||||||
|
for stored_uuid in peer_uuid_to_id:
|
||||||
|
if peer_uuid_to_id[stored_uuid] == 1:
|
||||||
|
had_host = true
|
||||||
|
break
|
||||||
|
|
||||||
|
# If we don't have a host connection and we're waiting for reconnection, this could be the host
|
||||||
|
if not had_host and not peer_connections.has(1):
|
||||||
|
log_print("MatchboxClient: Client received NewPeer while waiting for host reconnection - this might be the host reconnecting")
|
||||||
|
# Don't do anything yet - wait for host to assign peer ID via Signal
|
||||||
|
# But we can clear any reconnection flags in NetworkManager if needed
|
||||||
|
|
||||||
func _handle_peer_left_uuid(uuid: String):
|
func _handle_peer_left_uuid(uuid: String):
|
||||||
if uuid.is_empty():
|
if uuid.is_empty():
|
||||||
return
|
return
|
||||||
@@ -585,6 +599,14 @@ func _handle_signal_message(peer_id: int, signal_data: Dictionary):
|
|||||||
_handle_signal_message_dict(queued_msg)
|
_handle_signal_message_dict(queued_msg)
|
||||||
queued_signaling_messages.clear()
|
queued_signaling_messages.clear()
|
||||||
|
|
||||||
|
# If we're in reconnection mode, clear it since we've successfully reconnected
|
||||||
|
var network_manager = get_parent()
|
||||||
|
if network_manager and "reconnection_attempting" in network_manager:
|
||||||
|
if network_manager.reconnection_attempting:
|
||||||
|
log_print("MatchboxClient: Host reconnected and assigned peer ID - clearing reconnection state")
|
||||||
|
network_manager.reconnection_attempting = false
|
||||||
|
network_manager.reconnection_timer = 0.0
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Handle WebRTC signaling messages (offer, answer, ice-candidate)
|
# Handle WebRTC signaling messages (offer, answer, ice-candidate)
|
||||||
|
|||||||
@@ -519,6 +519,15 @@ func _on_matchbox_connected(was_reconnecting: bool = false):
|
|||||||
if chat_ui and chat_ui.has_method("add_colorful_local_message"):
|
if chat_ui and chat_ui.has_method("add_colorful_local_message"):
|
||||||
chat_ui.add_colorful_local_message("System", "Matchbox connection re-established!")
|
chat_ui.add_colorful_local_message("System", "Matchbox connection re-established!")
|
||||||
|
|
||||||
|
# For joiners: if we're connected to Matchbox but in reconnection mode,
|
||||||
|
# we're waiting for the host to reconnect and assign our peer ID
|
||||||
|
# Don't clear reconnection state yet - wait for peer ID assignment
|
||||||
|
if not is_hosting and reconnection_attempting and matchbox_client:
|
||||||
|
if matchbox_client.is_network_connected:
|
||||||
|
log_print("NetworkManager: Joiner connected to Matchbox, waiting for host to reconnect and assign peer ID...")
|
||||||
|
# Don't clear reconnection_attempting yet - wait for peer ID assignment
|
||||||
|
# The matchbox_client will clear it when peer ID is assigned
|
||||||
|
|
||||||
func _on_matchbox_webrtc_ready():
|
func _on_matchbox_webrtc_ready():
|
||||||
log_print("NetworkManager: WebRTC mesh is ready")
|
log_print("NetworkManager: WebRTC mesh is ready")
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,8 @@ var flame_spell_scene = preload("res://scenes/attack_spell_flame.tscn") # Flame
|
|||||||
var frostspike_spell_scene = preload("res://scenes/attack_spell_frostspike.tscn") # Frostspike for Tome of Frostspike
|
var frostspike_spell_scene = preload("res://scenes/attack_spell_frostspike.tscn") # Frostspike for Tome of Frostspike
|
||||||
var healing_effect_scene = preload("res://scenes/healing_effect.tscn") # Visual effect for Tome of Healing
|
var healing_effect_scene = preload("res://scenes/healing_effect.tscn") # Visual effect for Tome of Healing
|
||||||
var attack_bomb_scene = preload("res://scenes/attack_bomb.tscn") # Bomb projectile
|
var attack_bomb_scene = preload("res://scenes/attack_bomb.tscn") # Bomb projectile
|
||||||
|
var attack_punch_scene = preload("res://scenes/attack_punch.tscn") # Unarmed punch
|
||||||
|
var attack_axe_swing_scene = preload("res://scenes/attack_axe_swing.tscn") # Axe swing (orbits)
|
||||||
var blood_scene = preload("res://scenes/blood_clot.tscn")
|
var blood_scene = preload("res://scenes/blood_clot.tscn")
|
||||||
|
|
||||||
# Simulated Z-axis for height (when thrown)
|
# Simulated Z-axis for height (when thrown)
|
||||||
@@ -139,6 +141,7 @@ var spawn_landing: bool = false
|
|||||||
var spawn_landing_landed: bool = false
|
var spawn_landing_landed: bool = false
|
||||||
var spawn_landing_bounced: bool = false
|
var spawn_landing_bounced: bool = false
|
||||||
var spawn_landing_visible_shown: bool = false # One-shot: set true when we show right before falling
|
var spawn_landing_visible_shown: bool = false # One-shot: set true when we show right before falling
|
||||||
|
var has_seen_exit_this_level: bool = false # Track if player has seen exit notification for current level
|
||||||
|
|
||||||
# Components
|
# Components
|
||||||
# @onready var sprite = $Sprite2D # REMOVED: Now using layered sprites
|
# @onready var sprite = $Sprite2D # REMOVED: Now using layered sprites
|
||||||
@@ -237,7 +240,7 @@ const ANIMATIONS = {
|
|||||||
},
|
},
|
||||||
"PUNCH": {
|
"PUNCH": {
|
||||||
"frames": [16, 17, 18],
|
"frames": [16, 17, 18],
|
||||||
"frameDurations": [50, 70, 100],
|
"frameDurations": [60, 90, 120],
|
||||||
"loop": false,
|
"loop": false,
|
||||||
"nextAnimation": "IDLE"
|
"nextAnimation": "IDLE"
|
||||||
},
|
},
|
||||||
@@ -344,8 +347,8 @@ const ANIMATIONS = {
|
|||||||
"nextAnimation": null
|
"nextAnimation": null
|
||||||
},
|
},
|
||||||
"STAND": {
|
"STAND": {
|
||||||
"frames": [23,24,22,1],
|
"frames": [23, 24, 22, 1],
|
||||||
"frameDurations": [40,40,40,40],
|
"frameDurations": [40, 40, 40, 40],
|
||||||
"loop": false,
|
"loop": false,
|
||||||
"nextAnimation": "IDLE"
|
"nextAnimation": "IDLE"
|
||||||
}
|
}
|
||||||
@@ -386,7 +389,7 @@ func _ready():
|
|||||||
if cone_light:
|
if cone_light:
|
||||||
cone_light.visible = is_local_player
|
cone_light.visible = is_local_player
|
||||||
if point_light:
|
if point_light:
|
||||||
point_light.visible = is_local_player
|
point_light.visible = true # Show point light for all joiners (cone is local-only)
|
||||||
elif is_local_player:
|
elif is_local_player:
|
||||||
# Local players (initial spawn only): hide until right before fall-from-sky
|
# Local players (initial spawn only): hide until right before fall-from-sky
|
||||||
visible = false
|
visible = false
|
||||||
@@ -407,9 +410,8 @@ func _ready():
|
|||||||
visible = true
|
visible = true
|
||||||
spawn_landing = false
|
spawn_landing = false
|
||||||
if cone_light:
|
if cone_light:
|
||||||
cone_light.visible = false
|
cone_light.visible = false # Don't show other players' cone lights
|
||||||
if point_light:
|
# point_light stays visible for other players
|
||||||
point_light.visible = false
|
|
||||||
|
|
||||||
# Set respawn point to starting position
|
# Set respawn point to starting position
|
||||||
respawn_point = global_position
|
respawn_point = global_position
|
||||||
@@ -789,15 +791,25 @@ func _randomize_stats():
|
|||||||
func _setup_player_appearance():
|
func _setup_player_appearance():
|
||||||
# Randomize appearance - players spawn "bare" (naked, no equipment)
|
# Randomize appearance - players spawn "bare" (naked, no equipment)
|
||||||
# But with randomized hair, facial hair, eyes, etc.
|
# But with randomized hair, facial hair, eyes, etc.
|
||||||
|
|
||||||
# Ensure character_stats exists before setting appearance
|
# Ensure character_stats exists before setting appearance
|
||||||
if not character_stats:
|
if not character_stats:
|
||||||
LogManager.log_error("Player " + str(name) + " _setup_player_appearance: character_stats is null!", LogManager.CATEGORY_GAMEPLAY)
|
LogManager.log_error("Player " + str(name) + " _setup_player_appearance: character_stats is null!", LogManager.CATEGORY_GAMEPLAY)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Randomize race first (affects appearance constraints and stats)
|
# Use race from select screen if set (local player only); otherwise randomize (affects appearance and stats)
|
||||||
var races = ["Dwarf", "Elf", "Human"]
|
var races = ["Dwarf", "Elf", "Human"]
|
||||||
var selected_race = races[appearance_rng.randi() % races.size()]
|
var selected_race: String
|
||||||
|
var gs_race_read: String = ""
|
||||||
|
if is_local_player:
|
||||||
|
var gs = get_node_or_null("/root/GameState")
|
||||||
|
if gs:
|
||||||
|
gs_race_read = gs.selected_race
|
||||||
|
if gs.selected_race != "" and gs.selected_race in races:
|
||||||
|
selected_race = gs.selected_race
|
||||||
|
if selected_race.is_empty():
|
||||||
|
selected_race = races[appearance_rng.randi() % races.size()]
|
||||||
|
# Log what joiner/local player was made (authority runs this; joiner's client runs it for joiner's player)
|
||||||
|
print("Player ", name, " _setup_player_appearance: peer_id=", peer_id, " is_local_player=", is_local_player, " is_authority=", is_multiplayer_authority(), " GameState.selected_race='", gs_race_read, "' -> USING race='", selected_race, "'")
|
||||||
character_stats.setRace(selected_race)
|
character_stats.setRace(selected_race)
|
||||||
|
|
||||||
# Randomize stats AFTER race is set (race affects stat modifiers)
|
# Randomize stats AFTER race is set (race affects stat modifiers)
|
||||||
@@ -813,23 +825,38 @@ func _setup_player_appearance():
|
|||||||
character_stats.equipment["offhand"] = starting_arrows
|
character_stats.equipment["offhand"] = starting_arrows
|
||||||
print("Elf player ", name, " spawned with short bow and 3 arrows")
|
print("Elf player ", name, " spawned with short bow and 3 arrows")
|
||||||
|
|
||||||
# Give Dwarf race starting bomb
|
# Give Dwarf race starting bomb + debug weapons in inventory (axe, dagger/knife, sword)
|
||||||
if selected_race == "Dwarf":
|
if selected_race == "Dwarf":
|
||||||
var starting_bomb = ItemDatabase.create_item("bomb")
|
var starting_bomb = ItemDatabase.create_item("bomb")
|
||||||
if starting_bomb:
|
if starting_bomb:
|
||||||
starting_bomb.quantity = 5 # Give Dwarf 5 bombs to start
|
starting_bomb.quantity = 5 # Give Dwarf 5 bombs to start
|
||||||
character_stats.equipment["offhand"] = starting_bomb
|
character_stats.equipment["offhand"] = starting_bomb
|
||||||
print("Dwarf player ", name, " spawned with 5 bombs")
|
var debug_axe = ItemDatabase.create_item("axe")
|
||||||
|
if debug_axe:
|
||||||
|
character_stats.add_item(debug_axe)
|
||||||
|
var debug_dagger = ItemDatabase.create_item("knife")
|
||||||
|
if debug_dagger:
|
||||||
|
character_stats.add_item(debug_dagger)
|
||||||
|
var debug_sword = ItemDatabase.create_item("short_sword")
|
||||||
|
if debug_sword:
|
||||||
|
character_stats.add_item(debug_sword)
|
||||||
|
print("Dwarf player ", name, " spawned with 5 bombs and debug axe/dagger/sword in inventory")
|
||||||
|
|
||||||
# Give Human race (Wizard) starting spellbook (Tome of Flames) and Hat
|
# Give Human race (Wizard) starting spellbook (Tome of Flames), Tome of Healing, Tome of Frostspike, and Hat
|
||||||
if selected_race == "Human":
|
if selected_race == "Human":
|
||||||
var starting_tome = ItemDatabase.create_item("tome_of_flames")
|
var starting_tome = ItemDatabase.create_item("tome_of_flames")
|
||||||
if starting_tome:
|
if starting_tome:
|
||||||
character_stats.equipment["offhand"] = starting_tome
|
character_stats.equipment["offhand"] = starting_tome
|
||||||
|
var tome_healing = ItemDatabase.create_item("tome_of_healing")
|
||||||
|
if tome_healing:
|
||||||
|
character_stats.add_item(tome_healing)
|
||||||
|
var tome_frostspike = ItemDatabase.create_item("tome_of_frostspike")
|
||||||
|
if tome_frostspike:
|
||||||
|
character_stats.add_item(tome_frostspike)
|
||||||
var starting_hat = ItemDatabase.create_item("hat")
|
var starting_hat = ItemDatabase.create_item("hat")
|
||||||
if starting_hat:
|
if starting_hat:
|
||||||
character_stats.equipment["headgear"] = starting_hat
|
character_stats.equipment["headgear"] = starting_hat
|
||||||
print("Human player ", name, " spawned with Tome of Flames and Hat")
|
print("Human player ", name, " spawned with Tome of Flames, Tome of Healing, Tome of Frostspike, and Hat")
|
||||||
|
|
||||||
# Randomize skin (human only for players)
|
# Randomize skin (human only for players)
|
||||||
# Weighted random: Human1 has highest chance, Human7 has lowest chance
|
# Weighted random: Human1 has highest chance, Human7 has lowest chance
|
||||||
@@ -1310,8 +1337,8 @@ func _apply_weapon_color_replacements(sprite: Sprite2D, item: Item) -> void:
|
|||||||
var shader_material = sprite.material as ShaderMaterial
|
var shader_material = sprite.material as ShaderMaterial
|
||||||
# Staff colors that should be replaced on the weapon sprite
|
# Staff colors that should be replaced on the weapon sprite
|
||||||
var staff_colors = [
|
var staff_colors = [
|
||||||
Color(209/255.0, 142/255.0, 54/255.0),
|
Color(209 / 255.0, 142 / 255.0, 54 / 255.0),
|
||||||
Color(192/255.0, 112/255.0, 31/255.0)
|
Color(192 / 255.0, 112 / 255.0, 31 / 255.0)
|
||||||
]
|
]
|
||||||
|
|
||||||
var replacement_index = 0
|
var replacement_index = 0
|
||||||
@@ -1359,6 +1386,7 @@ func _on_character_changed(_char: CharacterStats):
|
|||||||
|
|
||||||
# ALWAYS sync race and base stats to all clients (for proper display)
|
# ALWAYS sync race and base stats to all clients (for proper display)
|
||||||
# This ensures new clients get appearance data even if they connect after initial setup
|
# This ensures new clients get appearance data even if they connect after initial setup
|
||||||
|
print("Player ", name, " (authority) SENDING _sync_race_and_stats to all peers: race='", character_stats.race, "'")
|
||||||
_rpc_to_ready_peers("_sync_race_and_stats", [character_stats.race, character_stats.baseStats.duplicate()])
|
_rpc_to_ready_peers("_sync_race_and_stats", [character_stats.race, character_stats.baseStats.duplicate()])
|
||||||
|
|
||||||
# Sync full appearance data (skin, hair, eyes, colors, etc.) to remote players
|
# Sync full appearance data (skin, hair, eyes, colors, etc.) to remote players
|
||||||
@@ -1708,7 +1736,7 @@ func _direction_to_angle(direction: int) -> float:
|
|||||||
# Update cone light rotation based on player's facing direction
|
# Update cone light rotation based on player's facing direction
|
||||||
func _update_cone_light_rotation():
|
func _update_cone_light_rotation():
|
||||||
if cone_light:
|
if cone_light:
|
||||||
cone_light.rotation = _direction_to_angle(current_direction)+(PI/2)
|
cone_light.rotation = _direction_to_angle(current_direction) + (PI / 2)
|
||||||
|
|
||||||
# Create a cone-shaped light texture programmatically
|
# Create a cone-shaped light texture programmatically
|
||||||
# Creates a directional cone texture that extends forward and fades to the sides
|
# Creates a directional cone texture that extends forward and fades to the sides
|
||||||
@@ -1906,6 +1934,13 @@ func _physics_process(delta):
|
|||||||
if is_airborne:
|
if is_airborne:
|
||||||
_update_z_physics(delta)
|
_update_z_physics(delta)
|
||||||
|
|
||||||
|
# Mana regeneration (slowly regain mana over time)
|
||||||
|
if character_stats and is_multiplayer_authority():
|
||||||
|
# Regenerate 2 mana per second (slow regeneration)
|
||||||
|
const MANA_REGEN_RATE = 2.0 # mana per second
|
||||||
|
if character_stats.mp < character_stats.maxmp:
|
||||||
|
character_stats.restore_mana(MANA_REGEN_RATE * delta)
|
||||||
|
|
||||||
# Spell charge visuals (incantation, tint pulse) - run for ALL players so others see the pulse
|
# Spell charge visuals (incantation, tint pulse) - run for ALL players so others see the pulse
|
||||||
if is_charging_spell:
|
if is_charging_spell:
|
||||||
var charge_time = (Time.get_ticks_msec() / 1000.0) - spell_charge_start_time
|
var charge_time = (Time.get_ticks_msec() / 1000.0) - spell_charge_start_time
|
||||||
@@ -2016,7 +2051,9 @@ func _physics_process(delta):
|
|||||||
if character_stats:
|
if character_stats:
|
||||||
var old_hp = character_stats.hp
|
var old_hp = character_stats.hp
|
||||||
character_stats.modify_health(-burn_debuff_damage_per_second)
|
character_stats.modify_health(-burn_debuff_damage_per_second)
|
||||||
if character_stats.hp <= 0:
|
# Check if dead (use epsilon to handle floating point precision)
|
||||||
|
if character_stats.hp <= 0.001:
|
||||||
|
character_stats.hp = 0.0 # Ensure exactly 0
|
||||||
character_stats.no_health.emit()
|
character_stats.no_health.emit()
|
||||||
character_stats.character_changed.emit(character_stats)
|
character_stats.character_changed.emit(character_stats)
|
||||||
var actual_damage = old_hp - character_stats.hp
|
var actual_damage = old_hp - character_stats.hp
|
||||||
@@ -2058,7 +2095,7 @@ func _physics_process(delta):
|
|||||||
# Exception: entrance walk-out - velocity is driven by game_world for cut-scene
|
# Exception: entrance walk-out - velocity is driven by game_world for cut-scene
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
# Reset animation to IDLE if not in a special state (skip when spawn_landing: we use DIE until stand up)
|
# Reset animation to IDLE if not in a special state (skip when spawn_landing: we use DIE until stand up)
|
||||||
if not spawn_landing and current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF":
|
if not spawn_landing and current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF" and current_animation != "AXE" and current_animation != "PUNCH":
|
||||||
if is_lifting:
|
if is_lifting:
|
||||||
_set_animation("IDLE_HOLD")
|
_set_animation("IDLE_HOLD")
|
||||||
elif is_pushing:
|
elif is_pushing:
|
||||||
@@ -2381,7 +2418,7 @@ func _handle_input():
|
|||||||
_set_animation("RUN_PULL")
|
_set_animation("RUN_PULL")
|
||||||
else:
|
else:
|
||||||
_set_animation("RUN_PUSH")
|
_set_animation("RUN_PUSH")
|
||||||
elif current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF" and current_animation != "CONJURE" and current_animation != "LIFT" and current_animation != "FINISH_SPELL":
|
elif current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF" and current_animation != "AXE" and current_animation != "PUNCH" and current_animation != "CONJURE" and current_animation != "LIFT" and current_animation != "FINISH_SPELL":
|
||||||
_set_animation("RUN")
|
_set_animation("RUN")
|
||||||
else:
|
else:
|
||||||
# Idle animations
|
# Idle animations
|
||||||
@@ -2412,7 +2449,7 @@ func _handle_input():
|
|||||||
current_direction = new_direction
|
current_direction = new_direction
|
||||||
_update_cone_light_rotation()
|
_update_cone_light_rotation()
|
||||||
else:
|
else:
|
||||||
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF" and current_animation != "CONJURE" and current_animation != "LIFT" and current_animation != "FINISH_SPELL":
|
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF" and current_animation != "AXE" and current_animation != "PUNCH" and current_animation != "CONJURE" and current_animation != "LIFT" and current_animation != "FINISH_SPELL":
|
||||||
_set_animation("IDLE")
|
_set_animation("IDLE")
|
||||||
|
|
||||||
# Handle drag sound for interactable objects
|
# Handle drag sound for interactable objects
|
||||||
@@ -2447,7 +2484,7 @@ func _handle_input():
|
|||||||
elif is_charging_bow:
|
elif is_charging_bow:
|
||||||
speed_multiplier = 0.5
|
speed_multiplier = 0.5
|
||||||
elif is_charging_spell:
|
elif is_charging_spell:
|
||||||
speed_multiplier = 0.2 # 20% speed (80% reduction)
|
speed_multiplier = 0.5 # 50% speed (50% reduction)
|
||||||
elif is_shielding:
|
elif is_shielding:
|
||||||
speed_multiplier = 0.6 # 60% speed when blocking with shield
|
speed_multiplier = 0.6 # 60% speed when blocking with shield
|
||||||
|
|
||||||
@@ -2640,6 +2677,24 @@ func _handle_interactions():
|
|||||||
break
|
break
|
||||||
|
|
||||||
if grab_just_pressed and not is_charging_spell and can_start_charge and not nearby_grabbable and not is_lifting and not held_object:
|
if grab_just_pressed and not is_charging_spell and can_start_charge and not nearby_grabbable and not is_lifting and not held_object:
|
||||||
|
# Check if player has enough mana before starting to charge
|
||||||
|
var has_enough_mana = false
|
||||||
|
if character_stats:
|
||||||
|
if is_fire:
|
||||||
|
has_enough_mana = character_stats.mp >= 15.0 # Flame spell cost
|
||||||
|
elif is_frost:
|
||||||
|
has_enough_mana = character_stats.mp >= 15.0 # Frostspike spell cost
|
||||||
|
else:
|
||||||
|
has_enough_mana = character_stats.mp >= 20.0 # Heal spell cost
|
||||||
|
|
||||||
|
if not has_enough_mana:
|
||||||
|
# Not enough mana - show message to local player only
|
||||||
|
if is_local_player:
|
||||||
|
_show_not_enough_mana_text()
|
||||||
|
print(name, " cannot start charging spell - not enough mana")
|
||||||
|
just_grabbed_this_frame = false
|
||||||
|
return
|
||||||
|
|
||||||
is_charging_spell = true
|
is_charging_spell = true
|
||||||
current_spell_element = "healing" if is_heal else ("frost" if is_frost else "fire")
|
current_spell_element = "healing" if is_heal else ("frost" if is_frost else "fire")
|
||||||
spell_charge_start_time = Time.get_ticks_msec() / 1000.0
|
spell_charge_start_time = Time.get_ticks_msec() / 1000.0
|
||||||
@@ -2681,12 +2736,40 @@ func _handle_interactions():
|
|||||||
has_valid_target = ((is_fire or is_frost) and target_pos != Vector2.ZERO) or (is_heal and heal_target != null)
|
has_valid_target = ((is_fire or is_frost) and target_pos != Vector2.ZERO) or (is_heal and heal_target != null)
|
||||||
|
|
||||||
if has_valid_target and is_fully_charged:
|
if has_valid_target and is_fully_charged:
|
||||||
|
# Check if player has enough mana before casting
|
||||||
|
var has_enough_mana = false
|
||||||
|
if character_stats:
|
||||||
|
if is_fire:
|
||||||
|
has_enough_mana = character_stats.mp >= 15.0 # Flame spell cost
|
||||||
|
elif is_frost:
|
||||||
|
has_enough_mana = character_stats.mp >= 15.0 # Frostspike spell cost
|
||||||
|
else:
|
||||||
|
has_enough_mana = character_stats.mp >= 20.0 # Heal spell cost
|
||||||
|
|
||||||
|
if has_enough_mana:
|
||||||
if is_fire:
|
if is_fire:
|
||||||
_cast_flame_spell(target_pos)
|
_cast_flame_spell(target_pos)
|
||||||
elif is_frost:
|
elif is_frost:
|
||||||
_cast_frostspike_spell(target_pos)
|
_cast_frostspike_spell(target_pos)
|
||||||
else:
|
else:
|
||||||
_cast_heal_spell(heal_target)
|
_cast_heal_spell(heal_target)
|
||||||
|
else:
|
||||||
|
# Not enough mana - cancel spell
|
||||||
|
print(name, " cannot cast spell - not enough mana")
|
||||||
|
is_charging_spell = false
|
||||||
|
current_spell_element = "fire"
|
||||||
|
spell_incantation_played = false
|
||||||
|
_stop_spell_charge_particles()
|
||||||
|
_stop_spell_charge_incantation()
|
||||||
|
_clear_spell_charge_tint()
|
||||||
|
_set_animation("IDLE")
|
||||||
|
if has_node("SfxSpellCharge"):
|
||||||
|
$SfxSpellCharge.stop()
|
||||||
|
if has_node("SfxSpellIncantation"):
|
||||||
|
$SfxSpellIncantation.stop()
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
_sync_spell_charge_end.rpc()
|
||||||
|
return
|
||||||
_set_animation("FINISH_SPELL")
|
_set_animation("FINISH_SPELL")
|
||||||
movement_lock_timer = SPELL_CAST_LOCK_DURATION
|
movement_lock_timer = SPELL_CAST_LOCK_DURATION
|
||||||
is_charging_spell = false
|
is_charging_spell = false
|
||||||
@@ -2853,6 +2936,14 @@ func _handle_interactions():
|
|||||||
# 2. Button is still down (shouldn't happen, but safety check)
|
# 2. Button is still down (shouldn't happen, but safety check)
|
||||||
# 3. grab_just_pressed is also true (same frame tap)
|
# 3. grab_just_pressed is also true (same frame tap)
|
||||||
if grab_just_released:
|
if grab_just_released:
|
||||||
|
# Stop reviving if was reviving
|
||||||
|
if is_reviving:
|
||||||
|
is_reviving = false
|
||||||
|
revive_charge = 0.0
|
||||||
|
# Sync revive end to all clients
|
||||||
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
|
_sync_revive_end.rpc()
|
||||||
|
else:
|
||||||
is_reviving = false
|
is_reviving = false
|
||||||
revive_charge = 0.0
|
revive_charge = 0.0
|
||||||
|
|
||||||
@@ -2920,17 +3011,33 @@ func _handle_interactions():
|
|||||||
var holding_dead_player = _is_player(held_object) and "is_dead" in held_object and held_object.is_dead
|
var holding_dead_player = _is_player(held_object) and "is_dead" in held_object and held_object.is_dead
|
||||||
var reviver_hp = character_stats.hp if character_stats else 1.0
|
var reviver_hp = character_stats.hp if character_stats else 1.0
|
||||||
if holding_dead_player and reviver_hp > 1.0:
|
if holding_dead_player and reviver_hp > 1.0:
|
||||||
|
# Start reviving if not already
|
||||||
|
if not is_reviving:
|
||||||
is_reviving = true
|
is_reviving = true
|
||||||
|
# Sync revive start to all clients
|
||||||
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
|
_sync_revive_start.rpc()
|
||||||
revive_charge += get_process_delta_time()
|
revive_charge += get_process_delta_time()
|
||||||
if revive_charge >= REVIVE_DURATION:
|
if revive_charge >= REVIVE_DURATION:
|
||||||
_do_revive(held_object)
|
_do_revive(held_object)
|
||||||
_place_down_object()
|
_place_down_object()
|
||||||
is_reviving = false
|
is_reviving = false
|
||||||
revive_charge = 0.0
|
revive_charge = 0.0
|
||||||
|
# Sync revive end to all clients
|
||||||
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
|
_sync_revive_end.rpc()
|
||||||
else:
|
else:
|
||||||
_update_lifted_object()
|
_update_lifted_object()
|
||||||
else:
|
else:
|
||||||
if holding_dead_player:
|
if holding_dead_player:
|
||||||
|
# Stop reviving if was reviving
|
||||||
|
if is_reviving:
|
||||||
|
is_reviving = false
|
||||||
|
revive_charge = 0.0
|
||||||
|
# Sync revive end to all clients
|
||||||
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
|
_sync_revive_end.rpc()
|
||||||
|
else:
|
||||||
is_reviving = false
|
is_reviving = false
|
||||||
revive_charge = 0.0
|
revive_charge = 0.0
|
||||||
_update_lifted_object()
|
_update_lifted_object()
|
||||||
@@ -3843,17 +3950,25 @@ func _perform_attack():
|
|||||||
|
|
||||||
var is_bow = false
|
var is_bow = false
|
||||||
var is_staff = false
|
var is_staff = false
|
||||||
|
var is_axe = false
|
||||||
|
var is_unarmed = (equipped_weapon == null)
|
||||||
if equipped_weapon:
|
if equipped_weapon:
|
||||||
if equipped_weapon.weapon_type == Item.WeaponType.BOW:
|
if equipped_weapon.weapon_type == Item.WeaponType.BOW:
|
||||||
is_bow = true
|
is_bow = true
|
||||||
elif equipped_weapon.weapon_type == Item.WeaponType.STAFF:
|
elif equipped_weapon.weapon_type == Item.WeaponType.STAFF:
|
||||||
is_staff = true
|
is_staff = true
|
||||||
|
elif equipped_weapon.weapon_type == Item.WeaponType.AXE:
|
||||||
|
is_axe = true
|
||||||
|
|
||||||
# Play attack animation based on weapon
|
# Play attack animation based on weapon (PUNCH when no mainhand)
|
||||||
if is_bow:
|
if is_bow:
|
||||||
_set_animation("BOW")
|
_set_animation("BOW")
|
||||||
elif is_staff:
|
elif is_staff:
|
||||||
_set_animation("STAFF")
|
_set_animation("STAFF")
|
||||||
|
elif is_axe:
|
||||||
|
_set_animation("AXE")
|
||||||
|
elif is_unarmed:
|
||||||
|
_set_animation("PUNCH")
|
||||||
else:
|
else:
|
||||||
_set_animation("SWORD")
|
_set_animation("SWORD")
|
||||||
|
|
||||||
@@ -3961,8 +4076,31 @@ func _perform_attack():
|
|||||||
var spawn_offset = attack_direction * 6.0
|
var spawn_offset = attack_direction * 6.0
|
||||||
projectile.global_position = global_position + spawn_offset
|
projectile.global_position = global_position + spawn_offset
|
||||||
print(name, " attacked with staff projectile! Damage: ", final_damage, " (base: ", base_damage, ", crit: ", is_crit, ")")
|
print(name, " attacked with staff projectile! Damage: ", final_damage, " (base: ", base_damage, ", crit: ", is_crit, ")")
|
||||||
|
elif is_axe:
|
||||||
|
# Axe swing - stays on player, plays directional animation
|
||||||
|
if attack_axe_swing_scene and equipped_weapon:
|
||||||
|
spawned_projectile_type = "axe"
|
||||||
|
var axe_swing = attack_axe_swing_scene.instantiate()
|
||||||
|
get_parent().add_child(axe_swing)
|
||||||
|
axe_swing.setup(attack_direction, self, -1.0, equipped_weapon)
|
||||||
|
axe_swing.global_position = global_position
|
||||||
|
print(name, " axe swing! Damage: ", final_damage)
|
||||||
|
elif is_unarmed:
|
||||||
|
# Unarmed punch - low STR-based damage (enemy armour mitigates via take_damage)
|
||||||
|
if attack_punch_scene:
|
||||||
|
spawned_projectile_type = "punch"
|
||||||
|
var punch_damage = 2.0
|
||||||
|
if character_stats:
|
||||||
|
var str_total = character_stats.baseStats.str + character_stats.get_pass("str")
|
||||||
|
punch_damage = 2.0 + str_total * 0.1
|
||||||
|
punch_damage = max(1.0, round(punch_damage * 10.0) / 10.0)
|
||||||
|
var punch = attack_punch_scene.instantiate()
|
||||||
|
get_parent().add_child(punch)
|
||||||
|
punch.setup(attack_direction, self, punch_damage)
|
||||||
|
punch.global_position = global_position + attack_direction * 12.0
|
||||||
|
print(name, " punched! Damage: ", punch_damage)
|
||||||
else:
|
else:
|
||||||
# Spawn sword projectile for non-bow/staff weapons
|
# Spawn sword projectile for non-bow/staff/axe weapons
|
||||||
if sword_projectile_scene:
|
if sword_projectile_scene:
|
||||||
spawned_projectile_type = "sword"
|
spawned_projectile_type = "sword"
|
||||||
var projectile = sword_projectile_scene.instantiate()
|
var projectile = sword_projectile_scene.instantiate()
|
||||||
@@ -4248,6 +4386,14 @@ func _cast_flame_spell(target_position: Vector2):
|
|||||||
if not is_multiplayer_authority():
|
if not is_multiplayer_authority():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check mana cost (15 mana for flame spell)
|
||||||
|
const FLAME_SPELL_MANA_COST = 15.0
|
||||||
|
if not character_stats:
|
||||||
|
return
|
||||||
|
if not character_stats.use_mana(FLAME_SPELL_MANA_COST):
|
||||||
|
print(name, " cannot cast flame spell - not enough mana (need ", FLAME_SPELL_MANA_COST, ", have ", character_stats.mp, ")")
|
||||||
|
return
|
||||||
|
|
||||||
# Find valid spell target position (closest valid if target is blocked)
|
# Find valid spell target position (closest valid if target is blocked)
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
var valid_target_pos = target_position
|
var valid_target_pos = target_position
|
||||||
@@ -4256,7 +4402,9 @@ func _cast_flame_spell(target_position: Vector2):
|
|||||||
if found_pos != Vector2.ZERO:
|
if found_pos != Vector2.ZERO:
|
||||||
valid_target_pos = found_pos
|
valid_target_pos = found_pos
|
||||||
else:
|
else:
|
||||||
# No valid position found, cancel spell
|
# No valid position found, cancel spell and refund mana
|
||||||
|
if character_stats:
|
||||||
|
character_stats.restore_mana(FLAME_SPELL_MANA_COST)
|
||||||
print(name, " cannot cast spell - no valid target position")
|
print(name, " cannot cast spell - no valid target position")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -4301,6 +4449,15 @@ func _cast_frostspike_spell(target_position: Vector2):
|
|||||||
return
|
return
|
||||||
if not is_multiplayer_authority():
|
if not is_multiplayer_authority():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check mana cost (15 mana for frostspike spell)
|
||||||
|
const FROSTSPIKE_SPELL_MANA_COST = 15.0
|
||||||
|
if not character_stats:
|
||||||
|
return
|
||||||
|
if not character_stats.use_mana(FROSTSPIKE_SPELL_MANA_COST):
|
||||||
|
print(name, " cannot cast frostspike - not enough mana (need ", FROSTSPIKE_SPELL_MANA_COST, ", have ", character_stats.mp, ")")
|
||||||
|
return
|
||||||
|
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
var valid_target_pos = target_position
|
var valid_target_pos = target_position
|
||||||
if game_world and game_world.has_method("_get_valid_spell_target_position"):
|
if game_world and game_world.has_method("_get_valid_spell_target_position"):
|
||||||
@@ -4308,6 +4465,9 @@ func _cast_frostspike_spell(target_position: Vector2):
|
|||||||
if found_pos != Vector2.ZERO:
|
if found_pos != Vector2.ZERO:
|
||||||
valid_target_pos = found_pos
|
valid_target_pos = found_pos
|
||||||
else:
|
else:
|
||||||
|
# No valid position found, cancel spell and refund mana
|
||||||
|
if character_stats:
|
||||||
|
character_stats.restore_mana(FROSTSPIKE_SPELL_MANA_COST)
|
||||||
print(name, " cannot cast frostspike - no valid target position")
|
print(name, " cannot cast frostspike - no valid target position")
|
||||||
return
|
return
|
||||||
var spell_damage = 15.0
|
var spell_damage = 15.0
|
||||||
@@ -4336,6 +4496,12 @@ func _cast_heal_spell(target: Node):
|
|||||||
return
|
return
|
||||||
if not character_stats:
|
if not character_stats:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check mana cost (20 mana for heal spell - more expensive since it's healing)
|
||||||
|
const HEAL_SPELL_MANA_COST = 20.0
|
||||||
|
if not character_stats.use_mana(HEAL_SPELL_MANA_COST):
|
||||||
|
print(name, " cannot cast heal spell - not enough mana (need ", HEAL_SPELL_MANA_COST, ", have ", character_stats.mp, ")")
|
||||||
|
return
|
||||||
var gw = get_tree().get_first_node_in_group("game_world")
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
var dungeon_seed: int = 0
|
var dungeon_seed: int = 0
|
||||||
if gw and "dungeon_seed" in gw:
|
if gw and "dungeon_seed" in gw:
|
||||||
@@ -4522,7 +4688,6 @@ func _get_heal_target() -> Node:
|
|||||||
func _can_cast_spell_at(target_position: Vector2) -> bool:
|
func _can_cast_spell_at(target_position: Vector2) -> bool:
|
||||||
# Check if spell can be cast at target position
|
# Check if spell can be cast at target position
|
||||||
# Must be on floor tile and not blocked by walls
|
# Must be on floor tile and not blocked by walls
|
||||||
|
|
||||||
# Get game world for dungeon data
|
# Get game world for dungeon data
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
if not game_world:
|
if not game_world:
|
||||||
@@ -4941,6 +5106,28 @@ func _sync_spell_charge_end():
|
|||||||
|
|
||||||
print(name, " (synced) ended charging spell")
|
print(name, " (synced) ended charging spell")
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_revive_start():
|
||||||
|
# Sync revive start to other clients - show AnimationIncantation effect
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
is_reviving = true
|
||||||
|
revive_charge = 0.0
|
||||||
|
# Play healing_charging animation on AnimationIncantation
|
||||||
|
if has_node("AnimationIncantation") and not is_charging_spell:
|
||||||
|
$AnimationIncantation.play("healing_charging")
|
||||||
|
print(name, " (synced) started reviving")
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_revive_end():
|
||||||
|
# Sync revive end to other clients - stop AnimationIncantation effect
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
is_reviving = false
|
||||||
|
revive_charge = 0.0
|
||||||
|
# Stop healing_charging animation
|
||||||
|
if has_node("AnimationIncantation") and not is_charging_spell:
|
||||||
|
_stop_spell_charge_incantation()
|
||||||
|
print(name, " (synced) stopped reviving")
|
||||||
|
|
||||||
func _apply_burn_debuff():
|
func _apply_burn_debuff():
|
||||||
# Apply burn debuff to player
|
# Apply burn debuff to player
|
||||||
var was_already_burning = burn_debuff_timer > 0.0
|
var was_already_burning = burn_debuff_timer > 0.0
|
||||||
@@ -5184,6 +5371,40 @@ func _rpc_to_ready_peers(method: String, args: Array = []):
|
|||||||
else:
|
else:
|
||||||
callv("rpc", [method] + args)
|
callv("rpc", [method] + args)
|
||||||
|
|
||||||
|
# Push this player's full state (equipment, inventory, race, appearance) to a single peer.
|
||||||
|
# Used when a new joiner connects so they receive the host's (and other existing players') state.
|
||||||
|
func _push_full_state_to_peer(target_peer_id: int) -> void:
|
||||||
|
if not is_multiplayer_authority() or not character_stats or not is_inside_tree():
|
||||||
|
return
|
||||||
|
var equipment_data = {}
|
||||||
|
for slot_name in character_stats.equipment.keys():
|
||||||
|
var item = character_stats.equipment[slot_name]
|
||||||
|
if item:
|
||||||
|
equipment_data[slot_name] = item.save()
|
||||||
|
else:
|
||||||
|
equipment_data[slot_name] = null
|
||||||
|
var inventory_data = []
|
||||||
|
for item in character_stats.inventory:
|
||||||
|
if item:
|
||||||
|
inventory_data.append(item.save())
|
||||||
|
var appearance_data = {
|
||||||
|
"skin": character_stats.skin,
|
||||||
|
"hairstyle": character_stats.hairstyle,
|
||||||
|
"hair_color": character_stats.hair_color.to_html(true),
|
||||||
|
"facial_hair": character_stats.facial_hair,
|
||||||
|
"facial_hair_color": character_stats.facial_hair_color.to_html(true),
|
||||||
|
"eyes": character_stats.eyes,
|
||||||
|
"eye_color": character_stats.eye_color.to_html(true),
|
||||||
|
"eye_lashes": character_stats.eye_lashes,
|
||||||
|
"eyelash_color": character_stats.eyelash_color.to_html(true),
|
||||||
|
"add_on": character_stats.add_on
|
||||||
|
}
|
||||||
|
_sync_equipment.rpc_id(target_peer_id, equipment_data)
|
||||||
|
_sync_inventory.rpc_id(target_peer_id, inventory_data)
|
||||||
|
_sync_race_and_stats.rpc_id(target_peer_id, character_stats.race, character_stats.baseStats.duplicate())
|
||||||
|
_sync_appearance.rpc_id(target_peer_id, appearance_data)
|
||||||
|
print(name, " pushed full state (equipment, inventory, race, appearance) to peer ", target_peer_id, " inventory size: ", inventory_data.size())
|
||||||
|
|
||||||
func _is_peer_connected_for_rpc(target_peer_id: int) -> bool:
|
func _is_peer_connected_for_rpc(target_peer_id: int) -> bool:
|
||||||
"""Check if a peer is still connected and has open data channels before sending RPC"""
|
"""Check if a peer is still connected and has open data channels before sending RPC"""
|
||||||
if not multiplayer.has_multiplayer_peer():
|
if not multiplayer.has_multiplayer_peer():
|
||||||
@@ -5291,6 +5512,10 @@ func _sync_attack(direction: int, attack_dir: Vector2, charge_percentage: float
|
|||||||
_set_animation("STAFF")
|
_set_animation("STAFF")
|
||||||
"arrow":
|
"arrow":
|
||||||
_set_animation("BOW")
|
_set_animation("BOW")
|
||||||
|
"axe":
|
||||||
|
_set_animation("AXE")
|
||||||
|
"punch":
|
||||||
|
_set_animation("PUNCH")
|
||||||
_:
|
_:
|
||||||
_set_animation("SWORD")
|
_set_animation("SWORD")
|
||||||
|
|
||||||
@@ -5316,6 +5541,19 @@ func _sync_attack(direction: int, attack_dir: Vector2, charge_percentage: float
|
|||||||
get_parent().add_child(arrow_projectile)
|
get_parent().add_child(arrow_projectile)
|
||||||
arrow_projectile.shoot(attack_dir, global_position, self, charge_percentage)
|
arrow_projectile.shoot(attack_dir, global_position, self, charge_percentage)
|
||||||
print(name, " performed synced bow attack with arrow (charge: ", charge_percentage * 100, "%)")
|
print(name, " performed synced bow attack with arrow (charge: ", charge_percentage * 100, "%)")
|
||||||
|
elif projectile_type == "axe" and attack_axe_swing_scene:
|
||||||
|
var axe_swing = attack_axe_swing_scene.instantiate()
|
||||||
|
get_parent().add_child(axe_swing)
|
||||||
|
var axe_item = character_stats.equipment.get("mainhand", null) if character_stats else null
|
||||||
|
axe_swing.setup(attack_dir, self, -1.0, axe_item)
|
||||||
|
axe_swing.global_position = global_position
|
||||||
|
print(name, " performed synced axe swing!")
|
||||||
|
elif projectile_type == "punch" and attack_punch_scene:
|
||||||
|
var punch = attack_punch_scene.instantiate()
|
||||||
|
get_parent().add_child(punch)
|
||||||
|
punch.setup(attack_dir, self, 3.0)
|
||||||
|
punch.global_position = global_position + attack_dir * 12.0
|
||||||
|
print(name, " performed synced punch!")
|
||||||
elif (projectile_type == "sword" or projectile_type == "") and sword_projectile_scene:
|
elif (projectile_type == "sword" or projectile_type == "") and sword_projectile_scene:
|
||||||
var projectile = sword_projectile_scene.instantiate()
|
var projectile = sword_projectile_scene.instantiate()
|
||||||
get_parent().add_child(projectile)
|
get_parent().add_child(projectile)
|
||||||
@@ -6126,7 +6364,9 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
|
|||||||
# Apply the reduced damage using take_damage (which handles health modification and signals)
|
# Apply the reduced damage using take_damage (which handles health modification and signals)
|
||||||
var _old_hp = character_stats.hp
|
var _old_hp = character_stats.hp
|
||||||
character_stats.modify_health(-actual_damage)
|
character_stats.modify_health(-actual_damage)
|
||||||
if character_stats.hp <= 0:
|
# Check if dead (use epsilon to handle floating point precision)
|
||||||
|
if character_stats.hp <= 0.001:
|
||||||
|
character_stats.hp = 0.0 # Ensure exactly 0
|
||||||
character_stats.no_health.emit()
|
character_stats.no_health.emit()
|
||||||
character_stats.character_changed.emit(character_stats)
|
character_stats.character_changed.emit(character_stats)
|
||||||
print(name, " took ", actual_damage, " damage (", amount, " base - ", character_stats.defense, " DEF = ", actual_damage, ")! Health: ", character_stats.hp, "/", character_stats.maxhp)
|
print(name, " took ", actual_damage, " damage (", amount, " base - ", character_stats.defense, " DEF = ", actual_damage, ")! Health: ", character_stats.hp, "/", character_stats.maxhp)
|
||||||
@@ -6160,7 +6400,7 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
|
|||||||
velocity = direction_from_attacker * 250.0 # Scaled down for 1x scale
|
velocity = direction_from_attacker * 250.0 # Scaled down for 1x scale
|
||||||
|
|
||||||
# Face the attacker (opposite of knockback direction)
|
# Face the attacker (opposite of knockback direction)
|
||||||
var face_direction = -direction_from_attacker
|
var face_direction = - direction_from_attacker
|
||||||
current_direction = _get_direction_from_vector(face_direction) as Direction
|
current_direction = _get_direction_from_vector(face_direction) as Direction
|
||||||
facing_direction_vector = face_direction.normalized()
|
facing_direction_vector = face_direction.normalized()
|
||||||
|
|
||||||
@@ -6187,12 +6427,13 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
|
|||||||
_rpc_to_ready_peers("_sync_damage", [actual_damage, attacker_position])
|
_rpc_to_ready_peers("_sync_damage", [actual_damage, attacker_position])
|
||||||
|
|
||||||
# Check if dead - but wait for damage animation to play first
|
# Check if dead - but wait for damage animation to play first
|
||||||
|
# Use small epsilon to handle floating point precision issues (HP might be 0.0000001 instead of exactly 0.0)
|
||||||
var health = character_stats.hp if character_stats else current_health
|
var health = character_stats.hp if character_stats else current_health
|
||||||
if health <= 0:
|
if health <= 0.001: # Use epsilon to catch values very close to 0
|
||||||
if character_stats:
|
if character_stats:
|
||||||
character_stats.hp = 0 # Clamp to 0
|
character_stats.hp = 0.0 # Clamp to exactly 0
|
||||||
else:
|
else:
|
||||||
current_health = 0 # Clamp to 0
|
current_health = 0.0 # Clamp to exactly 0
|
||||||
is_dead = true # Set flag immediately to prevent more damage
|
is_dead = true # Set flag immediately to prevent more damage
|
||||||
# Wait a bit for damage animation and knockback to show
|
# Wait a bit for damage animation and knockback to show
|
||||||
await get_tree().create_timer(0.3).timeout
|
await get_tree().create_timer(0.3).timeout
|
||||||
@@ -6407,7 +6648,8 @@ func _spawn_landing_stand_up():
|
|||||||
status_anim.play("idle")
|
status_anim.play("idle")
|
||||||
# STAND's nextAnimation -> IDLE, so we're already IDLE or about to be
|
# STAND's nextAnimation -> IDLE, so we're already IDLE or about to be
|
||||||
spawn_landing = false
|
spawn_landing = false
|
||||||
if cone_light:
|
# Only show cone light for local player (don't show other players' cone lights)
|
||||||
|
if is_local_player and cone_light:
|
||||||
cone_light.visible = true
|
cone_light.visible = true
|
||||||
if point_light:
|
if point_light:
|
||||||
point_light.visible = true
|
point_light.visible = true
|
||||||
@@ -6428,9 +6670,11 @@ func _respawn():
|
|||||||
print(name, " respawning!")
|
print(name, " respawning!")
|
||||||
was_revived = false
|
was_revived = false
|
||||||
|
|
||||||
|
# Get game_world reference (used multiple times in this function)
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
|
||||||
# Hide GAME OVER screen and fade in game graphics when player respawns (only on authority)
|
# Hide GAME OVER screen and fade in game graphics when player respawns (only on authority)
|
||||||
if is_multiplayer_authority():
|
if is_multiplayer_authority():
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
|
||||||
if game_world and game_world.has_method("_hide_game_over"):
|
if game_world and game_world.has_method("_hide_game_over"):
|
||||||
game_world._hide_game_over()
|
game_world._hide_game_over()
|
||||||
|
|
||||||
@@ -6469,7 +6713,6 @@ func _respawn():
|
|||||||
|
|
||||||
# Get respawn position - use spawn room (start room) for respawning
|
# Get respawn position - use spawn room (start room) for respawning
|
||||||
var new_respawn_pos = respawn_point
|
var new_respawn_pos = respawn_point
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
|
||||||
|
|
||||||
if not game_world:
|
if not game_world:
|
||||||
push_error(name, " respawn: Could not find game_world!")
|
push_error(name, " respawn: Could not find game_world!")
|
||||||
@@ -6621,6 +6864,10 @@ func _revive_from_player(hp_amount: int):
|
|||||||
_set_animation("IDLE")
|
_set_animation("IDLE")
|
||||||
# Same healing effect as Tome of Healing (green frames, pulse, +X HP)
|
# Same healing effect as Tome of Healing (green frames, pulse, +X HP)
|
||||||
_spawn_heal_effect_and_text(self, hp_amount, false, false)
|
_spawn_heal_effect_and_text(self, hp_amount, false, false)
|
||||||
|
# CRITICAL: Unregister from dead_players dictionary so game knows we're alive
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_unregister_player_died"):
|
||||||
|
game_world._unregister_player_died(self)
|
||||||
# Clear concussion on all clients (authority already did above; broadcast for others)
|
# Clear concussion on all clients (authority already did above; broadcast for others)
|
||||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
_rpc_to_ready_peers("_sync_revived_clear_concussion", [name])
|
_rpc_to_ready_peers("_sync_revived_clear_concussion", [name])
|
||||||
@@ -6661,6 +6908,10 @@ func _revive_from_heal(hp_amount: int):
|
|||||||
status_anim.play("idle")
|
status_anim.play("idle")
|
||||||
_set_animation("IDLE")
|
_set_animation("IDLE")
|
||||||
_spawn_heal_effect_and_text(self, hp_amount, false, false)
|
_spawn_heal_effect_and_text(self, hp_amount, false, false)
|
||||||
|
# CRITICAL: Unregister from dead_players dictionary so game knows we're alive
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_unregister_player_died"):
|
||||||
|
game_world._unregister_player_died(self)
|
||||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
_rpc_to_ready_peers("_sync_revived_clear_concussion", [name])
|
_rpc_to_ready_peers("_sync_revived_clear_concussion", [name])
|
||||||
|
|
||||||
@@ -6745,16 +6996,16 @@ func add_coins(amount: int):
|
|||||||
var the_peer_id = get_multiplayer_authority()
|
var the_peer_id = get_multiplayer_authority()
|
||||||
# Only sync if this is a client player (not server's own player)
|
# Only sync if this is a client player (not server's own player)
|
||||||
if the_peer_id != 0 and the_peer_id != multiplayer.get_unique_id():
|
if the_peer_id != 0 and the_peer_id != multiplayer.get_unique_id():
|
||||||
print(name, " syncing stats to client peer_id=", the_peer_id, " kills=", character_stats.kills, " coins=", character_stats.coin)
|
print(name, " syncing stats to client peer_id=", the_peer_id, " kills=", character_stats.kills, " coins=", character_stats.coin, " xp=", character_stats.xp)
|
||||||
_sync_stats_update.rpc_id(the_peer_id, character_stats.kills, character_stats.coin)
|
_sync_stats_update.rpc_id(the_peer_id, character_stats.kills, character_stats.coin, character_stats.xp)
|
||||||
else:
|
else:
|
||||||
coins += amount
|
coins += amount
|
||||||
print(name, " picked up ", amount, " coin(s)! Total coins: ", coins)
|
print(name, " picked up ", amount, " coin(s)! Total coins: ", coins)
|
||||||
|
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _sync_stats_update(kills_count: int, coins_count: int):
|
func _sync_stats_update(kills_count: int, coins_count: int, xp_amount: float = -1.0):
|
||||||
# Client receives stats update from server (for kills and coins)
|
# Client receives stats update from server (for kills, coins, and XP)
|
||||||
# Update local stats to match server
|
# Update local stats to match server
|
||||||
# Only process on client (not on server where the update originated)
|
# Only process on client (not on server where the update originated)
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
@@ -6763,13 +7014,21 @@ func _sync_stats_update(kills_count: int, coins_count: int):
|
|||||||
if character_stats:
|
if character_stats:
|
||||||
character_stats.kills = kills_count
|
character_stats.kills = kills_count
|
||||||
character_stats.coin = coins_count
|
character_stats.coin = coins_count
|
||||||
print(name, " stats synced from server: kills=", kills_count, " coins=", coins_count)
|
if xp_amount >= 0.0: # Only update XP if provided (backwards compatible)
|
||||||
|
# Calculate the difference and add it (to trigger level up if needed)
|
||||||
|
var xp_diff = xp_amount - character_stats.xp
|
||||||
|
if xp_diff > 0.0:
|
||||||
|
character_stats.add_xp(xp_diff)
|
||||||
|
else:
|
||||||
|
character_stats.xp = xp_amount
|
||||||
|
var xp_display = str(xp_amount) if xp_amount >= 0.0 else "unchanged"
|
||||||
|
print(name, " stats synced from server: kills=", kills_count, " coins=", coins_count, " xp=", xp_display)
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _sync_race_and_stats(race: String, base_stats: Dictionary):
|
func _sync_race_and_stats(race: String, base_stats: Dictionary):
|
||||||
# Client receives race and base stats from authority player
|
# Client receives race and base stats from authority player
|
||||||
# Accept initial sync (when race is empty), but reject changes if we're authority
|
# Accept initial sync (when race is empty), but reject changes if we're authority
|
||||||
print(name, " _sync_race_and_stats received: race=", race, " is_authority=", is_multiplayer_authority(), " current_race=", character_stats.race if character_stats else "null")
|
print("Player ", name, " RECEIVED _sync_race_and_stats: race='", race, "' (peer_id=", peer_id, " is_authority=", is_multiplayer_authority(), " current_race=", character_stats.race if character_stats else "null", ")")
|
||||||
|
|
||||||
# CRITICAL: If we're the authority for this player, we should NOT process race syncs
|
# CRITICAL: If we're the authority for this player, we should NOT process race syncs
|
||||||
# The authority player manages its own appearance and only syncs to others
|
# The authority player manages its own appearance and only syncs to others
|
||||||
@@ -6783,6 +7042,7 @@ func _sync_race_and_stats(race: String, base_stats: Dictionary):
|
|||||||
if character_stats:
|
if character_stats:
|
||||||
character_stats.race = race
|
character_stats.race = race
|
||||||
character_stats.baseStats = base_stats
|
character_stats.baseStats = base_stats
|
||||||
|
print("Player ", name, " APPLIED _sync_race_and_stats: this node now has race='", race, "' (server/other peer's view of this player)")
|
||||||
|
|
||||||
# For remote players, we don't re-initialize appearance here
|
# For remote players, we don't re-initialize appearance here
|
||||||
# Instead, we wait for _sync_appearance RPC which contains the full appearance data
|
# Instead, we wait for _sync_appearance RPC which contains the full appearance data
|
||||||
@@ -6819,7 +7079,7 @@ func _sync_race_and_stats(race: String, base_stats: Dictionary):
|
|||||||
|
|
||||||
"Dwarf":
|
"Dwarf":
|
||||||
character_stats.setEars(0)
|
character_stats.setEars(0)
|
||||||
# Give Dwarf starting bombs to remote players ONLY when offhand is null (initial sync)
|
# Give Dwarf starting bombs + debug weapons to remote players ONLY when offhand is null (initial sync)
|
||||||
# Never overwrite existing equipment (e.g. shield, tome) - preserves loadout across level transitions
|
# Never overwrite existing equipment (e.g. shield, tome) - preserves loadout across level transitions
|
||||||
if not is_multiplayer_authority():
|
if not is_multiplayer_authority():
|
||||||
if character_stats.equipment["offhand"] == null:
|
if character_stats.equipment["offhand"] == null:
|
||||||
@@ -6827,12 +7087,21 @@ func _sync_race_and_stats(race: String, base_stats: Dictionary):
|
|||||||
if starting_bomb:
|
if starting_bomb:
|
||||||
starting_bomb.quantity = 5 # Give Dwarf 5 bombs to start
|
starting_bomb.quantity = 5 # Give Dwarf 5 bombs to start
|
||||||
character_stats.equipment["offhand"] = starting_bomb
|
character_stats.equipment["offhand"] = starting_bomb
|
||||||
|
var debug_axe = ItemDatabase.create_item("axe")
|
||||||
|
if debug_axe:
|
||||||
|
character_stats.add_item(debug_axe)
|
||||||
|
var debug_dagger = ItemDatabase.create_item("knife")
|
||||||
|
if debug_dagger:
|
||||||
|
character_stats.add_item(debug_dagger)
|
||||||
|
var debug_sword = ItemDatabase.create_item("short_sword")
|
||||||
|
if debug_sword:
|
||||||
|
character_stats.add_item(debug_sword)
|
||||||
_apply_appearance_to_sprites()
|
_apply_appearance_to_sprites()
|
||||||
print("Dwarf player ", name, " (remote) received 5 bombs via race sync")
|
print("Dwarf player ", name, " (remote) received 5 bombs and debug axe/dagger/sword via race sync")
|
||||||
|
|
||||||
"Human":
|
"Human":
|
||||||
character_stats.setEars(0)
|
character_stats.setEars(0)
|
||||||
# Give Human (Wizard) starting tome and hat to remote players ONLY when slots are null (initial sync)
|
# Give Human (Wizard) starting tomes and hat to remote players ONLY when slots are null (initial sync)
|
||||||
# Never overwrite existing equipment - preserves loadout across level transitions
|
# Never overwrite existing equipment - preserves loadout across level transitions
|
||||||
if not is_multiplayer_authority():
|
if not is_multiplayer_authority():
|
||||||
var offhand_empty = character_stats.equipment["offhand"] == null
|
var offhand_empty = character_stats.equipment["offhand"] == null
|
||||||
@@ -6841,11 +7110,17 @@ func _sync_race_and_stats(race: String, base_stats: Dictionary):
|
|||||||
var starting_tome = ItemDatabase.create_item("tome_of_flames")
|
var starting_tome = ItemDatabase.create_item("tome_of_flames")
|
||||||
if starting_tome:
|
if starting_tome:
|
||||||
character_stats.equipment["offhand"] = starting_tome
|
character_stats.equipment["offhand"] = starting_tome
|
||||||
|
var tome_healing = ItemDatabase.create_item("tome_of_healing")
|
||||||
|
if tome_healing:
|
||||||
|
character_stats.add_item(tome_healing)
|
||||||
|
var tome_frostspike = ItemDatabase.create_item("tome_of_frostspike")
|
||||||
|
if tome_frostspike:
|
||||||
|
character_stats.add_item(tome_frostspike)
|
||||||
var starting_hat = ItemDatabase.create_item("hat")
|
var starting_hat = ItemDatabase.create_item("hat")
|
||||||
if starting_hat:
|
if starting_hat:
|
||||||
character_stats.equipment["headgear"] = starting_hat
|
character_stats.equipment["headgear"] = starting_hat
|
||||||
_apply_appearance_to_sprites()
|
_apply_appearance_to_sprites()
|
||||||
print("Human player ", name, " (remote) received Tome of Flames and Hat via race sync")
|
print("Human player ", name, " (remote) received Tome of Flames, Tome of Healing, Tome of Frostspike, and Hat via race sync")
|
||||||
|
|
||||||
_:
|
_:
|
||||||
character_stats.setEars(0)
|
character_stats.setEars(0)
|
||||||
@@ -7013,7 +7288,6 @@ func _sync_keys(new_key_count: int):
|
|||||||
func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false, is_blocked: bool = false):
|
func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false, is_blocked: bool = false):
|
||||||
# Show damage number (red, using dmg_numbers.png font) above player
|
# Show damage number (red, using dmg_numbers.png font) above player
|
||||||
# Show even if amount is 0 for MISS/DODGED/BLOCKED
|
# Show even if amount is 0 for MISS/DODGED/BLOCKED
|
||||||
|
|
||||||
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
||||||
if not damage_number_scene:
|
if not damage_number_scene:
|
||||||
return
|
return
|
||||||
@@ -7077,6 +7351,24 @@ func _show_revive_cost_number(amount: int):
|
|||||||
get_tree().current_scene.add_child(damage_label)
|
get_tree().current_scene.add_child(damage_label)
|
||||||
damage_label.global_position = global_position + Vector2(0, -16)
|
damage_label.global_position = global_position + Vector2(0, -16)
|
||||||
|
|
||||||
|
func _show_not_enough_mana_text():
|
||||||
|
"""Show 'NOT ENOUGH MANA' in damage_number font above player (local player only)."""
|
||||||
|
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
||||||
|
if not damage_number_scene:
|
||||||
|
return
|
||||||
|
var lbl = damage_number_scene.instantiate()
|
||||||
|
if not lbl:
|
||||||
|
return
|
||||||
|
lbl.label = "NOT ENOUGH MANA"
|
||||||
|
lbl.color = Color(1.0, 0.5, 0.2) # Orange/red color
|
||||||
|
lbl.z_index = 5
|
||||||
|
lbl.direction = Vector2(0, -1)
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
var parent = game_world.get_node_or_null("Entities") if game_world else get_tree().current_scene
|
||||||
|
if parent:
|
||||||
|
parent.add_child(lbl)
|
||||||
|
lbl.global_position = global_position + Vector2(0, -20)
|
||||||
|
|
||||||
func show_floating_status(text: String, col: Color = Color.WHITE) -> void:
|
func show_floating_status(text: String, col: Color = Color.WHITE) -> void:
|
||||||
"""Show a damage-number-style floating text above player (e.g. 'Encumbered!')."""
|
"""Show a damage-number-style floating text above player (e.g. 'Encumbered!')."""
|
||||||
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
||||||
@@ -7222,7 +7514,7 @@ func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = fa
|
|||||||
velocity = direction_from_attacker * 250.0
|
velocity = direction_from_attacker * 250.0
|
||||||
|
|
||||||
# Face the attacker
|
# Face the attacker
|
||||||
var face_direction = -direction_from_attacker
|
var face_direction = - direction_from_attacker
|
||||||
current_direction = _get_direction_from_vector(face_direction) as Direction
|
current_direction = _get_direction_from_vector(face_direction) as Direction
|
||||||
facing_direction_vector = face_direction.normalized()
|
facing_direction_vector = face_direction.normalized()
|
||||||
|
|
||||||
@@ -7297,6 +7589,13 @@ func _on_exit_found():
|
|||||||
if not is_multiplayer_authority():
|
if not is_multiplayer_authority():
|
||||||
return # Only authority triggers
|
return # Only authority triggers
|
||||||
|
|
||||||
|
# Only show notification once per level
|
||||||
|
if has_seen_exit_this_level:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Mark as seen for this level
|
||||||
|
has_seen_exit_this_level = true
|
||||||
|
|
||||||
# Show exclamation mark
|
# Show exclamation mark
|
||||||
_show_alert_indicator()
|
_show_alert_indicator()
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ func spawn_player(peer_id: int, local_index: int):
|
|||||||
|
|
||||||
player.position = spawn_pos
|
player.position = spawn_pos
|
||||||
|
|
||||||
|
# Set multiplayer authority BEFORE add_child so _ready() sees correct authority.
|
||||||
|
# Otherwise joiner's player runs _ready() with default authority (e.g. server) and skips _setup_player_appearance(),
|
||||||
|
# so joiner Wizard never gets tome_of_healing / tome_of_frostspike.
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
player.set_multiplayer_authority(peer_id)
|
||||||
|
|
||||||
# Add to YSort node for automatic Y-sorting
|
# Add to YSort node for automatic Y-sorting
|
||||||
var ysort = get_parent().get_node_or_null("Entities")
|
var ysort = get_parent().get_node_or_null("Entities")
|
||||||
if ysort:
|
if ysort:
|
||||||
@@ -91,10 +97,6 @@ func spawn_player(peer_id: int, local_index: int):
|
|||||||
# Fallback to parent if YSort doesn't exist
|
# Fallback to parent if YSort doesn't exist
|
||||||
get_parent().add_child(player)
|
get_parent().add_child(player)
|
||||||
|
|
||||||
# Set multiplayer authority AFTER adding to scene
|
|
||||||
if multiplayer.has_multiplayer_peer():
|
|
||||||
player.set_multiplayer_authority(peer_id)
|
|
||||||
|
|
||||||
players[unique_id] = player
|
players[unique_id] = player
|
||||||
|
|
||||||
func despawn_players_for_peer(peer_id: int):
|
func despawn_players_for_peer(peer_id: int):
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ func _on_player_entered_room(player: Node):
|
|||||||
LogManager.log("RoomTrigger: This trigger is for room (" + str(room.x) + ", " + str(room.y) + ", " + str(room.w) + "x" + str(room.h) + ")", LogManager.CATEGORY_DUNGEON)
|
LogManager.log("RoomTrigger: This trigger is for room (" + str(room.x) + ", " + str(room.y) + ", " + str(room.w) + "x" + str(room.h) + ")", LogManager.CATEGORY_DUNGEON)
|
||||||
LogManager.log("RoomTrigger: Found " + str(doors_in_room.size()) + " doors in this room", LogManager.CATEGORY_DUNGEON)
|
LogManager.log("RoomTrigger: Found " + str(doors_in_room.size()) + " doors in this room", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
|
# Check if this is the exit room (has EXIT modifier) - trigger exit found notification
|
||||||
|
if room.has("modifiers") and room.modifiers is Array:
|
||||||
|
for modifier in room.modifiers:
|
||||||
|
if modifier is Dictionary and modifier.has("type") and modifier.type == "EXIT":
|
||||||
|
# This is the exit room - notify player to show alert and play sound
|
||||||
|
if player and is_instance_valid(player) and player.has_method("_on_exit_found"):
|
||||||
|
player._on_exit_found()
|
||||||
|
LogManager.log("RoomTrigger: Exit room entered! Player " + str(player.name) + " found the exit!", LogManager.CATEGORY_DUNGEON)
|
||||||
|
break
|
||||||
|
|
||||||
# Mark room as entered and update debug label
|
# Mark room as entered and update debug label
|
||||||
room_entered = true
|
room_entered = true
|
||||||
_update_debug_label()
|
_update_debug_label()
|
||||||
|
|||||||
96
src/scripts/select_class.gd
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
extends Node2D
|
||||||
|
# Race select: hover/click the race buttons to reveal and select. Keyboard (move_left/right, attack) still works.
|
||||||
|
# Dwarf preselected. Emits race_selected(race_name) when confirmed.
|
||||||
|
|
||||||
|
signal race_selected(race_name: String)
|
||||||
|
|
||||||
|
const RACES: Array[String] = ["Dwarf", "Elf", "Human"] # Wizard = Human
|
||||||
|
|
||||||
|
var selected_index: int = 0 # Dwarf preselected
|
||||||
|
var hovered_index: int = -1 # -1 = no hover
|
||||||
|
var _selection_confirmed: bool = false # One-shot: only first click counts (avoids overlapping buttons overwriting)
|
||||||
|
|
||||||
|
@onready var dwarf_node: Node2D = $Dwarf
|
||||||
|
@onready var elf_node: Node2D = $Elf
|
||||||
|
@onready var wizard_node: Node2D = $Wizard
|
||||||
|
@onready var button_dwarf: Button = $Control/ButtonDwarf
|
||||||
|
@onready var button_elf: Button = $Control/ButtonElf
|
||||||
|
@onready var button_wizard: Button = $Control/ButtonWizard
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var vp = get_viewport()
|
||||||
|
if vp:
|
||||||
|
position = vp.get_visible_rect().size / 2.0
|
||||||
|
_update_display()
|
||||||
|
_connect_buttons()
|
||||||
|
|
||||||
|
func _connect_buttons() -> void:
|
||||||
|
var buttons := [button_dwarf, button_elf, button_wizard]
|
||||||
|
for i in buttons.size():
|
||||||
|
var btn: Button = buttons[i]
|
||||||
|
if not btn:
|
||||||
|
continue
|
||||||
|
# Hover: show this race
|
||||||
|
btn.mouse_entered.connect(_on_race_button_mouse_entered.bind(i))
|
||||||
|
btn.mouse_exited.connect(_on_race_button_mouse_exited)
|
||||||
|
# Click: select this race and confirm
|
||||||
|
btn.pressed.connect(_on_race_button_pressed.bind(i))
|
||||||
|
|
||||||
|
func _on_race_button_mouse_entered(race_index: int) -> void:
|
||||||
|
hovered_index = race_index
|
||||||
|
_update_display()
|
||||||
|
|
||||||
|
func _on_race_button_mouse_exited() -> void:
|
||||||
|
hovered_index = -1
|
||||||
|
_update_display()
|
||||||
|
|
||||||
|
func _on_race_button_pressed(race_index: int) -> void:
|
||||||
|
# Use the clicked button's index directly (don't rely on selected_index - another button may have fired too)
|
||||||
|
selected_index = race_index
|
||||||
|
_confirm_selection_with_index(race_index)
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
var vp = get_viewport()
|
||||||
|
if not vp:
|
||||||
|
return
|
||||||
|
if event.is_action_pressed("move_left"):
|
||||||
|
selected_index = (selected_index - 1 + RACES.size()) % RACES.size()
|
||||||
|
hovered_index = -1
|
||||||
|
_update_display()
|
||||||
|
vp.set_input_as_handled()
|
||||||
|
if event.is_action_pressed("move_right"):
|
||||||
|
selected_index = (selected_index + 1) % RACES.size()
|
||||||
|
hovered_index = -1
|
||||||
|
_update_display()
|
||||||
|
vp.set_input_as_handled()
|
||||||
|
if event.is_action_pressed("attack"):
|
||||||
|
# Attack can be mouse1 or keyboard: use hovered race if mouse is over a button, else keyboard selection
|
||||||
|
var index_to_confirm := hovered_index if hovered_index >= 0 else selected_index
|
||||||
|
_confirm_selection_with_index(index_to_confirm)
|
||||||
|
vp.set_input_as_handled()
|
||||||
|
|
||||||
|
func _update_display() -> void:
|
||||||
|
var show_index: int = hovered_index if hovered_index >= 0 else selected_index
|
||||||
|
if dwarf_node:
|
||||||
|
dwarf_node.visible = (show_index == 0)
|
||||||
|
if elf_node:
|
||||||
|
elf_node.visible = (show_index == 1)
|
||||||
|
if wizard_node:
|
||||||
|
wizard_node.visible = (show_index == 2)
|
||||||
|
|
||||||
|
func _confirm_selection_with_index(clicked_index: int) -> void:
|
||||||
|
# Only process first confirmation (overlapping buttons can fire multiple signals; first click wins)
|
||||||
|
if _selection_confirmed:
|
||||||
|
return
|
||||||
|
_selection_confirmed = true
|
||||||
|
# Use the index from the button that was actually clicked (not selected_index which could be overwritten)
|
||||||
|
if clicked_index < 0 or clicked_index >= RACES.size():
|
||||||
|
clicked_index = selected_index
|
||||||
|
var race := RACES[clicked_index]
|
||||||
|
var gs = get_node_or_null("/root/GameState")
|
||||||
|
if gs and "selected_race" in gs:
|
||||||
|
gs.selected_race = race
|
||||||
|
print("SelectClass: joiner clicked index ", clicked_index, " -> race '", race, "', GameState.selected_race = '", gs.selected_race, "'")
|
||||||
|
else:
|
||||||
|
print("SelectClass: joiner clicked index ", clicked_index, " -> race '", race, "' but GameState not found")
|
||||||
|
race_selected.emit(race)
|
||||||
1
src/scripts/select_class.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cwbrfwrwt3krh
|
||||||
@@ -127,22 +127,12 @@ func _on_body_entered(body):
|
|||||||
# Add to hit_targets IMMEDIATELY to prevent multiple hits (mark as hit before processing)
|
# Add to hit_targets IMMEDIATELY to prevent multiple hits (mark as hit before processing)
|
||||||
hit_targets[body] = true
|
hit_targets[body] = true
|
||||||
|
|
||||||
# Deal damage to players - call RPC to let victim apply damage on their client
|
# Ignore other players - projectile passes through (no friendly fire)
|
||||||
if body.is_in_group("player") and body.has_method("rpc_take_damage"):
|
if body.is_in_group("player") and body.has_method("rpc_take_damage"):
|
||||||
$SfxImpact.play()
|
return
|
||||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
|
||||||
var player_peer_id = body.get_multiplayer_authority()
|
|
||||||
if player_peer_id != 0:
|
|
||||||
if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id():
|
|
||||||
body.rpc_take_damage(damage, attacker_pos)
|
|
||||||
else:
|
|
||||||
body.rpc_take_damage.rpc_id(player_peer_id, damage, attacker_pos)
|
|
||||||
else:
|
|
||||||
body.rpc_take_damage.rpc(damage, attacker_pos)
|
|
||||||
print("Staff projectile hit player: ", body.name, " for ", damage, " damage!")
|
|
||||||
|
|
||||||
# Deal damage to enemies - only authority (creator) deals damage
|
# Deal damage to enemies - only authority (creator) deals damage
|
||||||
elif body.is_in_group("enemy") and body.has_method("rpc_take_damage"):
|
if body.is_in_group("enemy") and body.has_method("rpc_take_damage"):
|
||||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
var attacker_pos = player_owner.global_position if player_owner else global_position
|
||||||
var is_crit = get_meta("is_crit") if has_meta("is_crit") else false
|
var is_crit = get_meta("is_crit") if has_meta("is_crit") else false
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ func _on_body_entered(body: Node2D):
|
|||||||
if body and body.is_in_group("player") and not body.is_dead:
|
if body and body.is_in_group("player") and not body.is_dead:
|
||||||
print("Stairs: Player entered stairs! Player: ", body.name)
|
print("Stairs: Player entered stairs! Player: ", body.name)
|
||||||
|
|
||||||
# Notify the player to show alert and play sound
|
|
||||||
if body and is_instance_valid(body) and body.has_method("_on_exit_found"):
|
|
||||||
body._on_exit_found()
|
|
||||||
|
|
||||||
# Play stairs sound effect
|
# Play stairs sound effect
|
||||||
if sfx_stairs and sfx_stairs.stream:
|
if sfx_stairs and sfx_stairs.stream:
|
||||||
sfx_stairs.play()
|
sfx_stairs.play()
|
||||||
|
|||||||
@@ -81,28 +81,26 @@ func _on_body_entered(body):
|
|||||||
# Add to hit_targets IMMEDIATELY to prevent multiple hits (mark as hit before processing)
|
# Add to hit_targets IMMEDIATELY to prevent multiple hits (mark as hit before processing)
|
||||||
hit_targets[body] = true
|
hit_targets[body] = true
|
||||||
|
|
||||||
# Deal damage to players - call RPC to let victim apply damage on their client
|
# Friendly fire: only skip when owner is also a player. Enemies can hit players.
|
||||||
# Pass the attacker's position (not projectile position) for accurate direction
|
|
||||||
if body.is_in_group("player") and body.has_method("rpc_take_damage"):
|
if body.is_in_group("player") and body.has_method("rpc_take_damage"):
|
||||||
$SfxImpact.play()
|
if player_owner and player_owner.is_in_group("player"):
|
||||||
|
return # Owner is player → pass through, no damage
|
||||||
|
# Owner is enemy → deal damage to player (same pattern as enemy_base)
|
||||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
var attacker_pos = player_owner.global_position if player_owner else global_position
|
||||||
var player_peer_id = body.get_multiplayer_authority()
|
var player_peer_id = body.get_multiplayer_authority()
|
||||||
if player_peer_id != 0:
|
if player_peer_id != 0:
|
||||||
# If target peer is the same as server (us), call directly
|
if multiplayer.get_unique_id() == player_peer_id:
|
||||||
# rpc_id() might not execute locally when called to same peer
|
body.rpc_take_damage(damage, attacker_pos, false, false)
|
||||||
if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id():
|
|
||||||
# Call directly on the same peer
|
|
||||||
body.rpc_take_damage(damage, attacker_pos)
|
|
||||||
else:
|
else:
|
||||||
# Send RPC to remote peer
|
body.rpc_take_damage.rpc_id(player_peer_id, damage, attacker_pos, false, false)
|
||||||
body.rpc_take_damage.rpc_id(player_peer_id, damage, attacker_pos)
|
|
||||||
else:
|
else:
|
||||||
# Fallback: broadcast if we can't get peer_id
|
body.rpc_take_damage.rpc(damage, attacker_pos, false, false)
|
||||||
body.rpc_take_damage.rpc(damage, attacker_pos)
|
if has_node("SfxImpact"):
|
||||||
print("Sword projectile hit player: ", body.name, " for ", damage, " damage!")
|
$SfxImpact.play()
|
||||||
|
return
|
||||||
|
|
||||||
# Deal damage to enemies - only authority (creator) deals damage
|
# Deal damage to enemies - only authority (creator) deals damage
|
||||||
elif body.is_in_group("enemy") and body.has_method("rpc_take_damage"):
|
if body.is_in_group("enemy") and body.has_method("rpc_take_damage"):
|
||||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
var attacker_pos = player_owner.global_position if player_owner else global_position
|
||||||
var is_crit = get_meta("is_crit") if has_meta("is_crit") else false
|
var is_crit = get_meta("is_crit") if has_meta("is_crit") else false
|
||||||
|
|
||||||
@@ -147,6 +145,16 @@ func _on_body_entered(body):
|
|||||||
else:
|
else:
|
||||||
body.rpc_take_damage.rpc(damage, attacker_pos, is_crit)
|
body.rpc_take_damage.rpc(damage, attacker_pos, is_crit)
|
||||||
|
|
||||||
|
# Slash hit effect (sync so all clients see it)
|
||||||
|
var hit_pos = body.global_position
|
||||||
|
if game_world and game_world.has_method("_sync_slash_hit_effect"):
|
||||||
|
if multiplayer.is_server():
|
||||||
|
game_world._sync_slash_hit_effect(hit_pos.x, hit_pos.y)
|
||||||
|
if game_world.has_method("_rpc_to_ready_peers"):
|
||||||
|
game_world._rpc_to_ready_peers("_sync_slash_hit_effect", [hit_pos.x, hit_pos.y])
|
||||||
|
else:
|
||||||
|
game_world._request_slash_hit_effect.rpc_id(1, hit_pos.x, hit_pos.y)
|
||||||
|
|
||||||
# Debug print - handle null player_owner safely
|
# Debug print - handle null player_owner safely
|
||||||
var owner_name: String = "none"
|
var owner_name: String = "none"
|
||||||
var is_authority: bool = false
|
var is_authority: bool = false
|
||||||
|
|||||||
@@ -64,13 +64,24 @@ func _on_body_entered(body):
|
|||||||
# Add to hit_targets IMMEDIATELY to prevent multiple hits (mark as hit before processing)
|
# Add to hit_targets IMMEDIATELY to prevent multiple hits (mark as hit before processing)
|
||||||
hit_targets[body] = true
|
hit_targets[body] = true
|
||||||
|
|
||||||
# Deal damage to players
|
# Friendly fire: only skip when owner is also a player. Enemies can hit players.
|
||||||
if body.is_in_group("player") and body.has_method("take_damage"):
|
if body.is_in_group("player") and body.has_method("take_damage"):
|
||||||
body.take_damage(damage, global_position)
|
if player_owner and player_owner.is_in_group("player"):
|
||||||
print("Sword hit player: ", body.name, " for ", damage, " damage!")
|
return # Owner is player → pass through, no damage
|
||||||
|
# Owner is enemy → deal damage to player
|
||||||
|
var attacker_pos = player_owner.global_position if player_owner and is_instance_valid(player_owner) else global_position
|
||||||
|
var player_peer_id = body.get_multiplayer_authority()
|
||||||
|
if player_peer_id != 0:
|
||||||
|
if multiplayer.get_unique_id() == player_peer_id:
|
||||||
|
body.rpc_take_damage(damage, attacker_pos, false, false)
|
||||||
|
else:
|
||||||
|
body.rpc_take_damage.rpc_id(player_peer_id, damage, attacker_pos, false, false)
|
||||||
|
else:
|
||||||
|
body.rpc_take_damage.rpc(damage, attacker_pos, false, false)
|
||||||
|
return
|
||||||
|
|
||||||
# Deal damage to boxes or other damageable objects
|
# Deal damage to boxes or other damageable objects
|
||||||
elif "health" in body:
|
if "health" in body:
|
||||||
# Boxes have health property
|
# Boxes have health property
|
||||||
body.health -= damage
|
body.health -= damage
|
||||||
if body.health <= 0 and body.has_method("_break_into_pieces"):
|
if body.health <= 0 and body.has_method("_break_into_pieces"):
|
||||||
|
|||||||
@@ -264,6 +264,38 @@ func _complete_disarm() -> void:
|
|||||||
# Change trap visual to show it's disarmed (optional - could fade out or change color)
|
# Change trap visual to show it's disarmed (optional - could fade out or change color)
|
||||||
sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
|
sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
|
||||||
|
|
||||||
|
# Grant EXP to all players for disarming trap (only on server)
|
||||||
|
# CRITICAL: Only server should grant EXP to avoid duplicates
|
||||||
|
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||||
|
var trap_exp_reward = 8.0 # EXP reward for disarming a trap
|
||||||
|
var all_players = get_tree().get_nodes_in_group("player")
|
||||||
|
var valid_players = []
|
||||||
|
for player in all_players:
|
||||||
|
if is_instance_valid(player) and player.character_stats:
|
||||||
|
valid_players.append(player)
|
||||||
|
|
||||||
|
if valid_players.size() > 0:
|
||||||
|
# Split EXP evenly among all players
|
||||||
|
var exp_per_player = trap_exp_reward / valid_players.size()
|
||||||
|
for player in valid_players:
|
||||||
|
player.character_stats.add_xp(exp_per_player)
|
||||||
|
LogManager.log("Trap disarmed: granted " + str(exp_per_player) + " EXP to " + str(player.name) + " (shared from " + str(trap_exp_reward) + " total)", LogManager.CATEGORY_ENEMY)
|
||||||
|
|
||||||
|
# Sync EXP to client if this player belongs to a client
|
||||||
|
var player_peer_id = player.get_multiplayer_authority()
|
||||||
|
if player_peer_id != 0 and player_peer_id != multiplayer.get_unique_id() and player.has_method("_sync_stats_update"):
|
||||||
|
var coins = player.character_stats.coin if "coin" in player.character_stats else 0
|
||||||
|
var xp = player.character_stats.xp if "xp" in player.character_stats else 0.0
|
||||||
|
player._sync_stats_update.rpc_id(player_peer_id, player.character_stats.kills, coins, xp)
|
||||||
|
|
||||||
|
# Show floating EXP text at trap position and sync to all clients
|
||||||
|
# Show locally first
|
||||||
|
_show_exp_number(exp_per_player, global_position)
|
||||||
|
# Sync to all clients via game_world
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_sync_exp_text_at_position") and multiplayer.has_multiplayer_peer():
|
||||||
|
game_world._sync_exp_text_at_position.rpc(exp_per_player, global_position)
|
||||||
|
|
||||||
# Sync disarm to all clients (including host when joiner disarms)
|
# Sync disarm to all clients (including host when joiner disarms)
|
||||||
if multiplayer.has_multiplayer_peer() and is_inside_tree() and is_instance_valid(self):
|
if multiplayer.has_multiplayer_peer() and is_inside_tree() and is_instance_valid(self):
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
@@ -291,6 +323,38 @@ func _sync_trap_disarmed() -> void:
|
|||||||
if activation_area:
|
if activation_area:
|
||||||
activation_area.monitoring = false
|
activation_area.monitoring = false
|
||||||
|
|
||||||
|
func _show_exp_number(amount: float, exp_pos: Vector2):
|
||||||
|
# Show EXP number (green, using dmg_numbers.png font) at position
|
||||||
|
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
||||||
|
if not damage_number_scene:
|
||||||
|
return
|
||||||
|
|
||||||
|
var exp_label = damage_number_scene.instantiate()
|
||||||
|
if not exp_label:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set text and color for EXP (green)
|
||||||
|
exp_label.label = "+" + str(int(amount)) + " EXP"
|
||||||
|
exp_label.color = Color(0.4, 1.0, 0.4) # Bright green
|
||||||
|
exp_label.z_index = 5
|
||||||
|
|
||||||
|
# Direction is straight up
|
||||||
|
exp_label.direction = Vector2(0, -1)
|
||||||
|
|
||||||
|
# Position at the specified location
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world:
|
||||||
|
var entities_node = game_world.get_node_or_null("Entities")
|
||||||
|
if entities_node:
|
||||||
|
entities_node.add_child(exp_label)
|
||||||
|
exp_label.global_position = exp_pos + Vector2(0, -20) # Slightly above
|
||||||
|
else:
|
||||||
|
get_tree().current_scene.add_child(exp_label)
|
||||||
|
exp_label.global_position = exp_pos + Vector2(0, -20)
|
||||||
|
else:
|
||||||
|
get_tree().current_scene.add_child(exp_label)
|
||||||
|
exp_label.global_position = exp_pos + Vector2(0, -20)
|
||||||
|
|
||||||
func _show_floating_text(text: String, color: Color) -> void:
|
func _show_floating_text(text: String, color: Color) -> void:
|
||||||
var floating_text_scene = preload("res://scenes/floating_text.tscn")
|
var floating_text_scene = preload("res://scenes/floating_text.tscn")
|
||||||
if floating_text_scene:
|
if floating_text_scene:
|
||||||
|
|||||||