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
|
||||||
@@ -15,12 +17,12 @@ var others_collection_delay = 5.0 # Other players wait 5 seconds
|
|||||||
|
|
||||||
# Flight duration based on charge
|
# Flight duration based on charge
|
||||||
var flight_timer = 0.0
|
var flight_timer = 0.0
|
||||||
var max_flight_duration = 6.0 # How long arrow flies before landing (set by charge)
|
var max_flight_duration = 6.0 # How long arrow flies before landing (set by charge)
|
||||||
var can_deal_damage = true # False after arrow "lands" in flight
|
var can_deal_damage = true # False after arrow "lands" in flight
|
||||||
|
|
||||||
var initiated_by: Node2D = null
|
var initiated_by: Node2D = null
|
||||||
var player_owner: Node = null # Like sword_projectile
|
var player_owner: Node = null # Like sword_projectile
|
||||||
var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup)
|
var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup)
|
||||||
|
|
||||||
# Collection area for wall-stuck arrows
|
# Collection area for wall-stuck arrows
|
||||||
var collection_area: Area2D = null
|
var collection_area: Area2D = null
|
||||||
@@ -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()
|
||||||
@@ -78,7 +80,7 @@ func shoot(shoot_direction: Vector2, start_pos: Vector2, owner_player: Node = nu
|
|||||||
# Speed: min 120, max 320, scales with charge % (0.5 to 1.0)
|
# Speed: min 120, max 320, scales with charge % (0.5 to 1.0)
|
||||||
var min_speed = 120.0
|
var min_speed = 120.0
|
||||||
var max_speed = 320.0
|
var max_speed = 320.0
|
||||||
speed = lerp(min_speed, max_speed, (charge_percentage - 0.5) * 2.0) # Map 0.5-1.0 to 0.0-1.0 range
|
speed = lerp(min_speed, max_speed, (charge_percentage - 0.5) * 2.0) # Map 0.5-1.0 to 0.0-1.0 range
|
||||||
|
|
||||||
# Flight duration: 50% charge = 0.5s, 100% charge = 2.5s
|
# Flight duration: 50% charge = 0.5s, 100% charge = 2.5s
|
||||||
max_flight_duration = lerp(0.5, 2.5, (charge_percentage - 0.5) * 2.0)
|
max_flight_duration = lerp(0.5, 2.5, (charge_percentage - 0.5) * 2.0)
|
||||||
@@ -91,12 +93,10 @@ 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
|
return # Exit early to prevent further movement this frame
|
||||||
print("Arrow landed after flying for ", flight_timer, " seconds")
|
|
||||||
return # Exit early to prevent further movement this frame
|
|
||||||
|
|
||||||
# Continue flying
|
# Continue flying
|
||||||
velocity = direction * speed
|
velocity = direction * speed
|
||||||
@@ -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
|
||||||
@@ -187,10 +173,10 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
# CRITICAL: Only the authority should process enemy collisions
|
# CRITICAL: Only the authority should process enemy collisions
|
||||||
# This ensures hit/miss/dodge calculations happen once and are consistent
|
# This ensures hit/miss/dodge calculations happen once and are consistent
|
||||||
if player_owner and not player_owner.is_multiplayer_authority():
|
if player_owner and not player_owner.is_multiplayer_authority():
|
||||||
return # Non-authority ignores enemy collisions
|
return # Non-authority ignores enemy collisions
|
||||||
|
|
||||||
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 damage = 20.0 # TODO: Get actual damage from player
|
var damage = 20.0 # TODO: Get actual damage from player
|
||||||
if player_owner and player_owner.character_stats:
|
if player_owner and player_owner.character_stats:
|
||||||
damage = player_owner.character_stats.damage
|
damage = player_owner.character_stats.damage
|
||||||
|
|
||||||
@@ -204,14 +190,11 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
if is_miss:
|
if is_miss:
|
||||||
# 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)
|
||||||
@@ -224,14 +207,11 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
if is_dodge:
|
if is_dodge:
|
||||||
# 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")
|
||||||
# Find target by name in Entities node
|
if gw and gw.has_method("_sync_arrow_hit") and multiplayer.has_multiplayer_peer():
|
||||||
var target = null
|
gw._sync_arrow_hit.rpc(arrow_name, target_name)
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
|
||||||
if game_world:
|
func _sync_arrow_miss_via_gameworld(arrow_name: String, target_name: String):
|
||||||
var entities_node = game_world.get_node_or_null("Entities")
|
# Route arrow miss sync through game_world to avoid node path issues
|
||||||
if entities_node:
|
if arrow_name.is_empty() or target_name.is_empty():
|
||||||
target = entities_node.get_node_or_null(target_name)
|
return
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
if not target:
|
if gw and gw.has_method("_sync_arrow_miss") and multiplayer.has_multiplayer_peer():
|
||||||
print("WARNING: Arrow hit target not found: ", target_name)
|
gw._sync_arrow_miss.rpc(arrow_name, target_name)
|
||||||
|
|
||||||
|
func _sync_arrow_dodge_via_gameworld(arrow_name: String, target_name: String):
|
||||||
|
# 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)
|
||||||
func _initialize_swing():
|
if has_node("AttackSwosh"):
|
||||||
var tween = create_tween()
|
$AttackSwosh.play()
|
||||||
var move_target = global_position + (direction.normalized() * slash_amount) # Moves in given direction
|
|
||||||
tween.set_trans(Tween.TRANS_CUBIC) # Smooth acceleration & deceleration
|
func setup(attack_direction: Vector2, owner_player: Node, _arc_direction: float = 1.0, axe_item: Item = null) -> void:
|
||||||
tween.set_ease(Tween.EASE_OUT) # Fast start, then slows down
|
player_owner = owner_player
|
||||||
tween.tween_property(self, "global_position", move_target, move_duration)
|
if owner_player and owner_player.character_stats:
|
||||||
'
|
damage = owner_player.character_stats.damage
|
||||||
# Create stretch tween (grow and shrink slightly)
|
# Use equipped axe texture and frame
|
||||||
var stretch_tween = create_tween()
|
if axe_item and sprite:
|
||||||
stretch_tween.set_trans(Tween.TRANS_CUBIC)
|
var tex = load(axe_item.spritePath) as Texture2D
|
||||||
stretch_tween.set_ease(Tween.EASE_OUT)
|
if tex:
|
||||||
stretch_tween.tween_property($Sprite2D, "scale", Vector2.ONE, move_duration / 2) # start normal
|
sprite.texture = tex
|
||||||
stretch_tween.tween_property($Sprite2D, "scale", stretch_amount, move_duration / 2)
|
sprite.hframes = axe_item.spriteFrames.x if axe_item.spriteFrames.x > 0 else 20
|
||||||
'
|
sprite.vframes = axe_item.spriteFrames.y if axe_item.spriteFrames.y > 0 else 14
|
||||||
|
sprite.frame = axe_item.spriteFrame
|
||||||
# Wait until mid-move to start fade
|
# Pick direction animation: 8 sectors by angle
|
||||||
await get_tree().create_timer(fade_delay).timeout
|
var dir_norm = attack_direction.normalized()
|
||||||
|
var angle = dir_norm.angle()
|
||||||
# Start fade-out effect
|
var sector = int(round(angle / (TAU / 8.0))) % 8
|
||||||
var fade_tween = create_tween()
|
if sector < 0:
|
||||||
fade_tween.tween_property($Sprite2D, "modulate:a", 0.0, fade_duration) # Fade to transparent
|
sector += 8
|
||||||
await fade_tween.finished
|
var anim_name = DIR_ANIMATIONS[sector] if sector < DIR_ANIMATIONS.size() else "attack_down"
|
||||||
queue_free()
|
if swing_animation and swing_animation.has_animation(anim_name):
|
||||||
pass
|
swing_animation.play(anim_name)
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
elapsed_time += delta
|
||||||
|
if player_owner and is_instance_valid(player_owner):
|
||||||
|
global_position = player_owner.global_position
|
||||||
|
if elapsed_time >= LIFETIME:
|
||||||
|
queue_free()
|
||||||
|
|
||||||
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():
|
||||||
else:
|
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:
|
||||||
|
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,25 +206,32 @@ 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!")
|
||||||
|
|
||||||
# Destroy wooden interactable objects (box, barrel, pot, etc.) – they burn and break
|
# Destroy wooden interactable objects (box, barrel, pot, etc.) – they burn and break
|
||||||
elif body.is_in_group("interactable_object") and body.has_method("take_fire_damage"):
|
elif body.is_in_group("interactable_object") and body.has_method("take_fire_damage"):
|
||||||
if "is_being_held" in body and body.is_being_held:
|
if "is_being_held" in body and body.is_being_held:
|
||||||
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!")
|
||||||
|
|
||||||
|
# Update bodies_in_area for next check
|
||||||
|
bodies_in_area = current_bodies
|
||||||
|
|
||||||
func _on_body_entered(_body):
|
func _on_body_entered(body):
|
||||||
# Track bodies that enter the area (for periodic damage)
|
# Track bodies that enter the area - they will get high damage on next periodic check
|
||||||
# Don't add to hit_targets here - we want to deal damage multiple times
|
# The _deal_periodic_damage function will detect they just entered and deal high damage
|
||||||
pass
|
if body and body != player_owner:
|
||||||
|
# Mark that this body just entered (will be processed in _deal_periodic_damage)
|
||||||
|
pass
|
||||||
|
|
||||||
func _on_lifetime_expired():
|
func _on_lifetime_expired():
|
||||||
# Spell expires - fade out and remove
|
# Spell expires - fade out and remove
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -4,17 +4,17 @@ extends CharacterBody2D
|
|||||||
|
|
||||||
@export var max_health: float = 50.0
|
@export var max_health: float = 50.0
|
||||||
@export var move_speed: float = 80.0
|
@export var move_speed: float = 80.0
|
||||||
@export var damage: float = 10.0 # Legacy - use character_stats.damage instead
|
@export var damage: float = 10.0 # Legacy - use character_stats.damage instead
|
||||||
@export var attack_cooldown: float = 1.0
|
@export var attack_cooldown: float = 1.0
|
||||||
@export var exp_reward: float = 10.0 # EXP granted when this enemy is defeated
|
@export var exp_reward: float = 10.0 # EXP granted when this enemy is defeated
|
||||||
|
|
||||||
var current_health: float = 50.0
|
var current_health: float = 50.0
|
||||||
var character_stats: CharacterStats # RPG stats system (same as players)
|
var character_stats: CharacterStats # RPG stats system (same as players)
|
||||||
var is_dead: bool = false
|
var is_dead: bool = false
|
||||||
@export var is_undead: bool = false # Zombies etc.; healing spell damages them
|
@export var is_undead: bool = false # Zombies etc.; healing spell damages them
|
||||||
var target_player: Node = null
|
var target_player: Node = null
|
||||||
var attack_timer: float = 0.0
|
var attack_timer: float = 0.0
|
||||||
var killer_player: Node = null # Track who killed this enemy (for kill credit)
|
var killer_player: Node = null # Track who killed this enemy (for kill credit)
|
||||||
|
|
||||||
# Knockback
|
# Knockback
|
||||||
var is_knocked_back: bool = false
|
var is_knocked_back: bool = false
|
||||||
@@ -23,11 +23,11 @@ var knockback_duration: float = 0.3
|
|||||||
var knockback_force: float = 125.0 # Scaled down for 1x scale
|
var knockback_force: float = 125.0 # Scaled down for 1x scale
|
||||||
|
|
||||||
# Burn debuff
|
# Burn debuff
|
||||||
var burn_debuff_timer: float = 0.0 # Timer for burn debuff
|
var burn_debuff_timer: float = 0.0 # Timer for burn debuff
|
||||||
var burn_debuff_duration: float = 5.0 # Burn lasts 5 seconds
|
var burn_debuff_duration: float = 5.0 # Burn lasts 5 seconds
|
||||||
var burn_debuff_damage_per_second: float = 1.0 # 1 HP per second
|
var burn_debuff_damage_per_second: float = 1.0 # 1 HP per second
|
||||||
var burn_debuff_visual: Node2D = null # Visual indicator for burn debuff
|
var burn_debuff_visual: Node2D = null # Visual indicator for burn debuff
|
||||||
var burn_damage_timer: float = 0.0 # Timer for burn damage ticks
|
var burn_damage_timer: float = 0.0 # Timer for burn damage ticks
|
||||||
|
|
||||||
# Z-axis for flying enemies
|
# Z-axis for flying enemies
|
||||||
var position_z: float = 0.0
|
var position_z: float = 0.0
|
||||||
@@ -109,7 +109,7 @@ func _physics_process(delta):
|
|||||||
var burn_sprite = burn_debuff_visual as Sprite2D
|
var burn_sprite = burn_debuff_visual as Sprite2D
|
||||||
var anim_timer = burn_sprite.get_meta("burn_animation_timer", 0.0)
|
var anim_timer = burn_sprite.get_meta("burn_animation_timer", 0.0)
|
||||||
anim_timer += delta
|
anim_timer += delta
|
||||||
if anim_timer >= 0.1: # ~10 FPS
|
if anim_timer >= 0.1: # ~10 FPS
|
||||||
anim_timer = 0.0
|
anim_timer = 0.0
|
||||||
var frame = burn_sprite.get_meta("burn_animation_frame", 0)
|
var frame = burn_sprite.get_meta("burn_animation_frame", 0)
|
||||||
frame = (frame + 1) % 16
|
frame = (frame + 1) % 16
|
||||||
@@ -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
|
||||||
@@ -179,7 +182,7 @@ func _physics_process(delta):
|
|||||||
var burn_sprite = burn_debuff_visual as Sprite2D
|
var burn_sprite = burn_debuff_visual as Sprite2D
|
||||||
var anim_timer = burn_sprite.get_meta("burn_animation_timer", 0.0)
|
var anim_timer = burn_sprite.get_meta("burn_animation_timer", 0.0)
|
||||||
anim_timer += delta
|
anim_timer += delta
|
||||||
if anim_timer >= 0.1: # ~10 FPS
|
if anim_timer >= 0.1: # ~10 FPS
|
||||||
anim_timer = 0.0
|
anim_timer = 0.0
|
||||||
var frame = burn_sprite.get_meta("burn_animation_frame", 0)
|
var frame = burn_sprite.get_meta("burn_animation_frame", 0)
|
||||||
frame = (frame + 1) % 16
|
frame = (frame + 1) % 16
|
||||||
@@ -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")
|
||||||
@@ -364,7 +375,7 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
|
|||||||
# This allows us to credit kills correctly
|
# This allows us to credit kills correctly
|
||||||
var nearest_player = _find_nearest_player_to_position(from_position)
|
var nearest_player = _find_nearest_player_to_position(from_position)
|
||||||
if nearest_player:
|
if nearest_player:
|
||||||
killer_player = nearest_player # Update killer to the most recent attacker
|
killer_player = nearest_player # Update killer to the most recent attacker
|
||||||
|
|
||||||
# Check for dodge chance (based on DEX) - same as players
|
# Check for dodge chance (based on DEX) - same as players
|
||||||
var _was_dodged = false
|
var _was_dodged = false
|
||||||
@@ -376,7 +387,7 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
|
|||||||
var dex_total = character_stats.baseStats.dex + character_stats.get_pass("dex")
|
var dex_total = character_stats.baseStats.dex + character_stats.get_pass("dex")
|
||||||
LogManager.log(str(name) + " DODGED the attack! (DEX: " + str(dex_total) + ", dodge chance: " + str(dodge_chance * 100.0) + "%)", LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " DODGED the attack! (DEX: " + str(dex_total) + ", dodge chance: " + str(dodge_chance * 100.0) + "%)", LogManager.CATEGORY_ENEMY)
|
||||||
# Show "DODGED" text
|
# Show "DODGED" text
|
||||||
_show_damage_number(0.0, from_position, false, false, true) # is_dodged = true
|
_show_damage_number(0.0, from_position, false, false, true) # is_dodged = true
|
||||||
# Sync dodge visual to clients (pass damage_amount=0.0 and is_dodged=true to indicate dodge)
|
# Sync dodge visual to clients (pass damage_amount=0.0 and is_dodged=true to indicate dodge)
|
||||||
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
|
||||||
@@ -384,13 +395,13 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
|
|||||||
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_enemy_damage_visual"):
|
if game_world and game_world.has_method("_sync_enemy_damage_visual"):
|
||||||
game_world._rpc_to_ready_peers("_sync_enemy_damage_visual", [enemy_name, enemy_index, 0.0, from_position, false, true])
|
game_world._rpc_to_ready_peers("_sync_enemy_damage_visual", [enemy_name, enemy_index, 0.0, from_position, false, true])
|
||||||
return # No damage taken, exit early
|
return # No damage taken, exit early
|
||||||
|
|
||||||
# If not dodged, apply damage with DEF reduction
|
# If not dodged, apply damage with DEF reduction
|
||||||
var actual_damage = amount
|
var actual_damage = amount
|
||||||
if character_stats:
|
if character_stats:
|
||||||
# Calculate damage after DEF reduction (critical hits pierce 80% of DEF)
|
# Calculate damage after DEF reduction (critical hits pierce 80% of DEF)
|
||||||
actual_damage = character_stats.calculate_damage(amount, false, is_critical) # false = not magical, is_critical = crit pierce
|
actual_damage = character_stats.calculate_damage(amount, false, is_critical) # false = not magical, is_critical = crit pierce
|
||||||
character_stats.modify_health(-actual_damage)
|
character_stats.modify_health(-actual_damage)
|
||||||
current_health = character_stats.hp
|
current_health = character_stats.hp
|
||||||
if character_stats.hp <= 0:
|
if character_stats.hp <= 0:
|
||||||
@@ -439,7 +450,7 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
|
|||||||
if current_health <= 0:
|
if current_health <= 0:
|
||||||
# Prevent multiple death triggers
|
# Prevent multiple death triggers
|
||||||
if is_dead:
|
if is_dead:
|
||||||
return # Already dying
|
return # Already dying
|
||||||
|
|
||||||
# Don't set is_dead here - let _die() set it to avoid early return bug
|
# Don't set is_dead here - let _die() set it to avoid early return bug
|
||||||
# Mark as dead in _die() function instead of here
|
# Mark as dead in _die() function instead of here
|
||||||
@@ -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
|
||||||
@@ -489,7 +499,7 @@ func _show_damage_number(amount: float, from_position: Vector2, is_critical: boo
|
|||||||
damage_label.color = Color.GRAY
|
damage_label.color = Color.GRAY
|
||||||
else:
|
else:
|
||||||
damage_label.label = str(int(amount))
|
damage_label.label = str(int(amount))
|
||||||
damage_label.color = Color(1.0, 0.6, 0.2) if is_critical else Color(1.0, 0.35, 0.35) # Bright orange / bright red
|
damage_label.color = Color(1.0, 0.6, 0.2) if is_critical else Color(1.0, 0.35, 0.35) # Bright orange / bright red
|
||||||
damage_label.z_index = 5
|
damage_label.z_index = 5
|
||||||
|
|
||||||
# Calculate direction from attacker (slight upward variation)
|
# Calculate direction from attacker (slight upward variation)
|
||||||
@@ -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:
|
||||||
@@ -565,7 +607,7 @@ func _apply_burn_debuff():
|
|||||||
if burn_debuff_timer > 0.0:
|
if burn_debuff_timer > 0.0:
|
||||||
# Already burning - refresh duration
|
# Already burning - refresh duration
|
||||||
burn_debuff_timer = burn_debuff_duration
|
burn_debuff_timer = burn_debuff_duration
|
||||||
burn_damage_timer = 0.0 # Reset damage timer
|
burn_damage_timer = 0.0 # Reset damage timer
|
||||||
LogManager.log(str(name) + " burn debuff refreshed", LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " burn debuff refreshed", LogManager.CATEGORY_ENEMY)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -597,7 +639,7 @@ func _create_burn_debuff_visual():
|
|||||||
add_child(burn_debuff_visual)
|
add_child(burn_debuff_visual)
|
||||||
# Position on enemy (centered)
|
# Position on enemy (centered)
|
||||||
burn_debuff_visual.position = Vector2(0, 0)
|
burn_debuff_visual.position = Vector2(0, 0)
|
||||||
burn_debuff_visual.z_index = 5 # Above enemy sprite
|
burn_debuff_visual.z_index = 5 # Above enemy sprite
|
||||||
LogManager.log(str(name) + " created burn debuff visual", LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " created burn debuff visual", LogManager.CATEGORY_ENEMY)
|
||||||
else:
|
else:
|
||||||
# Fallback: create simple sprite if scene doesn't exist
|
# Fallback: create simple sprite if scene doesn't exist
|
||||||
@@ -610,7 +652,7 @@ func _create_burn_debuff_visual():
|
|||||||
burn_sprite.vframes = 4
|
burn_sprite.vframes = 4
|
||||||
burn_sprite.frame = 0
|
burn_sprite.frame = 0
|
||||||
burn_sprite.position = Vector2(0, 0)
|
burn_sprite.position = Vector2(0, 0)
|
||||||
burn_sprite.z_index = 5 # Above enemy sprite
|
burn_sprite.z_index = 5 # Above enemy sprite
|
||||||
burn_sprite.set_meta("burn_animation_frame", 0)
|
burn_sprite.set_meta("burn_animation_frame", 0)
|
||||||
burn_sprite.set_meta("burn_animation_timer", 0.0)
|
burn_sprite.set_meta("burn_animation_timer", 0.0)
|
||||||
add_child(burn_sprite)
|
add_child(burn_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()
|
||||||
@@ -720,7 +790,7 @@ func _spawn_loot():
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Get killer's LCK stat to influence loot drops
|
# Get killer's LCK stat to influence loot drops
|
||||||
var killer_lck = 10.0 # Default LCK if no killer
|
var killer_lck = 10.0 # Default LCK if no killer
|
||||||
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_lck = killer_player.character_stats.baseStats.lck + killer_player.character_stats.get_pass("lck")
|
killer_lck = killer_player.character_stats.baseStats.lck + killer_player.character_stats.get_pass("lck")
|
||||||
LogManager.log(str(name) + " killed by " + str(killer_player.name) + " with LCK: " + str(killer_lck), LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " killed by " + str(killer_player.name) + " with LCK: " + str(killer_lck), LogManager.CATEGORY_ENEMY)
|
||||||
@@ -728,7 +798,7 @@ func _spawn_loot():
|
|||||||
# Random chance to drop loot (85% chance - increased from 70%)
|
# Random chance to drop loot (85% chance - increased from 70%)
|
||||||
# LCK can increase this: +0.01% per LCK point (capped at 95%)
|
# LCK can increase this: +0.01% per LCK point (capped at 95%)
|
||||||
var base_loot_chance = 0.85
|
var base_loot_chance = 0.85
|
||||||
var lck_bonus = min(killer_lck * 0.001, 0.1) # +0.1% per LCK, max +10% (95% cap)
|
var lck_bonus = min(killer_lck * 0.001, 0.1) # +0.1% per LCK, max +10% (95% cap)
|
||||||
var loot_chance = randf()
|
var loot_chance = randf()
|
||||||
var loot_threshold = 1.0 - (base_loot_chance + lck_bonus)
|
var loot_threshold = 1.0 - (base_loot_chance + lck_bonus)
|
||||||
LogManager.log(str(name) + " loot chance roll: " + str(loot_chance) + " (need > " + str(loot_threshold) + ", base=" + str(base_loot_chance) + ", LCK bonus=" + str(lck_bonus) + ")", LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " loot chance roll: " + str(loot_chance) + " (need > " + str(loot_threshold) + ", base=" + str(base_loot_chance) + ", LCK bonus=" + str(lck_bonus) + ")", LogManager.CATEGORY_ENEMY)
|
||||||
@@ -736,18 +806,18 @@ func _spawn_loot():
|
|||||||
# Determine how many loot items to drop (1-4 items, influenced by LCK)
|
# Determine how many loot items to drop (1-4 items, influenced by LCK)
|
||||||
# Base: 1-3 items, LCK can push towards 2-4 items
|
# Base: 1-3 items, LCK can push towards 2-4 items
|
||||||
# LCK effect: Each 5 points of LCK above 10 increases chance for extra drops
|
# LCK effect: Each 5 points of LCK above 10 increases chance for extra drops
|
||||||
var lck_modifier = (killer_lck - 10.0) / 5.0 # +1 per 5 LCK above 10
|
var lck_modifier = (killer_lck - 10.0) / 5.0 # +1 per 5 LCK above 10
|
||||||
var num_drops_roll = randf()
|
var num_drops_roll = randf()
|
||||||
var base_num_drops_roll = num_drops_roll - (lck_modifier * 0.1) # LCK reduces roll needed (up to -0.6 at LCK 40)
|
var base_num_drops_roll = num_drops_roll - (lck_modifier * 0.1) # LCK reduces roll needed (up to -0.6 at LCK 40)
|
||||||
var num_drops = 1
|
var num_drops = 1
|
||||||
if base_num_drops_roll < 0.5:
|
if base_num_drops_roll < 0.5:
|
||||||
num_drops = 1 # 50% base chance for 1 item (reduced from 60%)
|
num_drops = 1 # 50% base chance for 1 item (reduced from 60%)
|
||||||
elif base_num_drops_roll < 0.8:
|
elif base_num_drops_roll < 0.8:
|
||||||
num_drops = 2 # 30% base chance for 2 items
|
num_drops = 2 # 30% base chance for 2 items
|
||||||
elif base_num_drops_roll < 0.95:
|
elif base_num_drops_roll < 0.95:
|
||||||
num_drops = 3 # 15% base chance for 3 items
|
num_drops = 3 # 15% base chance for 3 items
|
||||||
else:
|
else:
|
||||||
num_drops = 4 # 5% base chance for 4 items (LCK makes this more likely)
|
num_drops = 4 # 5% base chance for 4 items (LCK makes this more likely)
|
||||||
|
|
||||||
# Ensure at least 1 drop
|
# Ensure at least 1 drop
|
||||||
num_drops = max(1, num_drops)
|
num_drops = max(1, num_drops)
|
||||||
@@ -768,13 +838,14 @@ func _spawn_loot():
|
|||||||
for i in range(num_drops):
|
for i in range(num_drops):
|
||||||
# Decide what to drop for this item, influenced by LCK
|
# Decide what to drop for this item, influenced by LCK
|
||||||
# LCK makes better items more likely: reduces coin chance, increases item chance
|
# LCK makes better items more likely: reduces coin chance, increases item chance
|
||||||
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
|
||||||
@@ -785,7 +856,7 @@ func _spawn_loot():
|
|||||||
var drop_roll = randf()
|
var drop_roll = randf()
|
||||||
var loot_type = 0
|
var loot_type = 0
|
||||||
var drop_item = false
|
var drop_item = false
|
||||||
var item_rarity_boost = false # LCK can boost item rarity
|
var item_rarity_boost = false # LCK can boost item rarity
|
||||||
|
|
||||||
if drop_roll < coin_chance:
|
if drop_roll < coin_chance:
|
||||||
# Coin
|
# Coin
|
||||||
@@ -827,12 +898,12 @@ func _spawn_loot():
|
|||||||
|
|
||||||
# Create unique seed for this loot item: dungeon_seed + loot_id
|
# Create unique seed for this loot item: dungeon_seed + loot_id
|
||||||
# This ensures each loot item gets a unique but deterministic seed
|
# This ensures each loot item gets a unique but deterministic seed
|
||||||
var loot_seed = base_seed + loot_id + 10000 # Offset to avoid collisions
|
var loot_seed = base_seed + loot_id + 10000 # Offset to avoid collisions
|
||||||
loot_rng.seed = loot_seed
|
loot_rng.seed = loot_seed
|
||||||
|
|
||||||
var random_angle = loot_rng.randf() * PI * 2
|
var random_angle = loot_rng.randf() * PI * 2
|
||||||
var random_force = loot_rng.randf_range(25.0, 50.0) # Reduced to half speed
|
var random_force = loot_rng.randf_range(25.0, 50.0) # Reduced to half speed
|
||||||
var random_velocity_z = loot_rng.randf_range(40.0, 60.0) # Reduced to half speed
|
var random_velocity_z = loot_rng.randf_range(40.0, 60.0) # Reduced to half speed
|
||||||
|
|
||||||
# Generate initial velocity (same on all clients via RPC)
|
# Generate initial velocity (same on all clients via RPC)
|
||||||
var initial_velocity = Vector2(cos(random_angle), sin(random_angle)) * random_force
|
var initial_velocity = Vector2(cos(random_angle), sin(random_angle)) * random_force
|
||||||
@@ -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
|
if item_type_roll < 0.8:
|
||||||
# Roll for rarity with LCK bonus: each 5 LCK above 15 increases rare/epic chance
|
# Consumable drop (arrows, bombs, restoration items)
|
||||||
var rarity_roll = randf()
|
item = ItemDatabase.get_random_consumable_drop()
|
||||||
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
|
|
||||||
var common_threshold = max(0.0, 0.3 - lck_rarity_bonus)
|
|
||||||
var uncommon_threshold = max(common_threshold, 0.65 - (lck_rarity_bonus * 0.5))
|
|
||||||
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:
|
|
||||||
# Epic/Consumable (greatly increased by LCK)
|
|
||||||
var epic_roll = randf()
|
|
||||||
if epic_roll < 0.5:
|
|
||||||
item = ItemDatabase.get_random_item_by_rarity(ItemDatabase.ItemRarity.EPIC)
|
|
||||||
else:
|
|
||||||
item = ItemDatabase.get_random_item_by_rarity(ItemDatabase.ItemRarity.CONSUMABLE)
|
|
||||||
else:
|
else:
|
||||||
# Normal LCK: use standard enemy drop weights
|
# Equipment drop (much rarer - only 20% of item drops, which is 20% of 15% = 3% total)
|
||||||
item = ItemDatabase.get_random_enemy_drop()
|
# LCK boost still applies - higher LCK makes equipment drops more likely to be better quality
|
||||||
|
item = ItemDatabase.get_random_equipment_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():
|
||||||
@@ -908,8 +959,8 @@ func _spawn_loot():
|
|||||||
loot_rng.seed = real_loot_seed
|
loot_rng.seed = real_loot_seed
|
||||||
# Regenerate velocity with correct seed
|
# Regenerate velocity with correct seed
|
||||||
var real_random_angle = loot_rng.randf() * PI * 2
|
var real_random_angle = loot_rng.randf() * PI * 2
|
||||||
var real_random_force = loot_rng.randf_range(25.0, 50.0) # Reduced to half speed
|
var real_random_force = loot_rng.randf_range(25.0, 50.0) # Reduced to half speed
|
||||||
var real_random_velocity_z = loot_rng.randf_range(40.0, 60.0) # Reduced to half speed
|
var real_random_velocity_z = loot_rng.randf_range(40.0, 60.0) # Reduced to half speed
|
||||||
initial_velocity = Vector2(cos(real_random_angle), sin(real_random_angle)) * real_random_force
|
initial_velocity = Vector2(cos(real_random_angle), sin(real_random_angle)) * real_random_force
|
||||||
random_velocity_z = real_random_velocity_z
|
random_velocity_z = real_random_velocity_z
|
||||||
# Update loot with correct velocity
|
# Update loot with correct velocity
|
||||||
@@ -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:
|
||||||
@@ -1015,8 +1066,8 @@ func _sync_death():
|
|||||||
var current_state = get("state")
|
var current_state = get("state")
|
||||||
# SlimeState enum: IDLE=0, MOVING=1, JUMPING=2, DAMAGED=3, DYING=4
|
# SlimeState enum: IDLE=0, MOVING=1, JUMPING=2, DAMAGED=3, DYING=4
|
||||||
# Set state to DYING (4) if it's currently DAMAGED (3) or less
|
# Set state to DYING (4) if it's currently DAMAGED (3) or less
|
||||||
if current_state <= 3: # DAMAGED or less
|
if current_state <= 3: # DAMAGED or less
|
||||||
set("state", 4) # Set to DYING
|
set("state", 4) # Set to DYING
|
||||||
LogManager.log(str(name) + " (client) set state to DYING (4) in _sync_death to override DAMAGED state", LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " (client) set state to DYING (4) in _sync_death to override DAMAGED state", LogManager.CATEGORY_ENEMY)
|
||||||
|
|
||||||
# For humanoid enemies, ensure death animation is set immediately and animation state is reset
|
# For humanoid enemies, ensure death animation is set immediately and animation state is reset
|
||||||
|
|||||||
@@ -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] = []
|
||||||
@@ -15,9 +15,9 @@ var grab_cooldown_timer: float = 0.0
|
|||||||
const RANDOM_MOVE_INTERVAL: float = 1.2
|
const RANDOM_MOVE_INTERVAL: float = 1.2
|
||||||
const SNATCH_DURATION: float = 0.4
|
const SNATCH_DURATION: float = 0.4
|
||||||
const SNATCH_DAMAGE: float = 12.0
|
const SNATCH_DAMAGE: float = 12.0
|
||||||
const GRAB_COOLDOWN: float = 2.0 # Cooldown after grabbing before can grab again
|
const GRAB_COOLDOWN: float = 2.0 # Cooldown after grabbing before can grab again
|
||||||
const TILE_SIZE: int = 16
|
const TILE_SIZE: int = 16
|
||||||
const TILE_STRIDE: int = 17 # 16 + separation 1
|
const TILE_STRIDE: int = 17 # 16 + separation 1
|
||||||
@onready var emerge_area: Area2D = $EmergeArea
|
@onready var emerge_area: Area2D = $EmergeArea
|
||||||
@onready var grab_area: Area2D = $GrabPlayerArea
|
@onready var grab_area: Area2D = $GrabPlayerArea
|
||||||
@onready var interest_area: Area2D = $PlayerInterestArea
|
@onready var interest_area: Area2D = $PlayerInterestArea
|
||||||
@@ -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
|
||||||
@@ -163,18 +163,18 @@ func _spawn_hand_pieces():
|
|||||||
|
|
||||||
# 4 quadrants: top-left, top-right, bottom-left, bottom-right
|
# 4 quadrants: top-left, top-right, bottom-left, bottom-right
|
||||||
var regions = [
|
var regions = [
|
||||||
Rect2(base_x, base_y, half_width, half_height), # Top-left
|
Rect2(base_x, base_y, half_width, half_height), # Top-left
|
||||||
Rect2(base_x + half_width, base_y, half_width, half_height), # Top-right
|
Rect2(base_x + half_width, base_y, half_width, half_height), # Top-right
|
||||||
Rect2(base_x, base_y + half_height, half_width, half_height), # Bottom-left
|
Rect2(base_x, base_y + half_height, half_width, half_height), # Bottom-left
|
||||||
Rect2(base_x + half_width, base_y + half_height, half_width, half_height) # Bottom-right
|
Rect2(base_x + half_width, base_y + half_height, half_width, half_height) # Bottom-right
|
||||||
]
|
]
|
||||||
|
|
||||||
# 4 directions: up-left, up-right, down-left, down-right
|
# 4 directions: up-left, up-right, down-left, down-right
|
||||||
var directions = [
|
var directions = [
|
||||||
Vector2(-1, -1).normalized(), # Up-left
|
Vector2(-1, -1).normalized(), # Up-left
|
||||||
Vector2(1, -1).normalized(), # Up-right
|
Vector2(1, -1).normalized(), # Up-right
|
||||||
Vector2(-1, 1).normalized(), # Down-left
|
Vector2(-1, 1).normalized(), # Down-left
|
||||||
Vector2(1, 1).normalized() # Down-right
|
Vector2(1, 1).normalized() # Down-right
|
||||||
]
|
]
|
||||||
|
|
||||||
# Spawn 4 pieces
|
# Spawn 4 pieces
|
||||||
@@ -195,11 +195,11 @@ func _spawn_hand_pieces():
|
|||||||
|
|
||||||
# Fly in the direction for this piece
|
# Fly in the direction for this piece
|
||||||
var direction = directions[i]
|
var direction = directions[i]
|
||||||
var speed = randf_range(200.0, 300.0) # Fast enough to see them fly
|
var speed = randf_range(200.0, 300.0) # Fast enough to see them fly
|
||||||
p.velocity = direction * speed
|
p.velocity = direction * speed
|
||||||
p.angular_velocity = randf_range(-10.0, 10.0)
|
p.angular_velocity = randf_range(-10.0, 10.0)
|
||||||
p.position_z = 0.0
|
p.position_z = 0.0
|
||||||
p.velocity_z = randf_range(80.0, 140.0) # Upward burst, then gravity
|
p.velocity_z = randf_range(80.0, 140.0) # Upward burst, then gravity
|
||||||
|
|
||||||
# Use call_deferred to avoid physics query flush errors
|
# Use call_deferred to avoid physics query flush errors
|
||||||
parent.call_deferred("add_child", p)
|
parent.call_deferred("add_child", p)
|
||||||
@@ -217,7 +217,7 @@ func _ai_behavior(delta: float) -> void:
|
|||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
# Update grabbed player position to follow hand (slightly above)
|
# Update grabbed player position to follow hand (slightly above)
|
||||||
if grabbed_player and is_instance_valid(grabbed_player):
|
if grabbed_player and is_instance_valid(grabbed_player):
|
||||||
var target_pos = global_position + Vector2(0, -12) # Slightly above the hand
|
var target_pos = global_position + Vector2(0, -12) # Slightly above the hand
|
||||||
# Smoothly move player to hand position (only on authority)
|
# Smoothly move player to hand position (only on authority)
|
||||||
if is_multiplayer_authority():
|
if is_multiplayer_authority():
|
||||||
grabbed_player.global_position = grabbed_player.global_position.lerp(target_pos, delta * 8.0)
|
grabbed_player.global_position = grabbed_player.global_position.lerp(target_pos, delta * 8.0)
|
||||||
@@ -300,7 +300,7 @@ func _on_grab_player_area_body_entered(body: Node2D) -> void:
|
|||||||
if grabbed_player != null:
|
if grabbed_player != null:
|
||||||
return
|
return
|
||||||
if grab_cooldown_timer > 0.0:
|
if grab_cooldown_timer > 0.0:
|
||||||
return # Still on cooldown from previous grab
|
return # Still on cooldown from previous grab
|
||||||
if not is_multiplayer_authority():
|
if not is_multiplayer_authority():
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -327,7 +327,7 @@ func _on_grab_player_area_body_entered(body: Node2D) -> void:
|
|||||||
func _sync_hand_emerged():
|
func _sync_hand_emerged():
|
||||||
# Sync hand emergence visibility to clients
|
# Sync hand emergence visibility to clients
|
||||||
if is_multiplayer_authority():
|
if is_multiplayer_authority():
|
||||||
return # Authority already handled it locally
|
return # Authority already handled it locally
|
||||||
|
|
||||||
if state == HandState.HIDDEN:
|
if state == HandState.HIDDEN:
|
||||||
state = HandState.EMERGING
|
state = HandState.EMERGING
|
||||||
@@ -341,7 +341,7 @@ func _finish_snatch() -> void:
|
|||||||
var victim = grabbed_player
|
var victim = grabbed_player
|
||||||
grabbed_player = null
|
grabbed_player = null
|
||||||
state = HandState.IDLE
|
state = HandState.IDLE
|
||||||
grab_cooldown_timer = GRAB_COOLDOWN # Start cooldown after grab
|
grab_cooldown_timer = GRAB_COOLDOWN # Start cooldown after grab
|
||||||
if anim_player and anim_player.has_animation("idle"):
|
if anim_player and anim_player.has_animation("idle"):
|
||||||
anim_player.play("idle")
|
anim_player.play("idle")
|
||||||
if not is_instance_valid(victim):
|
if not is_instance_valid(victim):
|
||||||
@@ -402,14 +402,14 @@ func _spawn_emerge_tile_particles():
|
|||||||
# Get the tile at the hand's position
|
# Get the tile at the hand's position
|
||||||
var cell = center_cell
|
var cell = center_cell
|
||||||
if layer.get_cell_source_id(cell) < 0:
|
if layer.get_cell_source_id(cell) < 0:
|
||||||
return # No tile at this position
|
return # No tile at this position
|
||||||
|
|
||||||
var atlas = layer.get_cell_atlas_coords(cell)
|
var atlas = layer.get_cell_atlas_coords(cell)
|
||||||
var world = layer.map_to_local(cell) + layer.global_position
|
var world = layer.map_to_local(cell) + layer.global_position
|
||||||
|
|
||||||
var bx = atlas.x * TILE_STRIDE
|
var bx = atlas.x * TILE_STRIDE
|
||||||
var by = atlas.y * TILE_STRIDE
|
var by = atlas.y * TILE_STRIDE
|
||||||
var h = 8.0 # TILE_SIZE / 2
|
var h = 8.0 # TILE_SIZE / 2
|
||||||
var regions = [
|
var regions = [
|
||||||
Rect2(bx, by, h, h),
|
Rect2(bx, by, h, h),
|
||||||
Rect2(bx + h, by, h, h),
|
Rect2(bx + h, by, h, h),
|
||||||
@@ -434,11 +434,11 @@ func _spawn_emerge_tile_particles():
|
|||||||
# Particles fly outward in random directions (less intense than bomb)
|
# Particles fly outward in random directions (less intense than bomb)
|
||||||
var angle = randf() * TAU
|
var angle = randf() * TAU
|
||||||
var d = Vector2(cos(angle), sin(angle))
|
var d = Vector2(cos(angle), sin(angle))
|
||||||
var speed = randf_range(150.0, 250.0) # Slower than bomb explosion
|
var speed = randf_range(150.0, 250.0) # Slower than bomb explosion
|
||||||
p.velocity = d * speed
|
p.velocity = d * speed
|
||||||
p.angular_velocity = randf_range(-8.0, 8.0)
|
p.angular_velocity = randf_range(-8.0, 8.0)
|
||||||
p.position_z = 0.0
|
p.position_z = 0.0
|
||||||
p.velocity_z = randf_range(60.0, 120.0) # Upward burst, then gravity
|
p.velocity_z = randf_range(60.0, 120.0) # Upward burst, then gravity
|
||||||
# Use call_deferred to avoid physics query flush errors
|
# Use call_deferred to avoid physics query flush errors
|
||||||
parent.call_deferred("add_child", p)
|
parent.call_deferred("add_child", p)
|
||||||
|
|
||||||
|
|||||||
@@ -39,39 +39,39 @@ var can_attack: bool = true
|
|||||||
var is_attacking: bool = false
|
var is_attacking: bool = false
|
||||||
var is_charging_attack: bool = false
|
var is_charging_attack: bool = false
|
||||||
var attack_charge_time: float = 0.0
|
var attack_charge_time: float = 0.0
|
||||||
var base_attack_charge_time: float = 0.4 # Base charge time before attack
|
var base_attack_charge_time: float = 0.4 # Base charge time before attack
|
||||||
var dex: int = 10 # Dexterity stat (affects attack speed)
|
var dex: int = 10 # Dexterity stat (affects attack speed)
|
||||||
var blood_scene = preload("res://scenes/blood_clot.tscn")
|
var blood_scene = preload("res://scenes/blood_clot.tscn")
|
||||||
|
|
||||||
# Bow charge visual effect (pulsing)
|
# Bow charge visual effect (pulsing)
|
||||||
var bow_charge_tint: Color = Color(1.2, 1.2, 1.2, 1.0) # White tint when fully charged
|
var bow_charge_tint: Color = Color(1.2, 1.2, 1.2, 1.0) # White tint when fully charged
|
||||||
var bow_charge_tint_pulse_time: float = 0.0 # Timer for pulsing tint animation
|
var bow_charge_tint_pulse_time: float = 0.0 # Timer for pulsing tint animation
|
||||||
var bow_charge_tint_pulse_speed_charged: float = 20.0 # Speed of tint pulse animation when fully charged
|
var bow_charge_tint_pulse_speed_charged: float = 20.0 # Speed of tint pulse animation when fully charged
|
||||||
var original_sprite_tints: Dictionary = {} # Store original tint values for restoration
|
var original_sprite_tints: Dictionary = {} # Store original tint values for restoration
|
||||||
|
|
||||||
# Loadout (player-like abilities) — some humanoids have bow, bomb, spell, shield, lift/throw
|
# Loadout (player-like abilities) — some humanoids have bow, bomb, spell, shield, lift/throw
|
||||||
var has_bow: bool = false
|
var has_bow: bool = false
|
||||||
var arrows_left: int = 0
|
var arrows_left: int = 0
|
||||||
var has_bomb: bool = false
|
var has_bomb: bool = false
|
||||||
var bombs_left: int = 0
|
var bombs_left: int = 0
|
||||||
var spell_type: String = "" # "flames" | "frost" | "healing" | ""
|
var spell_type: String = "" # "flames" | "frost" | "healing" | ""
|
||||||
var has_shield: bool = false
|
var has_shield: bool = false
|
||||||
var shield_block_chance: float = 0.0
|
var shield_block_chance: float = 0.0
|
||||||
var is_blocking: bool = false # Whether enemy is actively blocking with shield
|
var is_blocking: bool = false # Whether enemy is actively blocking with shield
|
||||||
var shield_block_speed_multiplier: float = 0.5 # Movement speed multiplier when blocking
|
var shield_block_speed_multiplier: float = 0.5 # Movement speed multiplier when blocking
|
||||||
var shield_block_timer: float = 0.0 # Timer for how long to keep blocking
|
var shield_block_timer: float = 0.0 # Timer for how long to keep blocking
|
||||||
var shield_block_duration: float = 1.5 # How long to block after raising shield
|
var shield_block_duration: float = 1.5 # How long to block after raising shield
|
||||||
var can_lift_throw: bool = false
|
var can_lift_throw: bool = false
|
||||||
var spell_cooldown_timer: float = 0.0
|
var spell_cooldown_timer: float = 0.0
|
||||||
var bomb_cooldown_timer: float = 0.0
|
var bomb_cooldown_timer: float = 0.0
|
||||||
var lift_throw_cooldown_timer: float = 0.0
|
var lift_throw_cooldown_timer: float = 0.0
|
||||||
var ranged_decision_timer: float = 0.0 # Roll for bow/bomb/spell every 0.4s
|
var ranged_decision_timer: float = 0.0 # Roll for bow/bomb/spell every 0.4s
|
||||||
var attack_arrow_scene: PackedScene = preload("res://scenes/attack_arrow.tscn")
|
var attack_arrow_scene: PackedScene = preload("res://scenes/attack_arrow.tscn")
|
||||||
var attack_bomb_scene: PackedScene = preload("res://scenes/attack_bomb.tscn")
|
var attack_bomb_scene: PackedScene = preload("res://scenes/attack_bomb.tscn")
|
||||||
var flame_spell_scene: PackedScene = preload("res://scenes/attack_spell_flame.tscn")
|
var flame_spell_scene: PackedScene = preload("res://scenes/attack_spell_flame.tscn")
|
||||||
var frostspike_spell_scene: PackedScene = preload("res://scenes/attack_spell_frostspike.tscn")
|
var frostspike_spell_scene: PackedScene = preload("res://scenes/attack_spell_frostspike.tscn")
|
||||||
var interactable_object_scene: PackedScene = preload("res://scenes/interactable_object.tscn")
|
var interactable_object_scene: PackedScene = preload("res://scenes/interactable_object.tscn")
|
||||||
var held_bomb_object: Node = null # Bomb object held above enemy's head before throwing
|
var held_bomb_object: Node = null # Bomb object held above enemy's head before throwing
|
||||||
|
|
||||||
# AI state
|
# AI state
|
||||||
enum AIState {IDLE, WANDERING, NOTICED, CHASING, ATTACKING, GROUPING, BOW_CHARGING, THROWING_BOMB, CASTING_SPELL, LIFTING}
|
enum AIState {IDLE, WANDERING, NOTICED, CHASING, ATTACKING, GROUPING, BOW_CHARGING, THROWING_BOMB, CASTING_SPELL, LIFTING}
|
||||||
@@ -225,7 +225,7 @@ func _ready():
|
|||||||
super._ready()
|
super._ready()
|
||||||
|
|
||||||
# CRITICAL: Ensure collision mask is set correctly (walls are on layer 7 = bit 6 = 64)
|
# CRITICAL: Ensure collision mask is set correctly (walls are on layer 7 = bit 6 = 64)
|
||||||
collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
|
collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
|
||||||
|
|
||||||
# Override sprite reference (we use layered sprites, not single sprite)
|
# Override sprite reference (we use layered sprites, not single sprite)
|
||||||
sprite = null # Don't use base class sprite
|
sprite = null # Don't use base class sprite
|
||||||
@@ -361,7 +361,7 @@ func _randomize_appearance():
|
|||||||
Color(0.2, 0.2, 0.9), # Bright blue
|
Color(0.2, 0.2, 0.9), # Bright blue
|
||||||
Color(0.9, 0.4, 0.6), # Hot pink
|
Color(0.9, 0.4, 0.6), # Hot pink
|
||||||
Color(0.5, 0.2, 0.8), # Deep purple
|
Color(0.5, 0.2, 0.8), # Deep purple
|
||||||
Color(0.9, 0.6, 0.1) # Amber
|
Color(0.9, 0.6, 0.1) # Amber
|
||||||
]
|
]
|
||||||
var hair_color = hair_colors[appearance_rng.randi() % hair_colors.size()]
|
var hair_color = hair_colors[appearance_rng.randi() % hair_colors.size()]
|
||||||
_set_hair_color(hair_color)
|
_set_hair_color(hair_color)
|
||||||
@@ -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()
|
||||||
@@ -701,7 +700,7 @@ func _load_random_gloves():
|
|||||||
# Only load gloves if we don't already have clothes
|
# Only load gloves if we don't already have clothes
|
||||||
# This prevents gloves from overriding clothes
|
# This prevents gloves from overriding clothes
|
||||||
if sprite_armour.texture:
|
if sprite_armour.texture:
|
||||||
return # Already have clothes, skip gloves
|
return # Already have clothes, skip gloves
|
||||||
|
|
||||||
# Available gloves
|
# Available gloves
|
||||||
var gloves = [
|
var gloves = [
|
||||||
@@ -872,60 +871,60 @@ func _setup_stats():
|
|||||||
# Set stats based on type
|
# Set stats based on type
|
||||||
match humanoid_type:
|
match humanoid_type:
|
||||||
HumanoidType.CYCLOPS:
|
HumanoidType.CYCLOPS:
|
||||||
max_health = 55.0 # Reduced from 100.0 for better balance
|
max_health = 55.0 # Reduced from 100.0 for better balance
|
||||||
move_speed = 40.0
|
move_speed = 40.0
|
||||||
damage = 15.0
|
damage = 15.0
|
||||||
dex = 8 # Slow, strong
|
dex = 8 # Slow, strong
|
||||||
exp_reward = 25.0 # Strong enemies give more EXP
|
exp_reward = 25.0 # Strong enemies give more EXP
|
||||||
HumanoidType.DEMON:
|
HumanoidType.DEMON:
|
||||||
max_health = 45.0 # Reduced from 80.0 for better balance
|
max_health = 45.0 # Reduced from 80.0 for better balance
|
||||||
move_speed = 45.0
|
move_speed = 45.0
|
||||||
damage = 12.0
|
damage = 12.0
|
||||||
dex = 12 # Medium speed
|
dex = 12 # Medium speed
|
||||||
exp_reward = 20.0
|
exp_reward = 20.0
|
||||||
HumanoidType.HUMANOID:
|
HumanoidType.HUMANOID:
|
||||||
max_health = 35.0 # Reduced from 60.0 for better balance
|
max_health = 35.0 # Reduced from 60.0 for better balance
|
||||||
move_speed = 50.0
|
move_speed = 50.0
|
||||||
damage = 10.0
|
damage = 10.0
|
||||||
dex = 15 # Fast, agile
|
dex = 15 # Fast, agile
|
||||||
exp_reward = 15.0
|
exp_reward = 15.0
|
||||||
HumanoidType.NIGHTELF:
|
HumanoidType.NIGHTELF:
|
||||||
max_health = 40.0 # Reduced from 70.0 for better balance
|
max_health = 40.0 # Reduced from 70.0 for better balance
|
||||||
move_speed = 55.0
|
move_speed = 55.0
|
||||||
damage = 11.0
|
damage = 11.0
|
||||||
dex = 18 # Very fast
|
dex = 18 # Very fast
|
||||||
exp_reward = 18.0
|
exp_reward = 18.0
|
||||||
HumanoidType.GOBLIN:
|
HumanoidType.GOBLIN:
|
||||||
max_health = 25.0 # Reduced from 40.0 for better balance
|
max_health = 25.0 # Reduced from 40.0 for better balance
|
||||||
move_speed = 60.0
|
move_speed = 60.0
|
||||||
damage = 8.0
|
damage = 8.0
|
||||||
dex = 20 # Very fast, weak
|
dex = 20 # Very fast, weak
|
||||||
exp_reward = 10.0 # Weak enemies give less EXP
|
exp_reward = 10.0 # Weak enemies give less EXP
|
||||||
HumanoidType.ORC:
|
HumanoidType.ORC:
|
||||||
max_health = 50.0 # Reduced from 90.0 for better balance
|
max_health = 50.0 # Reduced from 90.0 for better balance
|
||||||
move_speed = 42.0
|
move_speed = 42.0
|
||||||
damage = 14.0
|
damage = 14.0
|
||||||
dex = 7 # Slow, very strong
|
dex = 7 # Slow, very strong
|
||||||
exp_reward = 22.0
|
exp_reward = 22.0
|
||||||
HumanoidType.SKELETON:
|
HumanoidType.SKELETON:
|
||||||
max_health = 30.0 # Reduced from 50.0 for better balance
|
max_health = 30.0 # Reduced from 50.0 for better balance
|
||||||
move_speed = 48.0
|
move_speed = 48.0
|
||||||
damage = 9.0
|
damage = 9.0
|
||||||
dex = 14 # Medium-fast
|
dex = 14 # Medium-fast
|
||||||
exp_reward = 12.0
|
exp_reward = 12.0
|
||||||
|
|
||||||
current_health = max_health
|
current_health = max_health
|
||||||
|
|
||||||
# Calculate attack cooldown based on DEX (higher DEX = faster attacks)
|
# Calculate attack cooldown based on DEX (higher DEX = faster attacks)
|
||||||
# Base cooldown of 1.5s, reduced by DEX (max reduction to 0.5s at DEX 20)
|
# Base cooldown of 1.5s, reduced by DEX (max reduction to 0.5s at DEX 20)
|
||||||
var dex_multiplier = 1.0 - (dex - 5) * 0.05 # Each point of DEX above 5 reduces cooldown by 5%
|
var dex_multiplier = 1.0 - (dex - 5) * 0.05 # Each point of DEX above 5 reduces cooldown by 5%
|
||||||
dex_multiplier = clamp(dex_multiplier, 0.33, 1.0) # Clamp between 0.33x (3x faster) and 1.0x
|
dex_multiplier = clamp(dex_multiplier, 0.33, 1.0) # Clamp between 0.33x (3x faster) and 1.0x
|
||||||
attack_cooldown = 1.5 * dex_multiplier
|
attack_cooldown = 1.5 * dex_multiplier
|
||||||
|
|
||||||
# Calculate attack charge time based on DEX (higher DEX = shorter charge)
|
# Calculate attack charge time based on DEX (higher DEX = shorter charge)
|
||||||
# Base charge of 0.4s, reduced by DEX (min charge of 0.15s at DEX 20)
|
# Base charge of 0.4s, reduced by DEX (min charge of 0.15s at DEX 20)
|
||||||
var charge_multiplier = 1.0 - (dex - 5) * 0.02 # Each point of DEX above 5 reduces charge by 2%
|
var charge_multiplier = 1.0 - (dex - 5) * 0.02 # Each point of DEX above 5 reduces charge by 2%
|
||||||
charge_multiplier = clamp(charge_multiplier, 0.375, 1.0) # Clamp between 0.375x (2.67x faster) and 1.0x
|
charge_multiplier = clamp(charge_multiplier, 0.375, 1.0) # Clamp between 0.375x (2.67x faster) and 1.0x
|
||||||
base_attack_charge_time = 0.4 * charge_multiplier
|
base_attack_charge_time = 0.4 * charge_multiplier
|
||||||
|
|
||||||
LogManager.log(str(name) + " stats: DEX=" + str(dex) + " attack_cooldown=" + str(attack_cooldown) + " charge_time=" + str(base_attack_charge_time), LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " stats: DEX=" + str(dex) + " attack_cooldown=" + str(attack_cooldown) + " charge_time=" + str(base_attack_charge_time), LogManager.CATEGORY_ENEMY)
|
||||||
@@ -1235,7 +1234,7 @@ func _chasing_behavior(delta_arg):
|
|||||||
if has_shield and shield_block_chance > 0:
|
if has_shield and shield_block_chance > 0:
|
||||||
# Check if player is attacking (recently attacked or in melee range)
|
# Check if player is attacking (recently attacked or in melee range)
|
||||||
var player_is_attacking = false
|
var player_is_attacking = false
|
||||||
if dist < 60.0: # Close enough that player might attack
|
if dist < 60.0: # Close enough that player might attack
|
||||||
# Check if player is facing us and might be attacking
|
# Check if player is facing us and might be attacking
|
||||||
if "is_attacking" in target_player and target_player.is_attacking:
|
if "is_attacking" in target_player and target_player.is_attacking:
|
||||||
player_is_attacking = true
|
player_is_attacking = true
|
||||||
@@ -1281,12 +1280,12 @@ func _chasing_behavior(delta_arg):
|
|||||||
attack_charge_time = base_attack_charge_time * 1.2
|
attack_charge_time = base_attack_charge_time * 1.2
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
current_direction = _get_direction_from_vector(to_player)
|
current_direction = _get_direction_from_vector(to_player)
|
||||||
bow_charge_tint_pulse_time = 0.0 # Reset pulse timer
|
bow_charge_tint_pulse_time = 0.0 # Reset pulse timer
|
||||||
return
|
return
|
||||||
elif has_bomb and bombs_left > 0 and bomb_cooldown_timer <= 0 and dist >= 48 and dist <= 130:
|
elif has_bomb and bombs_left > 0 and bomb_cooldown_timer <= 0 and dist >= 48 and dist <= 130:
|
||||||
if randf() < 0.12:
|
if randf() < 0.12:
|
||||||
ai_state = AIState.THROWING_BOMB
|
ai_state = AIState.THROWING_BOMB
|
||||||
state_timer = 2.0 # Increased timer: 1.0s to hold bomb visible, 1.0s for throw animation
|
state_timer = 2.0 # Increased timer: 1.0s to hold bomb visible, 1.0s for throw animation
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
current_direction = _get_direction_from_vector(to_player)
|
current_direction = _get_direction_from_vector(to_player)
|
||||||
# Create bomb object above enemy's head
|
# Create bomb object above enemy's head
|
||||||
@@ -1323,7 +1322,7 @@ func _chasing_behavior(delta_arg):
|
|||||||
|
|
||||||
if dist > desired_distance:
|
if dist > desired_distance:
|
||||||
# Still too far - chase player
|
# Still too far - chase player
|
||||||
velocity = to_player * move_speed * 0.8 * speed_mult # Apply blocking speed reduction
|
velocity = to_player * move_speed * 0.8 * speed_mult # Apply blocking speed reduction
|
||||||
else:
|
else:
|
||||||
# Close enough to attack - but only stop if we can attack soon
|
# Close enough to attack - but only stop if we can attack soon
|
||||||
# If attack is on cooldown, keep following at reduced speed to maintain distance
|
# If attack is on cooldown, keep following at reduced speed to maintain distance
|
||||||
@@ -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:
|
||||||
@@ -1400,7 +1399,7 @@ func _attacking_behavior(delta):
|
|||||||
# Set idle animation during charge
|
# Set idle animation during charge
|
||||||
if current_animation != "IDLE" and current_animation != "SWORD" and current_animation != "DAMAGE":
|
if current_animation != "IDLE" and current_animation != "SWORD" and current_animation != "DAMAGE":
|
||||||
_set_animation("IDLE")
|
_set_animation("IDLE")
|
||||||
return # Don't return to chasing yet
|
return # Don't return to chasing yet
|
||||||
|
|
||||||
# Return to chasing after attack completes
|
# Return to chasing after attack completes
|
||||||
# Check if attack animation is done (not in SWORD animation anymore) and cooldown is over
|
# Check if attack animation is done (not in SWORD animation anymore) and cooldown is over
|
||||||
@@ -1490,7 +1489,7 @@ func _perform_bow_attack():
|
|||||||
|
|
||||||
# Fire multiple arrows in a volley (2-3 arrows)
|
# Fire multiple arrows in a volley (2-3 arrows)
|
||||||
var num_arrows = randi_range(2, 3)
|
var num_arrows = randi_range(2, 3)
|
||||||
num_arrows = min(num_arrows, arrows_left) # Don't fire more than available
|
num_arrows = min(num_arrows, arrows_left) # Don't fire more than available
|
||||||
|
|
||||||
var base_dir = _get_attack_direction_vector()
|
var base_dir = _get_attack_direction_vector()
|
||||||
var charge_pct = 0.65 + randf() * 0.25
|
var charge_pct = 0.65 + randf() * 0.25
|
||||||
@@ -1499,7 +1498,7 @@ func _perform_bow_attack():
|
|||||||
for i in range(num_arrows):
|
for i in range(num_arrows):
|
||||||
var dir = base_dir
|
var dir = base_dir
|
||||||
# Add spread to arrows (cone pattern)
|
# Add spread to arrows (cone pattern)
|
||||||
var spread_angle = deg_to_rad(randf_range(-12.0, 12.0)) # ±12 degrees spread
|
var spread_angle = deg_to_rad(randf_range(-12.0, 12.0)) # ±12 degrees spread
|
||||||
dir = dir.rotated(spread_angle)
|
dir = dir.rotated(spread_angle)
|
||||||
# Add additional aim error
|
# Add additional aim error
|
||||||
dir = _add_aim_error(dir, randf_range(16.0, 28.0))
|
dir = _add_aim_error(dir, randf_range(16.0, 28.0))
|
||||||
@@ -1535,12 +1534,12 @@ func _throwing_bomb_behavior(delta):
|
|||||||
# Update held bomb position above enemy's head
|
# Update held bomb position above enemy's head
|
||||||
if held_bomb_object and is_instance_valid(held_bomb_object):
|
if held_bomb_object and is_instance_valid(held_bomb_object):
|
||||||
# Position bomb above enemy's head (offset upward)
|
# Position bomb above enemy's head (offset upward)
|
||||||
var head_offset = Vector2(0, -20) # Above the head
|
var head_offset = Vector2(0, -20) # Above the head
|
||||||
held_bomb_object.global_position = global_position + head_offset
|
held_bomb_object.global_position = global_position + head_offset
|
||||||
# Make sure bomb is visible and on top
|
# Make sure bomb is visible and on top
|
||||||
if held_bomb_object.has_node("Sprite2D"):
|
if held_bomb_object.has_node("Sprite2D"):
|
||||||
held_bomb_object.get_node("Sprite2D").visible = true
|
held_bomb_object.get_node("Sprite2D").visible = true
|
||||||
held_bomb_object.z_index = 10 # Above enemy sprites
|
held_bomb_object.z_index = 10 # Above enemy sprites
|
||||||
|
|
||||||
# Face the player while holding bomb
|
# Face the player while holding bomb
|
||||||
current_direction = _get_direction_from_vector((target_player.global_position - global_position).normalized())
|
current_direction = _get_direction_from_vector((target_player.global_position - global_position).normalized())
|
||||||
@@ -1589,8 +1588,8 @@ func _create_held_bomb_object():
|
|||||||
|
|
||||||
var bomb_obj = interactable_object_scene.instantiate()
|
var bomb_obj = interactable_object_scene.instantiate()
|
||||||
bomb_obj.name = "EnemyHeldBomb_" + str(Time.get_ticks_msec())
|
bomb_obj.name = "EnemyHeldBomb_" + str(Time.get_ticks_msec())
|
||||||
bomb_obj.global_position = global_position + Vector2(0, -20) # Above head
|
bomb_obj.global_position = global_position + Vector2(0, -20) # Above head
|
||||||
bomb_obj.z_index = 10 # Above enemy sprites
|
bomb_obj.z_index = 10 # Above enemy sprites
|
||||||
|
|
||||||
# Set multiplayer authority
|
# Set multiplayer authority
|
||||||
if multiplayer.has_multiplayer_peer():
|
if multiplayer.has_multiplayer_peer():
|
||||||
@@ -1605,7 +1604,7 @@ func _create_held_bomb_object():
|
|||||||
bomb_obj.set_collision_layer_value(2, false)
|
bomb_obj.set_collision_layer_value(2, false)
|
||||||
bomb_obj.set_collision_mask_value(1, false)
|
bomb_obj.set_collision_mask_value(1, false)
|
||||||
bomb_obj.set_collision_mask_value(2, false)
|
bomb_obj.set_collision_mask_value(2, false)
|
||||||
bomb_obj.set_collision_mask_value(7, true) # Keep wall collision
|
bomb_obj.set_collision_mask_value(7, true) # Keep wall collision
|
||||||
|
|
||||||
# Make sure sprite is visible
|
# Make sure sprite is visible
|
||||||
if bomb_obj.has_node("Sprite2D"):
|
if bomb_obj.has_node("Sprite2D"):
|
||||||
@@ -1630,8 +1629,14 @@ func _throw_held_bomb():
|
|||||||
var par = get_parent()
|
var par = get_parent()
|
||||||
if par:
|
if par:
|
||||||
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:
|
||||||
@@ -1757,7 +1762,7 @@ func _perform_attack():
|
|||||||
|
|
||||||
can_attack = false
|
can_attack = false
|
||||||
is_attacking = true
|
is_attacking = true
|
||||||
is_charging_attack = false # Reset charging flag
|
is_charging_attack = false # Reset charging flag
|
||||||
|
|
||||||
# CRITICAL: Set attack timer for cooldown (this will reset can_attack when it expires)
|
# CRITICAL: Set attack timer for cooldown (this will reset can_attack when it expires)
|
||||||
attack_timer = attack_cooldown
|
attack_timer = attack_cooldown
|
||||||
@@ -1766,7 +1771,7 @@ func _perform_attack():
|
|||||||
_set_animation("SWORD")
|
_set_animation("SWORD")
|
||||||
|
|
||||||
# Set state timer to allow attack animation to complete before returning to chasing
|
# Set state timer to allow attack animation to complete before returning to chasing
|
||||||
state_timer = attack_cooldown + 0.3 # Give extra time for attack animation
|
state_timer = attack_cooldown + 0.3 # Give extra time for attack animation
|
||||||
|
|
||||||
# Calculate attack direction
|
# Calculate attack direction
|
||||||
var attack_direction = Vector2.ZERO
|
var attack_direction = Vector2.ZERO
|
||||||
@@ -1883,7 +1888,7 @@ func _update_animation(delta):
|
|||||||
elif "IDLE" in ANIMATIONS:
|
elif "IDLE" in ANIMATIONS:
|
||||||
current_animation = "IDLE"
|
current_animation = "IDLE"
|
||||||
else:
|
else:
|
||||||
return # Can't update animation without valid animation
|
return # Can't update animation without valid animation
|
||||||
|
|
||||||
time_since_last_frame += delta
|
time_since_last_frame += delta
|
||||||
if time_since_last_frame >= ANIMATIONS[current_animation]["frameDurations"][current_frame] / 1000.0:
|
if time_since_last_frame >= ANIMATIONS[current_animation]["frameDurations"][current_frame] / 1000.0:
|
||||||
@@ -2036,7 +2041,7 @@ func _apply_bow_charge_tint():
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Calculate pulse value (0.0 to 1.0) using sine wave
|
# Calculate pulse value (0.0 to 1.0) using sine wave
|
||||||
var pulse_value = (sin(bow_charge_tint_pulse_time) + 1.0) / 2.0 # 0.0 to 1.0
|
var pulse_value = (sin(bow_charge_tint_pulse_time) + 1.0) / 2.0 # 0.0 to 1.0
|
||||||
|
|
||||||
for sprite_data in sprite_layers:
|
for sprite_data in sprite_layers:
|
||||||
var sprite_layer = sprite_data.sprite
|
var sprite_layer = sprite_data.sprite
|
||||||
@@ -2071,7 +2076,7 @@ func _apply_bow_charge_tint():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Interpolate between original and charged tint based on pulse
|
# Interpolate between original and charged tint based on pulse
|
||||||
var current_tint = original_tint.lerp(full_charged_tint, pulse_value * 0.5) # 50% pulse intensity
|
var current_tint = original_tint.lerp(full_charged_tint, pulse_value * 0.5) # 50% pulse intensity
|
||||||
shader_material.set_shader_parameter("tint", Vector4(current_tint.r, current_tint.g, current_tint.b, current_tint.a))
|
shader_material.set_shader_parameter("tint", Vector4(current_tint.r, current_tint.g, current_tint.b, current_tint.a))
|
||||||
|
|
||||||
func _clear_bow_charge_tint():
|
func _clear_bow_charge_tint():
|
||||||
@@ -2173,7 +2178,7 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
|
|||||||
# Attack not blocked, but raise shield anyway if we have one (defensive reaction)
|
# Attack not blocked, but raise shield anyway if we have one (defensive reaction)
|
||||||
if not is_blocking:
|
if not is_blocking:
|
||||||
is_blocking = true
|
is_blocking = true
|
||||||
shield_block_timer = shield_block_duration * 0.8 # Shorter duration for reactive blocking
|
shield_block_timer = shield_block_duration * 0.8 # Shorter duration for reactive blocking
|
||||||
_update_shield_visibility()
|
_update_shield_visibility()
|
||||||
if sfx_activate_shield:
|
if sfx_activate_shield:
|
||||||
sfx_activate_shield.play()
|
sfx_activate_shield.play()
|
||||||
|
|||||||
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,17 +17,22 @@ 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
|
||||||
var last_join_address: String = ""
|
var last_join_address: String = ""
|
||||||
var room_fetch_timer: Timer = null # Timer for retrying room fetches
|
var room_fetch_timer: Timer = null # Timer for retrying room fetches
|
||||||
var is_auto_joining: bool = false # Track if we're in auto-join mode
|
var is_auto_joining: bool = false # Track if we're in auto-join mode
|
||||||
var is_hosting: bool = false # Track if we're hosting (don't fetch rooms when hosting)
|
var is_hosting: bool = false # Track if we're hosting (don't fetch rooms when hosting)
|
||||||
var room_list_container: VBoxContainer = null # Container for displaying available rooms
|
var room_list_container: VBoxContainer = null # Container for displaying available rooms
|
||||||
var refresh_button: Button = null # Refresh button for manually reloading rooms
|
var refresh_button: Button = null # Refresh button for manually reloading rooms
|
||||||
var refresh_cooldown_timer: Timer = null # Timer for refresh button cooldown
|
var refresh_cooldown_timer: Timer = null # Timer for refresh button cooldown
|
||||||
var active_room_join_button: Button = null # Join button we're currently using (reset on fail)
|
var active_room_join_button: Button = null # Join button we're currently using (reset on fail)
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
# Wait for nodes to be ready
|
# Wait for nodes to be ready
|
||||||
@@ -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:
|
||||||
@@ -87,7 +95,7 @@ func _ready():
|
|||||||
# If WebRTC is selected at startup (not auto-joining and not hosting), fetch rooms
|
# If WebRTC is selected at startup (not auto-joining and not hosting), fetch rooms
|
||||||
if not is_auto_joining and not is_hosting:
|
if not is_auto_joining and not is_hosting:
|
||||||
var current_mode = network_manager.network_mode
|
var current_mode = network_manager.network_mode
|
||||||
if current_mode == 1: # WebRTC
|
if current_mode == 1: # WebRTC
|
||||||
_start_room_fetch()
|
_start_room_fetch()
|
||||||
|
|
||||||
func _check_command_line_args():
|
func _check_command_line_args():
|
||||||
@@ -99,7 +107,7 @@ func _check_command_line_args():
|
|||||||
var should_join = false
|
var should_join = false
|
||||||
var should_debug = false
|
var should_debug = false
|
||||||
var force_webrtc = false
|
var force_webrtc = false
|
||||||
var join_address = "" # Empty by default - will fetch rooms if WebRTC/WebSocket
|
var join_address = "" # Empty by default - will fetch rooms if WebRTC/WebSocket
|
||||||
var local_count = 1
|
var local_count = 1
|
||||||
|
|
||||||
for arg in args:
|
for arg in args:
|
||||||
@@ -119,17 +127,35 @@ 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)
|
||||||
|
|
||||||
# Force WebRTC mode if --webrtc flag is present
|
# Force WebRTC mode if --webrtc flag is present
|
||||||
if force_webrtc:
|
if force_webrtc:
|
||||||
network_manager.set_network_mode(1) # WebRTC
|
network_manager.set_network_mode(1) # WebRTC
|
||||||
if network_mode_option:
|
if network_mode_option:
|
||||||
if OS.get_name() == "Web":
|
if OS.get_name() == "Web":
|
||||||
network_mode_option.selected = 0 # WebRTC is first option on web
|
network_mode_option.selected = 0 # WebRTC is first option on web
|
||||||
else:
|
else:
|
||||||
network_mode_option.selected = 1 # WebRTC is second option on native
|
network_mode_option.selected = 1 # WebRTC is second option on native
|
||||||
_on_network_mode_changed(network_mode_option.selected)
|
_on_network_mode_changed(network_mode_option.selected)
|
||||||
LogManager.log("GameUI: WebRTC mode forced via --webrtc flag", LogManager.CATEGORY_UI)
|
LogManager.log("GameUI: WebRTC mode forced via --webrtc flag", LogManager.CATEGORY_UI)
|
||||||
|
|
||||||
@@ -142,33 +168,39 @@ func _check_command_line_args():
|
|||||||
|
|
||||||
# Auto-start based on arguments
|
# Auto-start based on arguments
|
||||||
if should_host:
|
if should_host:
|
||||||
is_hosting = true # Set flag so we don't fetch rooms
|
is_hosting = true # Set flag so we don't fetch rooms
|
||||||
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")
|
||||||
network_manager.set_local_player_count(local_count)
|
if gs and gs.skip_race_select:
|
||||||
is_auto_joining = true
|
# --race= was passed: skip race select and auto-join immediately
|
||||||
is_joining_attempt = true # Mark as joining attempt so connection failure handler works
|
LogManager.log("Auto-joining: No address provided, fetching available rooms (mode: " + str(current_mode) + ", race: " + gs.selected_race + ")...", LogManager.CATEGORY_UI)
|
||||||
# Create timer for retrying room fetches
|
network_manager.set_local_player_count(local_count)
|
||||||
room_fetch_timer = Timer.new()
|
is_auto_joining = true
|
||||||
room_fetch_timer.name = "RoomFetchTimer"
|
is_joining_attempt = true
|
||||||
room_fetch_timer.wait_time = 2.0 # Retry every 2 seconds
|
room_fetch_timer = Timer.new()
|
||||||
room_fetch_timer.timeout.connect(_retry_room_fetch)
|
room_fetch_timer.name = "RoomFetchTimer"
|
||||||
room_fetch_timer.autostart = false
|
room_fetch_timer.wait_time = 2.0
|
||||||
add_child(room_fetch_timer)
|
room_fetch_timer.timeout.connect(_retry_room_fetch)
|
||||||
# Connect to rooms_fetched signal (not one-shot, so we can keep retrying)
|
room_fetch_timer.autostart = false
|
||||||
if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join):
|
add_child(room_fetch_timer)
|
||||||
network_manager.rooms_fetched.connect(_on_rooms_fetched_auto_join)
|
if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join):
|
||||||
# Show room fetch status UI and start fetching
|
network_manager.rooms_fetched.connect(_on_rooms_fetched_auto_join)
|
||||||
_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
|
||||||
@@ -191,7 +223,7 @@ func _on_rooms_fetched_display(rooms: Array):
|
|||||||
# Only handle if not in auto-join mode (auto-join has its own handler)
|
# Only handle if not in auto-join mode (auto-join has its own handler)
|
||||||
if is_auto_joining:
|
if is_auto_joining:
|
||||||
LogManager.log("GameUI: Ignoring rooms_fetched_display - still in auto-join mode", LogManager.CATEGORY_UI)
|
LogManager.log("GameUI: Ignoring rooms_fetched_display - still in auto-join mode", LogManager.CATEGORY_UI)
|
||||||
return # Let auto-join handler take care of it
|
return # Let auto-join handler take care of it
|
||||||
|
|
||||||
LogManager.log("GameUI: Received rooms for display: " + str(rooms.size()) + " rooms", LogManager.CATEGORY_UI)
|
LogManager.log("GameUI: Received rooms for display: " + str(rooms.size()) + " rooms", LogManager.CATEGORY_UI)
|
||||||
|
|
||||||
@@ -224,7 +256,7 @@ func _on_rooms_fetched_display(rooms: Array):
|
|||||||
func _on_rooms_fetched_auto_join(rooms: Array):
|
func _on_rooms_fetched_auto_join(rooms: Array):
|
||||||
"""Auto-join the first available room when --join --webrtc is used without address"""
|
"""Auto-join the first available room when --join --webrtc is used without address"""
|
||||||
if not is_auto_joining:
|
if not is_auto_joining:
|
||||||
return # Not in auto-join mode, ignore
|
return # Not in auto-join mode, ignore
|
||||||
|
|
||||||
# Hide loading indicator - request completed
|
# Hide loading indicator - request completed
|
||||||
_hide_loading_indicator()
|
_hide_loading_indicator()
|
||||||
@@ -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:
|
||||||
@@ -304,7 +356,7 @@ func _retry_room_fetch():
|
|||||||
func _start_room_fetch():
|
func _start_room_fetch():
|
||||||
"""Start fetching rooms and show loading indicator"""
|
"""Start fetching rooms and show loading indicator"""
|
||||||
# Only fetch if WebRTC mode and not hosting
|
# Only fetch if WebRTC mode and not hosting
|
||||||
if network_manager.network_mode != 1: # Not WebRTC
|
if network_manager.network_mode != 1: # Not WebRTC
|
||||||
return
|
return
|
||||||
|
|
||||||
if is_hosting or network_manager.is_hosting:
|
if is_hosting or network_manager.is_hosting:
|
||||||
@@ -357,7 +409,7 @@ func _hide_room_fetch_status():
|
|||||||
func _create_refresh_button():
|
func _create_refresh_button():
|
||||||
"""Create a refresh button for manually reloading the room list"""
|
"""Create a refresh button for manually reloading the room list"""
|
||||||
if refresh_button:
|
if refresh_button:
|
||||||
return # Already exists
|
return # Already exists
|
||||||
|
|
||||||
if not room_fetch_status_container:
|
if not room_fetch_status_container:
|
||||||
return
|
return
|
||||||
@@ -448,7 +500,7 @@ func _update_last_fetch_time():
|
|||||||
func _create_room_list_container():
|
func _create_room_list_container():
|
||||||
"""Create the container for displaying available rooms"""
|
"""Create the container for displaying available rooms"""
|
||||||
if room_list_container:
|
if room_list_container:
|
||||||
return # Already exists
|
return # Already exists
|
||||||
|
|
||||||
if not room_fetch_status_container:
|
if not room_fetch_status_container:
|
||||||
return
|
return
|
||||||
@@ -456,7 +508,7 @@ func _create_room_list_container():
|
|||||||
# Create a ScrollContainer for the room list
|
# Create a ScrollContainer for the room list
|
||||||
var scroll_container = ScrollContainer.new()
|
var scroll_container = ScrollContainer.new()
|
||||||
scroll_container.name = "RoomListScrollContainer"
|
scroll_container.name = "RoomListScrollContainer"
|
||||||
scroll_container.custom_minimum_size = Vector2(0, 150) # Set a reasonable height
|
scroll_container.custom_minimum_size = Vector2(0, 150) # Set a reasonable height
|
||||||
scroll_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
scroll_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
|
||||||
# Create VBoxContainer inside scroll container
|
# Create VBoxContainer inside scroll container
|
||||||
@@ -579,7 +631,7 @@ func _on_network_mode_changed(index: int):
|
|||||||
var actual_mode: int
|
var actual_mode: int
|
||||||
if OS.get_name() == "Web":
|
if OS.get_name() == "Web":
|
||||||
# Web builds: 0 = WebRTC, 1 = WebSocket
|
# Web builds: 0 = WebRTC, 1 = WebSocket
|
||||||
actual_mode = index + 1 # Map 0->1 (WebRTC), 1->2 (WebSocket)
|
actual_mode = index + 1 # Map 0->1 (WebRTC), 1->2 (WebSocket)
|
||||||
else:
|
else:
|
||||||
# Native builds: 0 = ENet, 1 = WebRTC, 2 = WebSocket
|
# Native builds: 0 = ENet, 1 = WebRTC, 2 = WebSocket
|
||||||
actual_mode = index
|
actual_mode = index
|
||||||
@@ -589,18 +641,18 @@ func _on_network_mode_changed(index: int):
|
|||||||
# Update address input placeholder based on mode
|
# Update address input placeholder based on mode
|
||||||
if address_input:
|
if address_input:
|
||||||
match actual_mode:
|
match actual_mode:
|
||||||
0: # ENet
|
0: # ENet
|
||||||
address_input.placeholder_text = "Server IP or domain"
|
address_input.placeholder_text = "Server IP or domain"
|
||||||
1: # WebRTC
|
1: # WebRTC
|
||||||
address_input.placeholder_text = "Enter Room Code (e.g., ABC123)"
|
address_input.placeholder_text = "Enter Room Code (e.g., ABC123)"
|
||||||
2: # WebSocket
|
2: # WebSocket
|
||||||
address_input.placeholder_text = "Enter Room Code (e.g., ABC123)"
|
address_input.placeholder_text = "Enter Room Code (e.g., ABC123)"
|
||||||
|
|
||||||
var mode_names = ["ENet", "WebRTC", "WebSocket"]
|
var mode_names = ["ENet", "WebRTC", "WebSocket"]
|
||||||
LogManager.log("GameUI: Network mode changed to: " + mode_names[actual_mode], LogManager.CATEGORY_UI)
|
LogManager.log("GameUI: Network mode changed to: " + mode_names[actual_mode], LogManager.CATEGORY_UI)
|
||||||
|
|
||||||
# Handle room fetching based on mode
|
# Handle room fetching based on mode
|
||||||
if actual_mode == 1: # WebRTC mode
|
if actual_mode == 1: # WebRTC mode
|
||||||
# Only fetch if not auto-joining and not hosting
|
# Only fetch if not auto-joining and not hosting
|
||||||
if not is_auto_joining and not is_hosting and not network_manager.is_hosting:
|
if not is_auto_joining and not is_hosting and not network_manager.is_hosting:
|
||||||
LogManager.log("GameUI: Switched to WebRTC mode, fetching rooms", LogManager.CATEGORY_UI)
|
LogManager.log("GameUI: Switched to WebRTC mode, fetching rooms", LogManager.CATEGORY_UI)
|
||||||
@@ -613,26 +665,26 @@ func _on_network_mode_changed(index: int):
|
|||||||
_start_room_fetch()
|
_start_room_fetch()
|
||||||
else:
|
else:
|
||||||
LogManager.log("GameUI: Switched to WebRTC mode but skipping room fetch (auto_joining: " + str(is_auto_joining) + ", hosting: " + str(is_hosting) + ")", LogManager.CATEGORY_UI)
|
LogManager.log("GameUI: Switched to WebRTC mode but skipping room fetch (auto_joining: " + str(is_auto_joining) + ", hosting: " + str(is_hosting) + ")", LogManager.CATEGORY_UI)
|
||||||
else: # Not WebRTC mode (ENet or WebSocket)
|
else: # Not WebRTC mode (ENet or WebSocket)
|
||||||
# Hide room fetch status if switching away from WebRTC
|
# Hide room fetch status if switching away from WebRTC
|
||||||
LogManager.log("GameUI: Switched away from WebRTC mode, hiding room fetch UI", LogManager.CATEGORY_UI)
|
LogManager.log("GameUI: Switched away from WebRTC mode, hiding room fetch UI", LogManager.CATEGORY_UI)
|
||||||
_hide_room_fetch_status()
|
_hide_room_fetch_status()
|
||||||
|
|
||||||
func _on_host_pressed():
|
func _on_host_pressed():
|
||||||
is_hosting = true # Set flag so we don't fetch rooms
|
is_hosting = true # Set flag so we don't fetch rooms
|
||||||
var local_count = int(local_players_spinbox.value)
|
var local_count = int(local_players_spinbox.value)
|
||||||
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():
|
||||||
var mode = network_manager.network_mode
|
var mode = network_manager.network_mode
|
||||||
if mode == 1 or mode == 2: # WebRTC or WebSocket
|
if mode == 1 or mode == 2: # WebRTC or WebSocket
|
||||||
var room_id = network_manager.get_room_id()
|
var room_id = network_manager.get_room_id()
|
||||||
var mode_name = "WebRTC" if mode == 1 else "WebSocket"
|
var mode_name = "WebRTC" if mode == 1 else "WebSocket"
|
||||||
print("Hosting ", mode_name, " game - Room Code: ", room_id)
|
print("Hosting ", mode_name, " game - Room Code: ", room_id)
|
||||||
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
|
||||||
@@ -642,10 +694,10 @@ func _on_join_pressed():
|
|||||||
var address = address_input.text
|
var address = address_input.text
|
||||||
if address.is_empty():
|
if address.is_empty():
|
||||||
var mode = network_manager.network_mode
|
var mode = network_manager.network_mode
|
||||||
if mode == 1 or mode == 2: # WebRTC or WebSocket
|
if mode == 1 or mode == 2: # WebRTC or WebSocket
|
||||||
LogManager.log("Error: Please enter a room code", LogManager.CATEGORY_UI)
|
LogManager.log("Error: Please enter a room code", LogManager.CATEGORY_UI)
|
||||||
return
|
return
|
||||||
else: # ENet mode without address - use default
|
else: # ENet mode without address - use default
|
||||||
address = "127.0.0.1"
|
address = "127.0.0.1"
|
||||||
|
|
||||||
var local_count = int(local_players_spinbox.value)
|
var local_count = int(local_players_spinbox.value)
|
||||||
@@ -654,11 +706,11 @@ func _on_join_pressed():
|
|||||||
if network_manager.join_game(address):
|
if network_manager.join_game(address):
|
||||||
last_join_address = address
|
last_join_address = address
|
||||||
var mode = network_manager.network_mode
|
var mode = network_manager.network_mode
|
||||||
if mode == 1: # WebRTC
|
if mode == 1: # WebRTC
|
||||||
LogManager.log("Joining WebRTC game with room code: " + address, LogManager.CATEGORY_UI)
|
LogManager.log("Joining WebRTC game with room code: " + address, LogManager.CATEGORY_UI)
|
||||||
elif mode == 2: # WebSocket
|
elif mode == 2: # WebSocket
|
||||||
LogManager.log("Joining WebSocket game with room code: " + address, LogManager.CATEGORY_UI)
|
LogManager.log("Joining WebSocket game with room code: " + address, LogManager.CATEGORY_UI)
|
||||||
else: # ENet
|
else: # ENet
|
||||||
LogManager.log("Joining ENet game at " + address + " with " + str(local_count) + " local players", LogManager.CATEGORY_UI)
|
LogManager.log("Joining ENet game at " + address + " with " + str(local_count) + " local players", LogManager.CATEGORY_UI)
|
||||||
|
|
||||||
func _on_connection_succeeded():
|
func _on_connection_succeeded():
|
||||||
@@ -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():
|
||||||
@@ -757,7 +818,7 @@ func _show_connection_error(message: String):
|
|||||||
if vbox:
|
if vbox:
|
||||||
# Insert after title (index 0) or at the beginning
|
# Insert after title (index 0) or at the beginning
|
||||||
vbox.add_child(connection_error_label)
|
vbox.add_child(connection_error_label)
|
||||||
vbox.move_child(connection_error_label, 1) # Move to position 1 (after title)
|
vbox.move_child(connection_error_label, 1) # Move to position 1 (after title)
|
||||||
|
|
||||||
# Auto-hide after 5 seconds
|
# Auto-hide after 5 seconds
|
||||||
await get_tree().create_timer(5.0).timeout
|
await get_tree().create_timer(5.0).timeout
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -338,7 +338,7 @@ func _break_into_pieces(silent: bool = false):
|
|||||||
# Spawn item loot when breaking (30% chance)
|
# Spawn item loot when breaking (30% chance)
|
||||||
if is_multiplayer_authority():
|
if is_multiplayer_authority():
|
||||||
var drop_chance = randf()
|
var drop_chance = randf()
|
||||||
if drop_chance < 0.3: # 30% chance to drop item
|
if drop_chance < 0.3: # 30% chance to drop item
|
||||||
var item = ItemDatabase.get_random_container_item()
|
var item = ItemDatabase.get_random_container_item()
|
||||||
if item:
|
if item:
|
||||||
var entities_node = get_parent()
|
var entities_node = get_parent()
|
||||||
@@ -470,7 +470,7 @@ func on_grabbed(by_player):
|
|||||||
else:
|
else:
|
||||||
# Server or single player - open directly
|
# Server or single player - open directly
|
||||||
_open_chest(by_player)
|
_open_chest(by_player)
|
||||||
return # CRITICAL: Return early to prevent normal grab behavior
|
return # CRITICAL: Return early to prevent normal grab behavior
|
||||||
|
|
||||||
is_being_held = true
|
is_being_held = true
|
||||||
held_by_player = by_player
|
held_by_player = by_player
|
||||||
@@ -554,7 +554,7 @@ func _convert_to_bomb_projectile(by_player, force: Vector2):
|
|||||||
var bomb = attack_bomb_scene.instantiate()
|
var bomb = attack_bomb_scene.instantiate()
|
||||||
bomb.name = "ThrownBomb_" + name
|
bomb.name = "ThrownBomb_" + name
|
||||||
get_parent().add_child(bomb)
|
get_parent().add_child(bomb)
|
||||||
bomb.global_position = current_pos # Use current position, not target
|
bomb.global_position = current_pos # Use current position, not target
|
||||||
|
|
||||||
# Set multiplayer authority
|
# Set multiplayer authority
|
||||||
if multiplayer.has_multiplayer_peer():
|
if multiplayer.has_multiplayer_peer():
|
||||||
@@ -562,15 +562,24 @@ func _convert_to_bomb_projectile(by_player, force: Vector2):
|
|||||||
|
|
||||||
# Setup bomb with throw physics (pass force as throw_velocity)
|
# Setup bomb with throw physics (pass force as throw_velocity)
|
||||||
# The bomb will use throw_velocity for movement
|
# The bomb will use throw_velocity for movement
|
||||||
bomb.setup(current_pos, by_player, force, true) # true = is_thrown, force is throw_velocity
|
bomb.setup(current_pos, by_player, force, true) # true = is_thrown, force is throw_velocity
|
||||||
|
|
||||||
# Make sure bomb sprite is visible
|
# Make sure bomb sprite is visible
|
||||||
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():
|
||||||
by_player._rpc_to_ready_peers("_sync_throw_bomb", [name, current_pos, force])
|
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])
|
||||||
|
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()
|
||||||
@@ -602,7 +611,7 @@ func setup_pot():
|
|||||||
can_be_pushed = true
|
can_be_pushed = true
|
||||||
is_destroyable = true
|
is_destroyable = true
|
||||||
is_liftable = true
|
is_liftable = true
|
||||||
weight = 0.8 # Pots are very light and easy to throw far!
|
weight = 0.8 # Pots are very light and easy to throw far!
|
||||||
|
|
||||||
var pot_frames = [1, 2, 3, 20, 21, 22, 58]
|
var pot_frames = [1, 2, 3, 20, 21, 22, 58]
|
||||||
if sprite:
|
if sprite:
|
||||||
@@ -661,7 +670,7 @@ func setup_box():
|
|||||||
can_be_pushed = true
|
can_be_pushed = true
|
||||||
is_destroyable = true
|
is_destroyable = true
|
||||||
is_liftable = true
|
is_liftable = true
|
||||||
weight = 1.5 # Boxes are heavier than pots
|
weight = 1.5 # Boxes are heavier than pots
|
||||||
|
|
||||||
var box_frames = [7, 26]
|
var box_frames = [7, 26]
|
||||||
if sprite:
|
if sprite:
|
||||||
@@ -757,9 +766,9 @@ func setup_bomb():
|
|||||||
object_type = "Bomb"
|
object_type = "Bomb"
|
||||||
is_grabbable = true
|
is_grabbable = true
|
||||||
can_be_pushed = false
|
can_be_pushed = false
|
||||||
is_destroyable = false # Bombs don't break, they explode
|
is_destroyable = false # Bombs don't break, they explode
|
||||||
is_liftable = true
|
is_liftable = true
|
||||||
weight = 0.5 # Light weight for easy throwing
|
weight = 0.5 # Light weight for easy throwing
|
||||||
|
|
||||||
# Set bomb sprite (frame 199 from items_n_shit.png)
|
# Set bomb sprite (frame 199 from items_n_shit.png)
|
||||||
if sprite:
|
if sprite:
|
||||||
@@ -853,7 +862,7 @@ func _open_chest(by_player: Node = null):
|
|||||||
if chest_item.item_type == Item.ItemType.Restoration:
|
if chest_item.item_type == Item.ItemType.Restoration:
|
||||||
item_color = Color.GREEN
|
item_color = Color.GREEN
|
||||||
elif chest_item.item_type == Item.ItemType.Equippable:
|
elif chest_item.item_type == Item.ItemType.Equippable:
|
||||||
item_color = Color.CYAN # Cyan for equipment (matches loot pickup color)
|
item_color = Color.CYAN # Cyan for equipment (matches loot pickup color)
|
||||||
else:
|
else:
|
||||||
item_color = Color.WHITE
|
item_color = Color.WHITE
|
||||||
|
|
||||||
@@ -955,7 +964,7 @@ func _sync_chest_open(loot_type_str: String = "coin", player_peer_id: int = 0, i
|
|||||||
if chest_item.item_type == Item.ItemType.Restoration:
|
if chest_item.item_type == Item.ItemType.Restoration:
|
||||||
item_color = Color.GREEN
|
item_color = Color.GREEN
|
||||||
elif chest_item.item_type == Item.ItemType.Equippable:
|
elif chest_item.item_type == Item.ItemType.Equippable:
|
||||||
item_color = Color.CYAN # Cyan for equipment (matches loot pickup color)
|
item_color = Color.CYAN # Cyan for equipment (matches loot pickup color)
|
||||||
|
|
||||||
if items_texture:
|
if items_texture:
|
||||||
_show_item_pickup_notification(player, display_text, item_color, items_texture, chest_item.spriteFrames.x, chest_item.spriteFrames.y, chest_item.spriteFrame, chest_item)
|
_show_item_pickup_notification(player, display_text, item_color, items_texture, chest_item.spriteFrames.x, chest_item.spriteFrames.y, chest_item.spriteFrame, chest_item)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -353,6 +353,20 @@ func _handle_new_peer(uuid: String):
|
|||||||
# Client: we don't assign peer IDs, the host does
|
# Client: we don't assign peer IDs, the host does
|
||||||
# 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():
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -518,6 +518,15 @@ func _on_matchbox_connected(was_reconnecting: bool = false):
|
|||||||
var chat_ui = game_world.get_node_or_null("ChatUI")
|
var chat_ui = game_world.get_node_or_null("ChatUI")
|
||||||
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")
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func _ready():
|
|||||||
func setup(direction: Vector2, owner_player: Node, damage_value: float = 20.0):
|
func setup(direction: Vector2, owner_player: Node, damage_value: float = 20.0):
|
||||||
travel_direction = direction.normalized()
|
travel_direction = direction.normalized()
|
||||||
player_owner = owner_player
|
player_owner = owner_player
|
||||||
damage = damage_value # Set damage from player
|
damage = damage_value # Set damage from player
|
||||||
current_speed = initial_speed
|
current_speed = initial_speed
|
||||||
|
|
||||||
# Rotate sprite to face travel direction
|
# Rotate sprite to face travel direction
|
||||||
@@ -76,39 +76,37 @@ func _on_body_entered(body):
|
|||||||
# Server creates the projectile first, then clients create it via _sync_attack
|
# Server creates the projectile first, then clients create it via _sync_attack
|
||||||
# Without this check, both server and client projectiles would deal damage
|
# Without this check, both server and client projectiles would deal damage
|
||||||
if player_owner and not player_owner.is_multiplayer_authority():
|
if player_owner and not player_owner.is_multiplayer_authority():
|
||||||
return # Only the authority (creator) of the projectile can deal damage
|
return # Only the authority (creator) of the projectile can deal damage
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
# Check hit chance (based on player's DEX stat)
|
# Check hit chance (based on player's DEX stat)
|
||||||
var hit_roll = randf()
|
var hit_roll = randf()
|
||||||
var hit_chance = 0.95 # Base hit chance
|
var hit_chance = 0.95 # Base hit chance
|
||||||
if player_owner and player_owner.character_stats:
|
if player_owner and player_owner.character_stats:
|
||||||
hit_chance = player_owner.character_stats.hit_chance
|
hit_chance = player_owner.character_stats.hit_chance
|
||||||
var is_miss = hit_roll >= hit_chance
|
var is_miss = hit_roll >= hit_chance
|
||||||
@@ -118,8 +116,8 @@ func _on_body_entered(body):
|
|||||||
print("Player MISSED enemy: ", body.name, "! (hit chance: ", hit_chance * 100.0, "%)")
|
print("Player MISSED enemy: ", body.name, "! (hit chance: ", hit_chance * 100.0, "%)")
|
||||||
# Show MISS text on the enemy
|
# Show MISS text on the enemy
|
||||||
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
|
||||||
return # Don't deal damage, don't play impact sound, don't cause knockback
|
return # Don't deal damage, don't play impact sound, don't cause knockback
|
||||||
|
|
||||||
# Hit successful - play impact sound and deal damage
|
# Hit successful - play impact sound and deal damage
|
||||||
$SfxImpact.play()
|
$SfxImpact.play()
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ extends Node2D
|
|||||||
# Sword Slash - Swings around player and deals damage
|
# Sword Slash - Swings around player and deals damage
|
||||||
|
|
||||||
@export var damage: float = 20.0
|
@export var damage: float = 20.0
|
||||||
@export var swing_speed: float = 720.0 # Degrees per second
|
@export var swing_speed: float = 720.0 # Degrees per second
|
||||||
@export var swing_radius: float = 40.0 # Distance from player center (closer swing)
|
@export var swing_radius: float = 40.0 # Distance from player center (closer swing)
|
||||||
@export var lifetime: float = 0.3 # How long the slash lasts
|
@export var lifetime: float = 0.3 # How long the slash lasts
|
||||||
|
|
||||||
var swing_angle: float = 0.0 # Current angle
|
var swing_angle: float = 0.0 # Current angle
|
||||||
var swing_start_angle: float = 0.0 # Starting angle
|
var swing_start_angle: float = 0.0 # Starting angle
|
||||||
var swing_arc: float = 180.0 # Total arc to swing (180 degrees)
|
var swing_arc: float = 180.0 # Total arc to swing (180 degrees)
|
||||||
var elapsed_time: float = 0.0
|
var elapsed_time: float = 0.0
|
||||||
var player_owner: Node = null
|
var player_owner: Node = null
|
||||||
var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup)
|
var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup)
|
||||||
|
|
||||||
@onready var sprite = $Sprite2D
|
@onready var sprite = $Sprite2D
|
||||||
@onready var hit_area = $Area2D
|
@onready var hit_area = $Area2D
|
||||||
@@ -29,7 +29,7 @@ func setup(start_angle: float, owner_player: Node, arc_direction: float = 1.0):
|
|||||||
swing_start_angle = start_angle
|
swing_start_angle = start_angle
|
||||||
swing_angle = start_angle
|
swing_angle = start_angle
|
||||||
player_owner = owner_player
|
player_owner = owner_player
|
||||||
swing_arc = 180.0 * arc_direction # Positive or negative arc
|
swing_arc = 180.0 * arc_direction # Positive or negative arc
|
||||||
rotation = deg_to_rad(swing_start_angle)
|
rotation = deg_to_rad(swing_start_angle)
|
||||||
|
|
||||||
func _physics_process(delta):
|
func _physics_process(delta):
|
||||||
@@ -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:
|
||||||
|
|||||||