added more tomes

This commit is contained in:
2026-01-25 00:59:34 +01:00
parent 9ab4a13244
commit a95e22d2fa
79 changed files with 2429 additions and 337 deletions

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://c10ju1f6d4ed3"
path="res://.godot/imported/activate_shield.wav-4315e73a1b91a2001e5cfc56e2fd9c74.sample"
[deps]
source_file="res://assets/audio/sfx/shield/activate_shield.wav"
dest_files=["res://.godot/imported/activate_shield.wav-4315e73a1b91a2001e5cfc56e2fd9c74.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://nni5cgx3obpk"
path="res://.godot/imported/denied_activate_Shield.wav-0a9618c73f1763b87655b2b1207a185f.sample"
[deps]
source_file="res://assets/audio/sfx/shield/denied_activate_Shield.wav"
dest_files=["res://.godot/imported/denied_activate_Shield.wav-0a9618c73f1763b87655b2b1207a185f.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dvq72502qa46f"
path="res://.godot/imported/denied_activate_Shield2.wav-c8d26214a783643d1704131f67c77cb5.sample"
[deps]
source_file="res://assets/audio/sfx/shield/denied_activate_Shield2.wav"
dest_files=["res://.godot/imported/denied_activate_Shield2.wav-c8d26214a783643d1704131f67c77cb5.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dbdem0g2ivn7k"
path="res://.godot/imported/domedagsljud.wav-b4f6d0c4c012380dcda8492ee0223f5e.sample"
[deps]
source_file="res://assets/audio/sfx/shield/domedagsljud.wav"
dest_files=["res://.godot/imported/domedagsljud.wav-b4f6d0c4c012380dcda8492ee0223f5e.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://vijnmoeqpspl"
path="res://.godot/imported/pickup_weapon.wav-5daceda79276fbce185b7eae0942eb9f.sample"
[deps]
source_file="res://assets/audio/sfx/shield/pickup_weapon.wav"
dest_files=["res://.godot/imported/pickup_weapon.wav-5daceda79276fbce185b7eae0942eb9f.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://ly1euk0v3jxy"
path="res://.godot/imported/shield.wav-9f6d52c26ab431d241317a8914dbd400.sample"
[deps]
source_file="res://assets/audio/sfx/shield/shield.wav"
dest_files=["res://.godot/imported/shield.wav-9f6d52c26ab431d241317a8914dbd400.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://c4lh535yj010h"
path="res://.godot/imported/shield1.wav-612ea507d8afe53393d2a3524371577b.sample"
[deps]
source_file="res://assets/audio/sfx/shield/shield1.wav"
dest_files=["res://.godot/imported/shield1.wav-612ea507d8afe53393d2a3524371577b.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://ch3p57i7fvd1v"
path="res://.godot/imported/shield2.wav-2b1db8dc996b44c797a97c653e118567.sample"
[deps]
source_file="res://assets/audio/sfx/shield/shield2.wav"
dest_files=["res://.godot/imported/shield2.wav-2b1db8dc996b44c797a97c653e118567.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://t0sg2rxlfech"
path="res://.godot/imported/shield3.wav-bb74330e5d55af4d6399c4c2b21af2d8.sample"
[deps]
source_file="res://assets/audio/sfx/shield/shield3.wav"
dest_files=["res://.godot/imported/shield3.wav-bb74330e5d55af4d6399c4c2b21af2d8.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://b7kciskx5mn6y"
path="res://.godot/imported/stuff.mp3-7d25ebd8850ef3ff7524465aa1874914.mp3str"
[deps]
source_file="res://assets/audio/sfx/shield/stuff.mp3"
dest_files=["res://.godot/imported/stuff.mp3-7d25ebd8850ef3ff7524465aa1874914.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dyq0jwt648fl2" uid="uid://do5jdbxjiwen"
path="res://.godot/imported/SoldierGoldHelmBlue.png-2bb02261a7badbc0cd21e2f4e137c1ff.ctex" path="res://.godot/imported/AssassinBandanaBlack.png-f7bcc2f961d450d22f42560829f068d0.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierGoldHelmBlue.png" source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/AssassinBandanaBlack.png"
dest_files=["res://.godot/imported/SoldierGoldHelmBlue.png-2bb02261a7badbc0cd21e2f4e137c1ff.ctex"] dest_files=["res://.godot/imported/AssassinBandanaBlack.png-f7bcc2f961d450d22f42560829f068d0.ctex"]
[params] [params]

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dswnht88lm3ks" uid="uid://dswnht88lm3ks"
path="res://.godot/imported/StalkerHoodBlack.png-bc5c1af5d15f82e95db19b23af7b861f.ctex" path="res://.godot/imported/StalkerHoodBlack.png-db060deca6decafc4dc183500fb85065.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/StalkerHoodBlack.png" source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/StalkerHoodBlack.png"
dest_files=["res://.godot/imported/StalkerHoodBlack.png-bc5c1af5d15f82e95db19b23af7b861f.ctex"] dest_files=["res://.godot/imported/StalkerHoodBlack.png-db060deca6decafc4dc183500fb85065.ctex"]
[params] [params]

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -3,15 +3,15 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dt0otw6d11pa7" uid="uid://dt0otw6d11pa7"
path="res://.godot/imported/ThiefBandanaGreen.png-244270b26eabdab0125f17bea0cb15e5.ctex" path="res://.godot/imported/ThiefBandanaGreen.png-4d39120b40e65fba58f9d71c1dde48e1.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/ThiefBandanaGreen.png" source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/ThiefBandanaGreen.png"
dest_files=["res://.godot/imported/ThiefBandanaGreen.png-244270b26eabdab0125f17bea0cb15e5.ctex"] dest_files=["res://.godot/imported/ThiefBandanaGreen.png-4d39120b40e65fba58f9d71c1dde48e1.ctex"]
[params] [params]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@@ -2,7 +2,7 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bkaam8riwwft4" uid="uid://yumebu3noyau"
path="res://.godot/imported/ArcherHatCyan.png-359731c5c2a1a0c4d2a4e5623e2151c2.ctex" path="res://.godot/imported/ArcherHatCyan.png-359731c5c2a1a0c4d2a4e5623e2151c2.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://dqwwsajau10n7" uid="uid://bkca7nmt4du5e"
path="res://.godot/imported/RangerHatGreen.png-b7941d36e5dec52b1b4b8ba30452afa7.ctex" path="res://.godot/imported/ShieldOverlayer.png-801252b933f8a048302e86bf4a907ac9.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RangerHatGreen.png" source_file="res://assets/gfx/Puny-Characters/ShieldOverlayer.png"
dest_files=["res://.godot/imported/RangerHatGreen.png-b7941d36e5dec52b1b4b8ba30452afa7.ctex"] dest_files=["res://.godot/imported/ShieldOverlayer.png-801252b933f8a048302e86bf4a907ac9.ctex"]
[params] [params]

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -2,16 +2,16 @@
importer="texture" importer="texture"
type="CompressedTexture2D" type="CompressedTexture2D"
uid="uid://bxgu54fmyteul" uid="uid://bpxxpdpow5qyl"
path="res://.godot/imported/SoldierBronzeHelmRed.png-c2f0c80930493f16114c541823eaecff.ctex" path="res://.godot/imported/ShieldOverlayerHolding.png-973cd2b6c2eba0416ade721b4b48b9ac.ctex"
metadata={ metadata={
"vram_texture": false "vram_texture": false
} }
[deps] [deps]
source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierBronzeHelmRed.png" source_file="res://assets/gfx/Puny-Characters/ShieldOverlayerHolding.png"
dest_files=["res://.godot/imported/SoldierBronzeHelmRed.png-c2f0c80930493f16114c541823eaecff.ctex"] dest_files=["res://.godot/imported/ShieldOverlayerHolding.png-973cd2b6c2eba0416ade721b4b48b9ac.ctex"]
[params] [params]

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://dm0wpd6qub4i1"
path="res://.godot/imported/shield.wav-6f115adeaf28ccc3ea4a9f30145b8c34.sample"
[deps]
source_file="res://assets/gfx/Puny-Characters/shield.wav"
dest_files=["res://.godot/imported/shield.wav-6f115adeaf28ccc3ea4a9f30145b8c34.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cdqym40of00i"
path="res://.godot/imported/shield_init.png-c8f99f8a12f1cd02f5f0f86f9e835167.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/gfx/Puny-Characters/shield_init.png"
dest_files=["res://.godot/imported/shield_init.png-c8f99f8a12f1cd02f5f0f86f9e835167.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

View File

@@ -205,6 +205,7 @@ separation = Vector2i(1, 1)
10:3/0 = 0 10:3/0 = 0
11:3/0 = 0 11:3/0 = 0
12:3/0 = 0 12:3/0 = 0
12:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
0:4/0 = 0 0:4/0 = 0
0:4/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_hu0mk") 0:4/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_hu0mk")
0:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 0:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
@@ -232,6 +233,7 @@ separation = Vector2i(1, 1)
11:4/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_okmkx") 11:4/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_okmkx")
11:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 11:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
12:4/0 = 0 12:4/0 = 0
12:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
0:5/0 = 0 0:5/0 = 0
1:5/0 = 0 1:5/0 = 0
1:5/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 1:5/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
@@ -249,6 +251,7 @@ separation = Vector2i(1, 1)
10:5/0 = 0 10:5/0 = 0
11:5/0 = 0 11:5/0 = 0
12:5/0 = 0 12:5/0 = 0
12:5/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
0:6/0 = 0 0:6/0 = 0
0:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 0:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
1:6/0 = 0 1:6/0 = 0
@@ -493,6 +496,27 @@ separation = Vector2i(1, 1)
11:15/0 = 0 11:15/0 = 0
12:15/0 = 0 12:15/0 = 0
13:15/0 = 0 13:15/0 = 0
13:3/0 = 0
13:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
14:3/0 = 0
14:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
13:4/0 = 0
13:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
13:5/0 = 0
13:5/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
14:5/0 = 0
14:5/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
15:5/0 = 0
16:5/0 = 0
16:5/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
17:5/0 = 0
17:5/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
15:4/0 = 0
14:4/0 = 0
14:2/0 = 0
14:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
14:1/0 = 0
14:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
[resource] [resource]
occlusion_layer_0/light_mask = 1 occlusion_layer_0/light_mask = 1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dty1vobwvjhkw"
path="res://.godot/imported/yukon-salve.png-4bc768fa22c129f9ab6b79c2df853b34.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/gfx/yukon-salve.png"
dest_files=["res://.godot/imported/yukon-salve.png-4bc768fa22c129f9ab6b79c2df853b34.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

View File

@@ -0,0 +1,30 @@
[gd_scene format=3 uid="uid://d1u8p5rop4vye"]
[ext_resource type="Script" path="res://scripts/attack_spell_frostspike.gd" id="1_script"]
[ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="2_tex"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_frost"]
size = Vector2(16, 16)
[node name="FrostspikeSpell" type="Node2D"]
z_index = 4
script = ExtResource("1_script")
[node name="Sprite2D" type="Sprite2D" parent="."]
texture = ExtResource("2_tex")
hframes = 105
vframes = 79
frame = 4413
[node name="SpikeLight" type="PointLight2D" parent="."]
color = Color(0.35, 0.6, 1, 1)
energy = 1.0
texture_scale = 0.5
enabled = true
[node name="Area2D" type="Area2D" parent="."]
collision_layer = 4
collision_mask = 3
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
shape = SubResource("RectangleShape2D_frost")

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,21 @@
[gd_scene format=3 uid="uid://d3heal8fx2kqm"]
[ext_resource type="Script" path="res://scripts/healing_effect.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="HealingEffect" type="Node2D"]
z_index = 5
script = ExtResource("1_script")
[node name="FxSprite" type="Sprite2D" parent="."]
offset = Vector2(0, -24)
texture = ExtResource("2_tex")
hframes = 105
vframes = 79
frame = 589
[node name="HealLight" type="PointLight2D" parent="."]
color = Color(0.3, 1, 0.35, 1)
energy = 0.8
texture_scale = 0.5
enabled = false

View File

@@ -3,6 +3,7 @@
[ext_resource type="Script" uid="uid://jk7o0itmiwp6" path="res://scripts/loot.gd" id="1"] [ext_resource type="Script" uid="uid://jk7o0itmiwp6" path="res://scripts/loot.gd" id="1"]
[ext_resource type="Script" uid="uid://cpxabh3uq1kl4" path="res://scripts/create_shadow_sprite.gd" id="2"] [ext_resource type="Script" uid="uid://cpxabh3uq1kl4" path="res://scripts/create_shadow_sprite.gd" id="2"]
[ext_resource type="AudioStream" uid="uid://b60bke4f5uw4v" path="res://assets/audio/sfx/pickups/coin_pickup.mp3" id="3_30m34"] [ext_resource type="AudioStream" uid="uid://b60bke4f5uw4v" path="res://assets/audio/sfx/pickups/coin_pickup.mp3" id="3_30m34"]
[ext_resource type="Shader" uid="uid://ldl7vaq5n13f" path="res://shaders/cloth.gdshader" id="3_37k03"]
[ext_resource type="Texture2D" uid="uid://cimek2qjgoqa1" path="res://assets/gfx/pickups/gold_coin.png" id="3_531sv"] [ext_resource type="Texture2D" uid="uid://cimek2qjgoqa1" path="res://assets/gfx/pickups/gold_coin.png" id="3_531sv"]
[ext_resource type="AudioStream" uid="uid://brl8ivwb1l5i7" path="res://assets/audio/sfx/pickups/coin_drop_01.wav.mp3" id="4_rtp8m"] [ext_resource type="AudioStream" uid="uid://brl8ivwb1l5i7" path="res://assets/audio/sfx/pickups/coin_drop_01.wav.mp3" id="4_rtp8m"]
[ext_resource type="AudioStream" uid="uid://umoxmryvbm01" path="res://assets/audio/sfx/cloth/leather_cloth_01.wav.mp3" id="5_vl55g"] [ext_resource type="AudioStream" uid="uid://umoxmryvbm01" path="res://assets/audio/sfx/cloth/leather_cloth_01.wav.mp3" id="5_vl55g"]
@@ -22,6 +23,24 @@ fill = 1
fill_from = Vector2(0.51304346, 0.46086955) fill_from = Vector2(0.51304346, 0.46086955)
fill_to = Vector2(0, 0) fill_to = Vector2(0, 0)
[sub_resource type="ShaderMaterial" id="ShaderMaterial_8ty1w"]
shader = ExtResource("3_37k03")
shader_parameter/original_0 = Color(0, 0, 0, 1)
shader_parameter/original_1 = Color(0, 0, 0, 1)
shader_parameter/original_2 = Color(0, 0, 0, 1)
shader_parameter/original_3 = Color(0, 0, 0, 1)
shader_parameter/original_4 = Color(0, 0, 0, 1)
shader_parameter/original_5 = Color(0, 0, 0, 1)
shader_parameter/original_6 = Color(0, 0, 0, 1)
shader_parameter/replace_0 = Color(0, 0, 0, 1)
shader_parameter/replace_1 = Color(0, 0, 0, 1)
shader_parameter/replace_2 = Color(0, 0, 0, 1)
shader_parameter/replace_3 = Color(0, 0, 0, 1)
shader_parameter/replace_4 = Color(0, 0, 0, 1)
shader_parameter/replace_5 = Color(0, 0, 0, 1)
shader_parameter/replace_6 = Color(0, 0, 0, 1)
shader_parameter/tint = Color(1, 1, 1, 1)
[sub_resource type="CircleShape2D" id="CircleShape2D_2"] [sub_resource type="CircleShape2D" id="CircleShape2D_2"]
radius = 3.0 radius = 3.0
@@ -49,6 +68,7 @@ script = ExtResource("2")
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1501367665] [node name="Sprite2D" type="Sprite2D" parent="." unique_id=1501367665]
y_sort_enabled = true y_sort_enabled = true
material = SubResource("ShaderMaterial_8ty1w")
texture = ExtResource("3_531sv") texture = ExtResource("3_531sv")
hframes = 6 hframes = 6

View File

@@ -0,0 +1,16 @@
[gd_scene format=3 uid="uid://c8k2xmwv4nqyp"]
[ext_resource type="Script" path="res://scripts/off_screen_indicators.gd" id="1_script"]
[node name="OffScreenIndicators" type="CanvasLayer"]
layer = 199
[node name="IndicatorOverlay" type="Control" parent="."]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
script = ExtResource("1_script")

View File

@@ -14,7 +14,9 @@
[ext_resource type="Texture2D" uid="uid://cu5fkio3ajr5i" path="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/French/MusketeerHatPurple.png" id="12"] [ext_resource type="Texture2D" uid="uid://cu5fkio3ajr5i" path="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/French/MusketeerHatPurple.png" id="12"]
[ext_resource type="Texture2D" uid="uid://bloqx3mibftjn" path="res://assets/gfx/Puny-Characters/WeaponOverlayer.png" id="13"] [ext_resource type="Texture2D" uid="uid://bloqx3mibftjn" path="res://assets/gfx/Puny-Characters/WeaponOverlayer.png" id="13"]
[ext_resource type="AudioStream" uid="uid://cbio6f0ssxvd6" path="res://assets/audio/sfx/walk/stone/walk_stone_1.wav.mp3" id="13_fulsm"] [ext_resource type="AudioStream" uid="uid://cbio6f0ssxvd6" path="res://assets/audio/sfx/walk/stone/walk_stone_1.wav.mp3" id="13_fulsm"]
[ext_resource type="Texture2D" uid="uid://bkca7nmt4du5e" path="res://assets/gfx/Puny-Characters/ShieldOverlayer.png" id="13_t4otl"]
[ext_resource type="AudioStream" uid="uid://dq1va2882v23v" path="res://assets/audio/sfx/walk/stone/walk_stone_2.wav.mp3" id="14_4r5pv"] [ext_resource type="AudioStream" uid="uid://dq1va2882v23v" path="res://assets/audio/sfx/walk/stone/walk_stone_2.wav.mp3" id="14_4r5pv"]
[ext_resource type="Texture2D" uid="uid://bpxxpdpow5qyl" path="res://assets/gfx/Puny-Characters/ShieldOverlayerHolding.png" id="14_j2b1d"]
[ext_resource type="AudioStream" uid="uid://dsuf4oa710gi8" path="res://assets/audio/sfx/walk/stone/walk_stone_3.wav.mp3" id="15_60mlk"] [ext_resource type="AudioStream" uid="uid://dsuf4oa710gi8" path="res://assets/audio/sfx/walk/stone/walk_stone_3.wav.mp3" id="15_60mlk"]
[ext_resource type="AudioStream" uid="uid://fvhvmxtcq018" path="res://assets/audio/sfx/walk/stone/walk_stone_4.wav.mp3" id="16_i4ail"] [ext_resource type="AudioStream" uid="uid://fvhvmxtcq018" path="res://assets/audio/sfx/walk/stone/walk_stone_4.wav.mp3" id="16_i4ail"]
[ext_resource type="AudioStream" uid="uid://cw74evef8fm0t" path="res://assets/audio/sfx/walk/stone/walk_stone_5.wav.mp3" id="17_a38lo"] [ext_resource type="AudioStream" uid="uid://cw74evef8fm0t" path="res://assets/audio/sfx/walk/stone/walk_stone_5.wav.mp3" id="17_a38lo"]
@@ -37,6 +39,12 @@
[ext_resource type="AudioStream" uid="uid://bvi00vbftbgc5" path="res://assets/audio/sfx/player/ultra_run/shinespark_start.wav" id="35_bj30b"] [ext_resource type="AudioStream" uid="uid://bvi00vbftbgc5" path="res://assets/audio/sfx/player/ultra_run/shinespark_start.wav" id="35_bj30b"]
[ext_resource type="AudioStream" uid="uid://0xm3gyh8051h" path="res://assets/audio/sfx/wizard/incantations/indignation.mp3" id="36_jc3p3"] [ext_resource type="AudioStream" uid="uid://0xm3gyh8051h" path="res://assets/audio/sfx/wizard/incantations/indignation.mp3" id="36_jc3p3"]
[ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="37_hax0n"] [ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="37_hax0n"]
[ext_resource type="AudioStream" uid="uid://c10ju1f6d4ed3" path="res://assets/audio/sfx/shield/activate_shield.wav" id="40_hhpqf"]
[ext_resource type="AudioStream" uid="uid://ly1euk0v3jxy" path="res://assets/audio/sfx/shield/shield.wav" id="41_g5jhy"]
[ext_resource type="AudioStream" uid="uid://c4lh535yj010h" path="res://assets/audio/sfx/shield/shield1.wav" id="42_holxr"]
[ext_resource type="AudioStream" uid="uid://ch3p57i7fvd1v" path="res://assets/audio/sfx/shield/shield2.wav" id="43_mx1m4"]
[ext_resource type="AudioStream" uid="uid://t0sg2rxlfech" path="res://assets/audio/sfx/shield/shield3.wav" id="44_4gjji"]
[ext_resource type="AudioStream" uid="uid://dvq72502qa46f" path="res://assets/audio/sfx/shield/denied_activate_Shield2.wav" id="45_g5jhy"]
[sub_resource type="Gradient" id="Gradient_wqfne"] [sub_resource type="Gradient" id="Gradient_wqfne"]
colors = PackedColorArray(0, 0, 0, 1, 1, 0.13732082, 0.092538536, 1) colors = PackedColorArray(0, 0, 0, 1, 1, 0.13732082, 0.092538536, 1)
@@ -234,6 +242,24 @@ shader_parameter/replace_5 = Color(0, 0, 0, 1)
shader_parameter/replace_6 = Color(0, 0, 0, 1) shader_parameter/replace_6 = Color(0, 0, 0, 1)
shader_parameter/tint = Color(1, 1, 1, 1) shader_parameter/tint = Color(1, 1, 1, 1)
[sub_resource type="ShaderMaterial" id="ShaderMaterial_hhpqf"]
shader = ExtResource("3_wnwbv")
shader_parameter/original_0 = Color(0, 0, 0, 1)
shader_parameter/original_1 = Color(0, 0, 0, 1)
shader_parameter/original_2 = Color(0, 0, 0, 1)
shader_parameter/original_3 = Color(0, 0, 0, 1)
shader_parameter/original_4 = Color(0, 0, 0, 1)
shader_parameter/original_5 = Color(0, 0, 0, 1)
shader_parameter/original_6 = Color(0, 0, 0, 1)
shader_parameter/replace_0 = Color(0, 0, 0, 1)
shader_parameter/replace_1 = Color(0, 0, 0, 1)
shader_parameter/replace_2 = Color(0, 0, 0, 1)
shader_parameter/replace_3 = Color(0, 0, 0, 1)
shader_parameter/replace_4 = Color(0, 0, 0, 1)
shader_parameter/replace_5 = Color(0, 0, 0, 1)
shader_parameter/replace_6 = Color(0, 0, 0, 1)
shader_parameter/tint = Color(1, 1, 1, 1)
[sub_resource type="ShaderMaterial" id="ShaderMaterial_fdfoy"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_fdfoy"]
shader = ExtResource("3_wnwbv") shader = ExtResource("3_wnwbv")
shader_parameter/original_0 = Color(0, 0, 0, 1) shader_parameter/original_0 = Color(0, 0, 0, 1)
@@ -342,6 +368,73 @@ tracks/0/keys = {
"values": [2037, 2038, 2039, 2040, 2041, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051, 2052, 2053] "values": [2037, 2038, 2039, 2040, 2041, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051, 2052, 2053]
} }
[sub_resource type="Animation" id="Animation_frost_ch"]
resource_name = "frost_charging"
length = 0.566
loop_mode = 1
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("IncantationSprite:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.03333334, 0.06666667, 0.1, 0.13333334, 0.16666667, 0.2, 0.23333333, 0.26666668, 0.3, 0.33333334, 0.36666667, 0.4, 0.43333334, 0.46666667, 0.5, 0.53333336),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
"update": 1,
"values": [3678, 3679, 3680, 3681, 3682, 3683, 3684, 3685, 3686, 3687, 3688, 3689, 3690, 3691, 3692, 3693, 3694]
}
[sub_resource type="Animation" id="Animation_frost_rdy"]
resource_name = "frost_ready"
length = 0.566
loop_mode = 1
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("IncantationSprite:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.03333334, 0.06666667, 0.1, 0.13333334, 0.16666667, 0.2, 0.23333333, 0.26666668, 0.3, 0.33333334, 0.36666667, 0.4, 0.43333334, 0.46666667, 0.5, 0.53333336),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
"update": 1,
"values": [3678, 3679, 3680, 3681, 3682, 3683, 3684, 3685, 3686, 3687, 3688, 3689, 3690, 3691, 3692, 3693, 3694]
}
[sub_resource type="Animation" id="Animation_heal_ch"]
resource_name = "healing_charging"
length = 0.5
loop_mode = 1
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("IncantationSprite:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
"update": 1,
"values": [589, 590, 591, 592, 593, 594, 595, 596, 597, 598]
}
[sub_resource type="Animation" id="Animation_heal_rdy"]
resource_name = "healing_ready"
length = 0.5
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("IncantationSprite:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
"update": 1,
"values": [589, 590, 591, 592, 593, 594, 595, 596, 597, 598]
}
[sub_resource type="Animation" id="Animation_hax0n"] [sub_resource type="Animation" id="Animation_hax0n"]
resource_name = "idle" resource_name = "idle"
length = 0.1 length = 0.1
@@ -363,9 +456,111 @@ _data = {
&"RESET": SubResource("Animation_t4otl"), &"RESET": SubResource("Animation_t4otl"),
&"fire_charging": SubResource("Animation_j2b1d"), &"fire_charging": SubResource("Animation_j2b1d"),
&"fire_ready": SubResource("Animation_cs1tg"), &"fire_ready": SubResource("Animation_cs1tg"),
&"frost_charging": SubResource("Animation_frost_ch"),
&"frost_ready": SubResource("Animation_frost_rdy"),
&"healing_charging": SubResource("Animation_heal_ch"),
&"healing_ready": SubResource("Animation_heal_rdy"),
&"idle": SubResource("Animation_hax0n") &"idle": SubResource("Animation_hax0n")
} }
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_u2ulf"]
streams_count = 4
stream_0/stream = ExtResource("41_g5jhy")
stream_1/stream = ExtResource("42_holxr")
stream_2/stream = ExtResource("43_mx1m4")
stream_3/stream = ExtResource("44_4gjji")
[sub_resource type="Animation" id="Animation_g5jhy"]
length = 0.001
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath(".:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 1,
"values": [711]
}
[sub_resource type="Animation" id="Animation_holxr"]
resource_name = "concussion"
length = 1.0666667
loop_mode = 1
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath(".:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.06666667, 0.13333333, 0.2, 0.26666668, 0.33333334, 0.4, 0.46666667, 0.53333336, 0.6, 0.6666667, 0.73333335, 0.8, 0.8666667, 0.93333334, 1),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
"update": 1,
"values": [711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726]
}
[sub_resource type="Animation" id="Animation_u2ulf"]
resource_name = "idle"
length = 0.46666667
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath(".:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 1,
"values": [0]
}
[sub_resource type="Animation" id="Animation_4gjji"]
resource_name = "poison"
length = 0.6
loop_mode = 1
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath(".:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.06666667, 0.13333334, 0.2, 0.26666668, 0.33333334, 0.4, 0.46666667, 0.53333336),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1),
"update": 1,
"values": [1893, 1894, 1895, 1896, 1897, 1898, 1899, 1900, 1901]
}
[sub_resource type="Animation" id="Animation_mx1m4"]
resource_name = "sleep"
length = 0.6
loop_mode = 1
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath(".:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.1, 0.2, 0.3, 0.4, 0.5),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1),
"update": 1,
"values": [291, 292, 293, 294, 295, 296]
}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_mx1m4"]
_data = {
&"RESET": SubResource("Animation_g5jhy"),
&"concussion": SubResource("Animation_holxr"),
&"idle": SubResource("Animation_u2ulf"),
&"poison": SubResource("Animation_4gjji"),
&"sleep": SubResource("Animation_mx1m4")
}
[node name="Player" type="CharacterBody2D" unique_id=937429705] [node name="Player" type="CharacterBody2D" unique_id=937429705]
collision_mask = 67 collision_mask = 67
motion_mode = 1 motion_mode = 1
@@ -462,6 +657,18 @@ texture = ExtResource("12")
hframes = 35 hframes = 35
vframes = 8 vframes = 8
[node name="Sprite2DShield" type="Sprite2D" parent="." unique_id=738217548]
material = SubResource("ShaderMaterial_hhpqf")
texture = ExtResource("13_t4otl")
hframes = 35
vframes = 8
[node name="Sprite2DShieldHolding" type="Sprite2D" parent="." unique_id=1000811066]
material = SubResource("ShaderMaterial_hhpqf")
texture = ExtResource("14_j2b1d")
hframes = 35
vframes = 8
[node name="Sprite2DWeapon" type="Sprite2D" parent="." unique_id=1889932388] [node name="Sprite2DWeapon" type="Sprite2D" parent="." unique_id=1889932388]
z_index = 1 z_index = 1
y_sort_enabled = true y_sort_enabled = true
@@ -562,7 +769,7 @@ stream = ExtResource("35_bj30b")
[node name="SfxSpellIncantation" type="AudioStreamPlayer2D" parent="." unique_id=300820616] [node name="SfxSpellIncantation" type="AudioStreamPlayer2D" parent="." unique_id=300820616]
stream = ExtResource("36_jc3p3") stream = ExtResource("36_jc3p3")
volume_db = 5.729 volume_db = -46.271
attenuation = 7.727487 attenuation = 7.727487
panning_strength = 1.04 panning_strength = 1.04
bus = &"Sfx" bus = &"Sfx"
@@ -576,3 +783,31 @@ frame = 2037
[node name="AnimationIncantation" type="AnimationPlayer" parent="." unique_id=17658820] [node name="AnimationIncantation" type="AnimationPlayer" parent="." unique_id=17658820]
libraries/ = SubResource("AnimationLibrary_2dvfe") libraries/ = SubResource("AnimationLibrary_2dvfe")
autoplay = &"idle" autoplay = &"idle"
[node name="SfxActivateShield" type="AudioStreamPlayer2D" parent="." unique_id=1247414383]
stream = ExtResource("40_hhpqf")
volume_db = 9.695
attenuation = 1.3660401
panning_strength = 1.78
[node name="SfxBlockWithShield" type="AudioStreamPlayer2D" parent="." unique_id=1010944313]
stream = SubResource("AudioStreamRandomizer_u2ulf")
volume_db = 7.254
attenuation = 1.3195078
panning_strength = 1.06
bus = &"Sfx"
[node name="SfxDenyActivateShield" type="AudioStreamPlayer2D" parent="." unique_id=1239738371]
stream = ExtResource("45_g5jhy")
volume_db = 9.458
[node name="Sprite2DStatus" type="Sprite2D" parent="." unique_id=1335748461]
position = Vector2(0, -10)
texture = ExtResource("37_hax0n")
hframes = 105
vframes = 79
frame = 711
[node name="AnimationPlayerStatus" type="AnimationPlayer" parent="Sprite2DStatus" unique_id=721795152]
libraries/ = SubResource("AnimationLibrary_mx1m4")
autoplay = &"idle"

View File

@@ -73,6 +73,9 @@ func _ready():
shadow.modulate = Color(0, 0, 0, 0.5) shadow.modulate = Color(0, 0, 0, 0.5)
shadow.z_index = -1 shadow.z_index = -1
# Group for sync lookup when collected (multiplayer)
add_to_group("attack_bomb")
# Defer area/shape setup and fuse start may run during physics (e.g. trap damage → throw) # Defer area/shape setup and fuse start may run during physics (e.g. trap damage → throw)
call_deferred("_deferred_ready") call_deferred("_deferred_ready")
@@ -338,12 +341,12 @@ func _deal_explosion_damage():
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"):
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()
# Avoid "RPC on yourself": call take_damage directly when victim is local peer
if player_peer_id != 0: if player_peer_id != 0 and player_peer_id == multiplayer.get_unique_id():
if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id(): if body.has_method("take_damage"):
body.rpc_take_damage(final_damage, attacker_pos) body.take_damage(final_damage, attacker_pos)
else: elif player_peer_id != 0:
body.rpc_take_damage.rpc_id(player_peer_id, final_damage, attacker_pos) body.rpc_take_damage.rpc_id(player_peer_id, final_damage, attacker_pos)
else: else:
body.rpc_take_damage.rpc(final_damage, attacker_pos) body.rpc_take_damage.rpc(final_damage, attacker_pos)
@@ -353,12 +356,12 @@ func _deal_explosion_damage():
elif body.is_in_group("enemy") and body.has_method("rpc_take_damage"): elif 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 enemy_peer_id = body.get_multiplayer_authority() var enemy_peer_id = body.get_multiplayer_authority()
# Avoid "RPC on yourself": call take_damage directly when enemy authority is local peer
if enemy_peer_id != 0: if enemy_peer_id != 0 and enemy_peer_id == multiplayer.get_unique_id():
if multiplayer.is_server() and enemy_peer_id == multiplayer.get_unique_id(): if body.has_method("take_damage"):
body.rpc_take_damage(final_damage, attacker_pos, false, false, false) body.take_damage(final_damage, attacker_pos, false, false, false)
else: elif enemy_peer_id != 0:
body.rpc_take_damage.rpc_id(enemy_peer_id, final_damage, attacker_pos, false, false, false) body.rpc_take_damage.rpc_id(enemy_peer_id, final_damage, attacker_pos, false, false, false)
else: else:
body.rpc_take_damage.rpc(final_damage, attacker_pos, false, false, false) body.rpc_take_damage.rpc(final_damage, attacker_pos, false, false, false)
@@ -530,6 +533,11 @@ func on_grabbed(by_player):
print(by_player.name, " collected bomb!") print(by_player.name, " collected bomb!")
# Sync removal to other clients so bomb doesn't keep exploding on their sessions
if multiplayer.has_multiplayer_peer() and by_player and is_instance_valid(by_player):
if by_player.has_method("_rpc_to_ready_peers") and by_player.is_inside_tree():
by_player._rpc_to_ready_peers("_sync_bomb_collected", [name])
# Remove bomb immediately # Remove bomb immediately
queue_free() queue_free()

View File

@@ -0,0 +1,102 @@
extends Node2D
# Frostspike spell — instant damage, no debuff. Frames 44134416, blue PointLight2D.
# If is_center: spawn center spike, then 0.5s later spawn 4 adjacent spikes.
var player_owner: Node = null
var damage: float = 15.0
var is_center: bool = false
var damage_dealt: bool = false
var elapsed: float = 0.0
var _frames: Array = [4413, 4414, 4415, 4416]
@onready var sprite: Sprite2D = $Sprite2D
@onready var spike_light: PointLight2D = $SpikeLight
@onready var hit_area: Area2D = $Area2D
func _ready() -> void:
if spike_light:
spike_light.color = Color(0.35, 0.6, 1.0)
spike_light.energy = 1.0
spike_light.enabled = true
if sprite:
sprite.frame = _frames[0]
sprite.modulate = Color(0.6, 0.8, 1.0)
if is_center:
_spawn_adjacent_after_delay()
func setup(target_pos: Vector2, owner_player: Node, damage_value: float, center: bool) -> void:
global_position = target_pos
player_owner = owner_player
damage = damage_value
is_center = center
func _spawn_adjacent_after_delay() -> void:
await get_tree().create_timer(0.5).timeout
if not is_instance_valid(self):
return
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"):
_finish_center_spike()
return
var player_pos = player_owner.global_position if player_owner else global_position
var adjacent = gw._get_adjacent_valid_spell_tile_centers(global_position, player_pos)
var scene = load("res://scenes/attack_spell_frostspike.tscn") as PackedScene
if not scene:
_finish_center_spike()
return
var par = get_parent()
for pos in adjacent:
var sp = scene.instantiate()
par.add_child(sp)
sp.setup(pos, player_owner, damage, false)
_finish_center_spike()
func _finish_center_spike() -> void:
if hit_area:
hit_area.set_deferred("monitoring", false)
queue_free()
func _process(delta: float) -> void:
elapsed += delta
if not damage_dealt and player_owner and player_owner.is_multiplayer_authority() and elapsed >= 0.05:
_deal_damage_once()
damage_dealt = true
if sprite and _frames.size() > 0:
var idx = min(int(elapsed / 0.05), _frames.size() - 1)
sprite.frame = _frames[idx]
# Non-center spikes: short lifetime. Center spike freed after spawning adjacent (in coroutine).
if not is_center and elapsed >= 0.25:
if hit_area:
hit_area.set_deferred("monitoring", false)
queue_free()
func _deal_damage_once() -> void:
if not hit_area:
return
for body in hit_area.get_overlapping_bodies():
if body == player_owner:
continue
var final_damage = damage
if player_owner and player_owner.character_stats:
var int_stat = player_owner.character_stats.baseStats.int + player_owner.character_stats.get_pass("int")
final_damage += int_stat * 0.5
var attacker_pos = player_owner.global_position if player_owner else global_position
if body.is_in_group("player") and body.has_method("rpc_take_damage"):
var pid = body.get_multiplayer_authority()
if pid != 0:
if multiplayer.get_unique_id() == pid:
body.take_damage(final_damage, attacker_pos, false, false)
else:
body.rpc_take_damage.rpc_id(pid, final_damage, attacker_pos, false, false)
else:
body.rpc_take_damage.rpc(final_damage, attacker_pos, false, false)
elif body.is_in_group("enemy") and body.has_method("rpc_take_damage"):
var eid = body.get_multiplayer_authority()
if eid != 0:
if multiplayer.get_unique_id() == eid:
body.take_damage(final_damage, attacker_pos, false, false, false)
else:
body.rpc_take_damage.rpc_id(eid, final_damage, attacker_pos, false, false, false)
else:
body.rpc_take_damage.rpc(final_damage, attacker_pos, false, false, false)

View File

@@ -0,0 +1 @@
uid://cy3rt80axidqg

View File

@@ -26,6 +26,7 @@ var is_hosting: bool = false # Track if we're hosting (don't fetch rooms when h
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)
func _ready(): func _ready():
# Wait for nodes to be ready # Wait for nodes to be ready
@@ -513,19 +514,47 @@ func _add_room_item(room_code: String, players: int, level: int):
# Join button # Join button
var room_join_button = Button.new() var room_join_button = Button.new()
room_join_button.name = "JoinButton"
room_join_button.text = "Join" room_join_button.text = "Join"
room_join_button.custom_minimum_size = Vector2(80, 0) room_join_button.custom_minimum_size = Vector2(80, 0)
# Connect button to join function room_join_button.pressed.connect(func(): _join_room(room_code, room_join_button))
room_join_button.pressed.connect(func(): _join_room(room_code))
room_row.add_child(room_join_button) room_row.add_child(room_join_button)
room_list_container.add_child(room_row) room_list_container.add_child(room_row)
func _join_room(room_code: String): func _disable_all_room_join_buttons():
"""Disable all room Join buttons to prevent multiple clicks"""
if not room_list_container:
return
for row in room_list_container.get_children():
if row.name.begins_with("RoomRow_"):
var btn = row.get_node_or_null("JoinButton")
if btn and is_instance_valid(btn):
btn.disabled = true
func _reset_room_join_buttons():
"""Re-enable all room Join buttons and restore 'Join' text"""
if not room_list_container:
return
for row in room_list_container.get_children():
if row.name.begins_with("RoomRow_"):
var btn = row.get_node_or_null("JoinButton")
if btn and is_instance_valid(btn):
btn.disabled = false
btn.text = "Join"
active_room_join_button = null
func _join_room(room_code: String, room_join_button: Button = null):
"""Join a room by setting the address and clicking join""" """Join a room by setting the address and clicking join"""
if room_code.is_empty(): if room_code.is_empty():
return return
# Prevent multiple clicks: disable all join buttons and show loader state
_disable_all_room_join_buttons()
if room_join_button and is_instance_valid(room_join_button):
room_join_button.text = "Joining..."
active_room_join_button = room_join_button
# Set the address input # Set the address input
if address_input: if address_input:
address_input.text = room_code address_input.text = room_code
@@ -534,9 +563,15 @@ func _join_room(room_code: String):
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)
is_joining_attempt = true
last_join_address = room_code
# Join the game # Join the game
if network_manager.join_game(room_code): if not network_manager.join_game(room_code):
LogManager.log("Joining room: " + room_code, LogManager.CATEGORY_UI) _reset_room_join_buttons()
is_joining_attempt = false
return
LogManager.log("Joining room: " + room_code, LogManager.CATEGORY_UI)
func _on_network_mode_changed(index: int): func _on_network_mode_changed(index: int):
# On web builds, index 0 = WebRTC, index 1 = WebSocket # On web builds, index 0 = WebRTC, index 1 = WebSocket
@@ -648,6 +683,9 @@ func _on_connection_succeeded():
func _on_connection_failed(): func _on_connection_failed():
LogManager.log("Connection failed", LogManager.CATEGORY_UI) LogManager.log("Connection failed", LogManager.CATEGORY_UI)
# Always reset room Join buttons on failure (they may be stuck on "Joining...")
if is_joining_attempt:
_reset_room_join_buttons()
if connection_error_shown: if connection_error_shown:
# Already shown, don't spam # Already shown, don't spam
return return

View File

@@ -1127,6 +1127,31 @@ func _show_loot_floating_text(player: Node, text: String, color: Color, item_tex
floating_text.global_position = Vector2(player.global_position.x, player.global_position.y - 20) floating_text.global_position = Vector2(player.global_position.x, player.global_position.y - 20)
floating_text.setup(text, color, 0.5, 0.5, item_texture, sprite_hframes, sprite_vframes, sprite_frame) floating_text.setup(text, color, 0.5, 0.5, item_texture, sprite_hframes, sprite_vframes, sprite_frame)
func _apply_heal_spell_sync(target_name: String, amount: float):
var target: Node = null
for p in get_tree().get_nodes_in_group("player"):
if p.name == target_name and is_instance_valid(p):
target = p
break
if not target:
return
var me = multiplayer.get_unique_id()
var tid = target.get_multiplayer_authority()
if me == tid and target.has_method("heal"):
target.heal(amount)
var entities = get_node_or_null("Entities")
var parent = entities if entities else target.get_parent()
if not parent:
return
var eff_scene = load("res://scenes/healing_effect.tscn") as PackedScene
if eff_scene:
var eff = eff_scene.instantiate()
parent.add_child(eff)
eff.global_position = target.global_position
if eff.has_method("setup"):
eff.setup(target)
_show_loot_floating_text(target, "+" + str(int(amount)) + " HP", Color.GREEN, null, 1, 1, 0)
@rpc("authority", "unreliable") @rpc("authority", "unreliable")
func _sync_enemy_position(enemy_name: String, enemy_index: int, pos: Vector2, vel: Vector2, z_pos: float, dir: int, frame: int, anim: String, frame_num: int, state_value: int): func _sync_enemy_position(enemy_name: String, enemy_index: int, pos: Vector2, vel: Vector2, z_pos: float, dir: int, frame: int, anim: String, frame_num: int, state_value: int):
# Clients receive enemy position updates from server # Clients receive enemy position updates from server
@@ -1844,15 +1869,15 @@ func _update_mouse_cursor(delta: float):
if not spell_cursor_sprite or not is_instance_valid(spell_cursor_sprite): if not spell_cursor_sprite or not is_instance_valid(spell_cursor_sprite):
return return
# Check if player is charging a spell
var is_charging_spell = false var is_charging_spell = false
var spell_element = "fire" # Default to fire, can be extended later var spell_element = "fire"
if local_players.size() > 0: if local_players.size() > 0:
var player = local_players[0] var player = local_players[0]
if player and is_instance_valid(player) and player.is_local_player: if player and is_instance_valid(player) and player.is_local_player:
if "is_charging_spell" in player: if "is_charging_spell" in player:
is_charging_spell = player.is_charging_spell is_charging_spell = player.is_charging_spell
# TODO: Get spell element from player/equipment when element system is added if "current_spell_element" in player:
spell_element = player.current_spell_element
# Update pulse time for grid cursor color animation # Update pulse time for grid cursor color animation
cursor_pulse_time += delta * CURSOR_PULSE_SPEED cursor_pulse_time += delta * CURSOR_PULSE_SPEED
@@ -1895,11 +1920,16 @@ func _update_mouse_cursor(delta: float):
# Update spell cursor if charging spell # Update spell cursor if charging spell
if is_charging_spell: if is_charging_spell:
# Hide normal grid cursor
grid_cursor_sprite.visible = false grid_cursor_sprite.visible = false
var spell_target_pos = Vector2.ZERO
# Show spell cursor at valid spell target position if spell_element == "healing" and local_players.size() > 0:
var spell_target_pos = _get_valid_spell_target_position(world_pos) var cp = local_players[0]
if cp and is_instance_valid(cp) and cp.has_method("_get_heal_target"):
var ht = cp._get_heal_target()
if ht and is_instance_valid(ht):
spell_target_pos = ht.global_position
else:
spell_target_pos = _get_valid_spell_target_position(world_pos)
if spell_target_pos != Vector2.ZERO: if spell_target_pos != Vector2.ZERO:
spell_cursor_sprite.visible = true spell_cursor_sprite.visible = true
# Convert world position to screen position # Convert world position to screen position
@@ -1912,6 +1942,10 @@ func _update_mouse_cursor(delta: float):
match spell_element: match spell_element:
"fire": "fire":
spell_cursor_sprite.modulate = Color(1.0, 0.3, 0.3, 0.5) # Red spell_cursor_sprite.modulate = Color(1.0, 0.3, 0.3, 0.5) # Red
"healing":
spell_cursor_sprite.modulate = Color(0.3, 1.0, 0.35, 0.5) # Green
"frost":
spell_cursor_sprite.modulate = Color(0.3, 0.6, 1.0, 0.5) # Blue
"water", "ice": "water", "ice":
spell_cursor_sprite.modulate = Color(0.3, 0.5, 1.0, 0.5) # Blue spell_cursor_sprite.modulate = Color(0.3, 0.5, 1.0, 0.5) # Blue
"electric": "electric":
@@ -2068,6 +2102,19 @@ func _is_valid_spell_target(target_pos: Vector2, player_pos: Vector2) -> bool:
return true return true
func _get_adjacent_valid_spell_tile_centers(center_world_pos: Vector2, player_pos: Vector2) -> Array:
var out: Array = []
if not dungeon_tilemap_layer or dungeon_data.is_empty() or not dungeon_data.has("grid"):
return out
var center_tile = dungeon_tilemap_layer.local_to_map(center_world_pos - dungeon_tilemap_layer.global_position)
var offsets = [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]
for off in offsets:
var t = center_tile + off
var tile_center = dungeon_tilemap_layer.map_to_local(t) + dungeon_tilemap_layer.global_position
if _is_valid_spell_target(tile_center, player_pos):
out.append(tile_center)
return out
func _init_fog_of_war(): func _init_fog_of_war():
if dungeon_data.is_empty() or not dungeon_data.has("map_size"): if dungeon_data.is_empty() or not dungeon_data.has("map_size"):
return return
@@ -2165,87 +2212,85 @@ func _update_fog_of_war(delta: float) -> void:
if local_player_list.size() > 0 and local_player_list[0]: if local_player_list.size() > 0 and local_player_list[0]:
var p_tile = Vector2i(int(local_player_list[0].global_position.x / FOG_TILE_SIZE), int(local_player_list[0].global_position.y / FOG_TILE_SIZE)) var p_tile = Vector2i(int(local_player_list[0].global_position.x / FOG_TILE_SIZE), int(local_player_list[0].global_position.y / FOG_TILE_SIZE))
current_room = _find_room_at_tile(p_tile) current_room = _find_room_at_tile(p_tile)
if not current_room.is_empty(): if not current_room.is_empty():
_mark_room_explored(current_room) _mark_room_explored(current_room)
_mark_room_visible(current_room) _mark_room_visible(current_room)
for y in range(map_size.y):
for x in range(map_size.x):
if not _is_tile_in_room_or_walls(Vector2i(x, y), current_room):
var idx = x + y * map_size.x
if idx >= 0 and idx < combined_seen.size():
combined_seen[idx] = 0
else:
# In corridors (no room), only show tiles connected to the corridor component
# AND explicitly clear combined_seen for all tiles in rooms that aren't connected
var player_tile = Vector2i(int(local_player_list[0].global_position.x / FOG_TILE_SIZE), int(local_player_list[0].global_position.y / FOG_TILE_SIZE))
# Cache corridor data - only rebuild if player moved more than 1 tile
var should_rebuild_corridor = cached_corridor_mask.is_empty() or cached_corridor_player_tile.distance_to(player_tile) > 1
var corridor_mask: PackedInt32Array
var corridor_rooms: Array
var allowed_room_ids: Dictionary
if should_rebuild_corridor:
# Rebuild corridor mask and rooms (expensive operation)
cached_corridor_mask = _build_corridor_mask(player_tile)
cached_corridor_rooms = _get_rooms_connected_to_corridor(cached_corridor_mask, player_tile)
cached_corridor_player_tile = player_tile
# Build a set of allowed room IDs for fast lookup
cached_corridor_allowed_room_ids = {}
for room in cached_corridor_rooms:
var room_id = str(room.x) + "," + str(room.y) + "," + str(room.w) + "," + str(room.h)
cached_corridor_allowed_room_ids[room_id] = true
# Use the rebuilt data
corridor_mask = cached_corridor_mask
corridor_rooms = cached_corridor_rooms
allowed_room_ids = cached_corridor_allowed_room_ids
else:
# Use cached data (much faster!)
corridor_mask = cached_corridor_mask
corridor_rooms = cached_corridor_rooms
allowed_room_ids = cached_corridor_allowed_room_ids
# Check explored rooms and mark them visible
for room in corridor_rooms:
# If this room was previously explored, mark the entire room (including outer walls) as visible
var was_explored = false
for x in range(room.x - 2, room.x + room.w + 2):
for y in range(room.y - 2, room.y + room.h + 2):
if x < 0 or y < 0 or x >= map_size.x or y >= map_size.y:
continue
var idx = x + y * map_size.x
if idx >= 0 and idx < explored_map.size() and explored_map[idx] == 1:
was_explored = true
break
if was_explored:
break
if was_explored:
_mark_room_visible(room)
# Explicitly clear combined_seen for ANY tile not in corridor mask or allowed rooms
# OPTIMIZATION: Only do expensive per-tile checks when corridor state changed or player moved significantly
var needs_tile_clear = corridor_state_changed or should_rebuild_corridor
if needs_tile_clear:
for y in range(map_size.y): for y in range(map_size.y):
for x in range(map_size.x): for x in range(map_size.x):
var idx = x + y * map_size.x if not _is_tile_in_room_or_walls(Vector2i(x, y), current_room):
if idx < 0 or idx >= combined_seen.size(): var idx = x + y * map_size.x
continue if idx >= 0 and idx < combined_seen.size():
var tile_in_corridor = idx < corridor_mask.size() and corridor_mask[idx] == 1 combined_seen[idx] = 0
# Check if this tile is in a room, and if so, is it an allowed room? else:
var tile_room = _find_room_at_tile(Vector2i(x, y)) # In corridors (no room), only show tiles connected to the corridor component
var in_allowed_room = false # AND explicitly clear combined_seen for all tiles in rooms that aren't connected
if not tile_room.is_empty(): var player_tile = Vector2i(int(local_player_list[0].global_position.x / FOG_TILE_SIZE), int(local_player_list[0].global_position.y / FOG_TILE_SIZE))
var room_id = str(tile_room.x) + "," + str(tile_room.y) + "," + str(tile_room.w) + "," + str(tile_room.h)
in_allowed_room = allowed_room_ids.has(room_id) # Cache corridor data - only rebuild if player moved more than 1 tile
# Clear combined_seen for any tile not in corridor or allowed rooms var should_rebuild_corridor = cached_corridor_mask.is_empty() or cached_corridor_player_tile.distance_to(player_tile) > 1
if not tile_in_corridor and not in_allowed_room:
combined_seen[idx] = 0 var corridor_mask: PackedInt32Array
var corridor_rooms: Array
# Update last corridor fog update time var allowed_room_ids: Dictionary
last_corridor_fog_update = Time.get_ticks_msec() / 1000.0
if should_rebuild_corridor:
# Rebuild corridor mask and rooms (expensive operation)
cached_corridor_mask = _build_corridor_mask(player_tile)
cached_corridor_rooms = _get_rooms_connected_to_corridor(cached_corridor_mask, player_tile)
cached_corridor_player_tile = player_tile
# Build a set of allowed room IDs for fast lookup
cached_corridor_allowed_room_ids = {}
for room in cached_corridor_rooms:
var room_id = str(room.x) + "," + str(room.y) + "," + str(room.w) + "," + str(room.h)
cached_corridor_allowed_room_ids[room_id] = true
# Use the rebuilt data
corridor_mask = cached_corridor_mask
corridor_rooms = cached_corridor_rooms
allowed_room_ids = cached_corridor_allowed_room_ids
else:
# Use cached data (much faster!)
corridor_mask = cached_corridor_mask
corridor_rooms = cached_corridor_rooms
allowed_room_ids = cached_corridor_allowed_room_ids
# Check explored rooms and mark them visible
for room in corridor_rooms:
# If this room was previously explored, mark the entire room (including outer walls) as visible
var was_explored = false
for x in range(room.x - 2, room.x + room.w + 2):
for y in range(room.y - 2, room.y + room.h + 2):
if x < 0 or y < 0 or x >= map_size.x or y >= map_size.y:
continue
var idx = x + y * map_size.x
if idx >= 0 and idx < explored_map.size() and explored_map[idx] == 1:
was_explored = true
break
if was_explored:
break
if was_explored:
_mark_room_visible(room)
# Explicitly clear combined_seen for ANY tile not in corridor mask or allowed rooms
# OPTIMIZATION: Only do expensive per-tile checks when corridor state changed or player moved significantly
var needs_tile_clear = corridor_state_changed or should_rebuild_corridor
if needs_tile_clear:
for y in range(map_size.y):
for x in range(map_size.x):
var idx = x + y * map_size.x
if idx < 0 or idx >= combined_seen.size():
continue
var tile_in_corridor = idx < corridor_mask.size() and corridor_mask[idx] == 1
# Check if this tile is in a room, and if so, is it an allowed room?
var tile_room = _find_room_at_tile(Vector2i(x, y))
var in_allowed_room = false
if not tile_room.is_empty():
var room_id = str(tile_room.x) + "," + str(tile_room.y) + "," + str(tile_room.w) + "," + str(tile_room.h)
in_allowed_room = allowed_room_ids.has(room_id)
# Clear combined_seen for any tile not in corridor or allowed rooms
if not tile_in_corridor and not in_allowed_room:
combined_seen[idx] = 0
# Update last corridor fog update time
last_corridor_fog_update = Time.get_ticks_msec() / 1000.0
if fog_node.has_method("set_maps"): if fog_node.has_method("set_maps"):
fog_node.set_maps(explored_map, combined_seen) fog_node.set_maps(explored_map, combined_seen)
@@ -6491,6 +6536,19 @@ func _load_hud():
print("GameWorld: HUD loaded successfully and added to scene tree") print("GameWorld: HUD loaded successfully and added to scene tree")
print("GameWorld: HUD visible: ", hud.visible, ", layer: ", hud.layer) print("GameWorld: HUD visible: ", hud.visible, ", layer: ", hud.layer)
print("GameWorld: HUD is_inside_tree: ", hud.is_inside_tree()) print("GameWorld: HUD is_inside_tree: ", hud.is_inside_tree())
_load_off_screen_indicators()
func _load_off_screen_indicators():
var existing = get_node_or_null("OffScreenIndicators")
if existing and is_instance_valid(existing):
return
var scene = load("res://scenes/off_screen_indicators.tscn") as PackedScene
if not scene:
return
var layer = scene.instantiate()
layer.name = "OffScreenIndicators"
add_child(layer)
func _initialize_hud(): func _initialize_hud():
# Find or get the HUD and reset its level timer # Find or get the HUD and reset its level timer

View File

@@ -0,0 +1,97 @@
extends Node2D
# Visual effect when a player is healed by Tome of Healing.
# Plays frames 589-598 from shade_spell_effects, green PointLight2D flicker,
# and pulses target player sprites green for a short duration.
const DURATION: float = 1.2
var _frames: Array = [589, 590, 591, 592, 593, 594, 595, 596, 597, 598]
const HEAL_PULSE_TINT: Color = Color(0.35, 1.5, 0.4, 1.0)
var target_player: Node = null
var elapsed: float = 0.0
var _original_tints: Dictionary = {}
@onready var fx_sprite: Sprite2D = $FxSprite
@onready var heal_light: PointLight2D = $HealLight
func _ready() -> void:
pass
func setup(p_target: Node) -> void:
target_player = p_target
if not target_player or not is_instance_valid(target_player):
queue_free()
return
global_position = target_player.global_position
elapsed = 0.0
if fx_sprite and _frames.size() > 0:
fx_sprite.frame = _frames[0]
fx_sprite.modulate = Color(0.4, 1.0, 0.5)
if heal_light:
heal_light.color = Color(0.3, 1.0, 0.35)
heal_light.energy = 0.8
heal_light.enabled = true
func _process(delta: float) -> void:
if not target_player or not is_instance_valid(target_player):
queue_free()
return
elapsed += delta
global_position = target_player.global_position
# Animate frames 589-598
if fx_sprite and _frames.size() > 0:
var idx = int((elapsed / DURATION) * float(_frames.size())) % _frames.size()
fx_sprite.frame = _frames[idx]
# Flicker light
if heal_light:
var flicker = 0.7 + 0.4 * sin(elapsed * 18.0) * cos(elapsed * 7.0)
heal_light.energy = clamp(flicker, 0.3, 1.2)
# Pulse green on target sprites
_apply_heal_pulse()
if elapsed >= DURATION:
_clear_heal_pulse()
queue_free()
func _sprites_for_target() -> Array:
if not target_player:
return []
var names = ["Sprite2DBody", "Sprite2DBoots", "Sprite2DArmour", "Sprite2DFacialHair", "Sprite2DHair", "Sprite2DEyes", "Sprite2DEyeLashes", "Sprite2DAddons", "Sprite2DHeadgear", "Sprite2DShield", "Sprite2DShieldHolding", "Sprite2DWeapon"]
var out: Array = []
for n in names:
var node = target_player.get_node_or_null(n)
if node and node is Sprite2D:
out.append({"sprite": node, "name": n})
return out
func _apply_heal_pulse() -> void:
var pulse = (sin(elapsed * 12.0) + 1.0) * 0.5 # 0..1 oscillating
for s in _sprites_for_target():
var sprite: Sprite2D = s.sprite
var key: String = s.name
if not sprite or not is_instance_valid(sprite) or not sprite.material or not (sprite.material is ShaderMaterial):
continue
var sm: ShaderMaterial = sprite.material as ShaderMaterial
if not _original_tints.has(key):
var tp = sm.get_shader_parameter("tint")
if tp is Vector4:
_original_tints[key] = Color(tp.x, tp.y, tp.z, tp.w)
elif tp is Color:
_original_tints[key] = tp
else:
_original_tints[key] = Color.WHITE
var orig: Color = _original_tints[key]
var green_tint = Color(orig.r * HEAL_PULSE_TINT.r, orig.g * HEAL_PULSE_TINT.g, orig.b * HEAL_PULSE_TINT.b, orig.a * HEAL_PULSE_TINT.a)
var cur = orig.lerp(green_tint, pulse * 0.6)
sm.set_shader_parameter("tint", Vector4(cur.r, cur.g, cur.b, cur.a))
func _clear_heal_pulse() -> void:
for s in _sprites_for_target():
var sprite: Sprite2D = s.sprite
var key: String = s.name
if not sprite or not is_instance_valid(sprite) or not sprite.material or not (sprite.material is ShaderMaterial):
continue
if _original_tints.has(key):
var c: Color = _original_tints[key]
(sprite.material as ShaderMaterial).set_shader_parameter("tint", Vector4(c.r, c.g, c.b, c.a))
_original_tints.clear()

View File

@@ -0,0 +1 @@
uid://27wuloudfkme

View File

@@ -533,6 +533,7 @@ func _convert_to_bomb_projectile(by_player, force: Vector2):
# Spawn bomb projectile at current position # Spawn bomb projectile at current position
var bomb = attack_bomb_scene.instantiate() var bomb = attack_bomb_scene.instantiate()
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
@@ -857,6 +858,19 @@ func _open_chest(by_player: Node = null):
# Sync chest open visual with item_data so clients can show the floating text # Sync chest open visual with item_data so clients can show the floating text
var item_data = chest_item.save() if chest_item else {} var item_data = chest_item.save() if chest_item else {}
game_world._rpc_to_ready_peers("_sync_chest_open_by_name", [chest_name, "item", player_peer_id, item_data]) game_world._rpc_to_ready_peers("_sync_chest_open_by_name", [chest_name, "item", player_peer_id, item_data])
# Sync inventory+equipment to joiner (server added item; joiner's client must apply)
if multiplayer.is_server():
var owner_id = by_player.get_multiplayer_authority()
if owner_id != 1 and owner_id != multiplayer.get_unique_id():
var inv_data: Array = []
for inv_item in by_player.character_stats.inventory:
inv_data.append(inv_item.save() if inv_item else null)
var equip_data: Dictionary = {}
for slot_name in by_player.character_stats.equipment.keys():
var eq = by_player.character_stats.equipment[slot_name]
equip_data[slot_name] = eq.save() if eq else null
if by_player.has_method("_apply_inventory_and_equipment_from_server"):
by_player._apply_inventory_and_equipment_from_server.rpc_id(owner_id, inv_data, equip_data)
else: else:
push_error("Chest: ERROR - No valid player to give item to!") push_error("Chest: ERROR - No valid player to give item to!")

View File

@@ -512,6 +512,7 @@ func _update_ui():
sprite.centered = false # Like inspiration system sprite.centered = false # Like inspiration system
sprite.position = Vector2(4, 4) # Like inspiration system sprite.position = Vector2(4, 4) # Like inspiration system
sprite.scale = Vector2(2.0, 2.0) # 2x size as requested sprite.scale = Vector2(2.0, 2.0) # 2x size as requested
ItemDatabase.apply_item_colors_to_sprite(sprite, equipped_item)
button.add_child(sprite) button.add_child(sprite)
# Add quantity label if item can have multiple (like arrows, bombs) # Add quantity label if item can have multiple (like arrows, bombs)
@@ -594,6 +595,7 @@ func _update_ui():
sprite.centered = false # Like inspiration system sprite.centered = false # Like inspiration system
sprite.position = Vector2(4, 4) # Like inspiration system sprite.position = Vector2(4, 4) # Like inspiration system
sprite.scale = Vector2(2.0, 2.0) # 2x size as requested sprite.scale = Vector2(2.0, 2.0) # 2x size as requested
ItemDatabase.apply_item_colors_to_sprite(sprite, item)
button.add_child(sprite) button.add_child(sprite)
# Add quantity label if item quantity > 1 (show for all stacked items) # Add quantity label if item quantity > 1 (show for all stacked items)

View File

@@ -163,105 +163,623 @@ static func _load_all_items():
"rarity": ItemRarity.UNCOMMON "rarity": ItemRarity.UNCOMMON
}) })
# HEADGEAR items (row 1) # HEADGEAR items
_register_item("beanie", { # MageHatRed (frame 9) variants
"item_name": "Beanie", var _mhr_o = [Color(255/255.0,39/255.0,44/255.0), Color(182/255.0,0,0), Color(118/255.0,1/255.0,0), Color(72/255.0,0,12/255.0)]
"description": "A warm beanie", var _mhr_brown = [Color(139/255.0,90/255.0,43/255.0), Color(101/255.0,67/255.0,33/255.0), Color(80/255.0,50/255.0,20/255.0), Color(50/255.0,30/255.0,10/255.0)]
var _mhr_blue = [Color(30/255.0,80/255.0,180/255.0), Color(20/255.0,50/255.0,120/255.0), Color(10/255.0,30/255.0,80/255.0), Color(5/255.0,15/255.0,50/255.0)]
var _mhr_lightblue = [Color(170/255.0,220/255.0,1.0), Color(130/255.0,190/255.0,1.0), Color(90/255.0,150/255.0,220/255.0), Color(50/255.0,100/255.0,180/255.0)]
var _mhr_white = [Color(250/255.0,250/255.0,250/255.0), Color(220/255.0,220/255.0,220/255.0), Color(190/255.0,190/255.0,190/255.0), Color(150/255.0,150/255.0,150/255.0)]
var _shc_o = [Color(0,180/255.0,157/255.0), Color(0,121/255.0,102/255.0), Color(0,79/255.0,67/255.0), Color(0,46/255.0,93/255.0)]
var _shc_red = [Color(180/255.0,40/255.0,40/255.0), Color(130/255.0,0,0), Color(90/255.0,0,0), Color(60/255.0,0,0)]
var _shc_green = [Color(40/255.0,160/255.0,40/255.0), Color(0,120/255.0,0), Color(0,80/255.0,0), Color(0,50/255.0,0)]
var _sb_o = [Color(248/255.0,219/255.0,108/255.0), Color(225/255.0,159/255.0,57/255.0), Color(199/255.0,115/255.0,29/255.0), Color(151/255.0,73/255.0,9/255.0), Color(108/255.0,43/255.0,0), Color(58/255.0,23/255.0,11/255.0), Color(0,76/255.0,218/255.0), Color(9/255.0,35/255.0,105/255.0), Color(0,14/255.0,62/255.0)]
var _sb_iron = [Color(190/255.0,187/255.0,181/255.0), Color(162/255.0,158/255.0,150/255.0), Color(125/255.0,123/255.0,118/255.0), Color(77/255.0,76/255.0,75/255.0), Color(54/255.0,54/255.0,54/255.0), Color(30/255.0,30/255.0,30/255.0), Color(0,76/255.0,218/255.0), Color(9/255.0,35/255.0,105/255.0), Color(0,14/255.0,62/255.0)]
var _sb_steel = [Color(227/255.0,227/255.0,227/255.0), Color(183/255.0,183/255.0,183/255.0), Color(116/255.0,116/255.0,116/255.0), Color(77/255.0,76/255.0,75/255.0), Color(54/255.0,54/255.0,54/255.0), Color(30/255.0,30/255.0,30/255.0), Color(0,76/255.0,218/255.0), Color(9/255.0,35/255.0,105/255.0), Color(0,14/255.0,62/255.0)]
_register_item("hat", {
"item_name": "Hat",
"description": "A simple cloth hat",
"item_type": Item.ItemType.Equippable, "item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR, "equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE, "weapon_type": Item.WeaponType.NONE,
"spriteFrame": 1 * 20 + 10, # 10,1 "spriteFrame": 9,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Headband.png", "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/MageHatRed.png",
"modifiers": {"def": 1},
"buy_cost": 20,
"sell_worth": 6,
"rarity": ItemRarity.COMMON,
"colorReplacements": [{"original": _mhr_o[0], "replace": _mhr_brown[0]}, {"original": _mhr_o[1], "replace": _mhr_brown[1]}, {"original": _mhr_o[2], "replace": _mhr_brown[2]}, {"original": _mhr_o[3], "replace": _mhr_brown[3]}]
})
_register_item("red_hat", {
"item_name": "Red hat",
"description": "A red mage hat",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 9,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/MageHatRed.png",
"modifiers": {"def": 1},
"buy_cost": 25,
"sell_worth": 8,
"rarity": ItemRarity.COMMON
})
_register_item("blue_mages_hat", {
"item_name": "Blue mage's hat",
"description": "A blue mage hat",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 9,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/MageHatRed.png",
"modifiers": {"def": 1},
"buy_cost": 28,
"sell_worth": 9,
"rarity": ItemRarity.COMMON,
"colorReplacements": [{"original": _mhr_o[0], "replace": _mhr_blue[0]}, {"original": _mhr_o[1], "replace": _mhr_blue[1]}, {"original": _mhr_o[2], "replace": _mhr_blue[2]}, {"original": _mhr_o[3], "replace": _mhr_blue[3]}]
})
_register_item("wizards_hat", {
"item_name": "Wizard's hat",
"description": "A light blue wizard hat",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 9,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/MageHatRed.png",
"modifiers": {"def": 1, "wis": 1},
"buy_cost": 55,
"sell_worth": 18,
"rarity": ItemRarity.UNCOMMON,
"colorReplacements": [{"original": _mhr_o[0], "replace": _mhr_lightblue[0]}, {"original": _mhr_o[1], "replace": _mhr_lightblue[1]}, {"original": _mhr_o[2], "replace": _mhr_lightblue[2]}, {"original": _mhr_o[3], "replace": _mhr_lightblue[3]}]
})
_register_item("gandolfs_hat", {
"item_name": "Gandolf's Hat",
"description": "A white wizard hat",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 9,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/MageHatRed.png",
"modifiers": {"def": 1, "wis": 1},
"buy_cost": 60,
"sell_worth": 20,
"rarity": ItemRarity.UNCOMMON,
"colorReplacements": [{"original": _mhr_o[0], "replace": _mhr_white[0]}, {"original": _mhr_o[1], "replace": _mhr_white[1]}, {"original": _mhr_o[2], "replace": _mhr_white[2]}, {"original": _mhr_o[3], "replace": _mhr_white[3]}]
})
_register_item("sorcerors_hood", {
"item_name": "Sorceror's Hood",
"description": "A cyan sorceror hood",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 27,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/SorcererHoodCyan.png",
"modifiers": {"def": 2, "wis": 1},
"buy_cost": 50,
"sell_worth": 16,
"rarity": ItemRarity.COMMON
})
_register_item("red_hood", {
"item_name": "Red Hood",
"description": "A red hood",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 27,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/SorcererHoodCyan.png",
"modifiers": {"def": 2},
"buy_cost": 45,
"sell_worth": 14,
"rarity": ItemRarity.COMMON,
"colorReplacements": [{"original": _shc_o[0], "replace": _shc_red[0]}, {"original": _shc_o[1], "replace": _shc_red[1]}, {"original": _shc_o[2], "replace": _shc_red[2]}, {"original": _shc_o[3], "replace": _shc_red[3]}]
})
_register_item("green_hood", {
"item_name": "Green Hood",
"description": "A green hood",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 27,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/SorcererHoodCyan.png",
"modifiers": {"def": 2, "dex": 1},
"buy_cost": 52,
"sell_worth": 17,
"rarity": ItemRarity.COMMON,
"colorReplacements": [{"original": _shc_o[0], "replace": _shc_green[0]}, {"original": _shc_o[1], "replace": _shc_green[1]}, {"original": _shc_o[2], "replace": _shc_green[2]}, {"original": _shc_o[3], "replace": _shc_green[3]}]
})
_register_item("high_mage_hat", {
"item_name": "High Mage Hat",
"description": "A high mage hat",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 5,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/HighMageHatCyan.png",
"modifiers": {"def": 2, "wis": 1},
"buy_cost": 70,
"sell_worth": 22,
"rarity": ItemRarity.UNCOMMON
})
_register_item("esper_hat", {
"item_name": "Esper Hat",
"description": "An esper hat",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 4,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/EsperHatBlue.png",
"modifiers": {"def": 2, "wis": 1},
"buy_cost": 65,
"sell_worth": 20,
"rarity": ItemRarity.UNCOMMON
})
_register_item("jarl_helm", {
"item_name": "Jarl Helm",
"description": "A jarl's helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 26,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Viking/JarlHelm.png",
"modifiers": {"def": 4, "str": 1},
"buy_cost": 150,
"sell_worth": 48,
"rarity": ItemRarity.RARE
})
_register_item("karl_helm", {
"item_name": "Karl Helm",
"description": "A karl's helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 25,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Viking/KarlHelm.png",
"modifiers": {"def": 3},
"buy_cost": 90,
"sell_worth": 28,
"rarity": ItemRarity.UNCOMMON
})
_register_item("valkyrie_helm", {
"item_name": "Valkyrie Helm",
"description": "A valkyrie helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 24,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Viking/ValkyrieHelm.png",
"modifiers": {"def": 4},
"buy_cost": 130,
"sell_worth": 40,
"rarity": ItemRarity.RARE
})
_register_item("warrior_helm", {
"item_name": "Warrior Helm",
"description": "A warrior helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 23,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Viking/WarriorHelmRed.png",
"modifiers": {"def": 3, "str": 1},
"buy_cost": 100,
"sell_worth": 32,
"rarity": ItemRarity.UNCOMMON
})
_register_item("enforced_warrior_helm", {
"item_name": "Enforced Warrior Helm",
"description": "A reinforced warrior helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 22,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Viking/WarriorHelmYellow.png",
"modifiers": {"def": 4, "str": 1},
"buy_cost": 140,
"sell_worth": 44,
"rarity": ItemRarity.RARE
})
_register_item("cherbi_helm", {
"item_name": "Cherbi Helm",
"description": "A cherbi helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 49,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Mongol/CherbiHelm.png",
"modifiers": {"def": 3},
"buy_cost": 85,
"sell_worth": 26,
"rarity": ItemRarity.UNCOMMON
})
_register_item("khaan_helm", {
"item_name": "Khaan Helm",
"description": "A khaan's helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 48,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Mongol/KhaanHelm.png",
"modifiers": {"def": 5, "str": 1},
"buy_cost": 180,
"sell_worth": 56,
"rarity": ItemRarity.RARE
})
_register_item("kheshig_helm", {
"item_name": "Kheshig Helm",
"description": "A kheshig helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 47,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Mongol/KheshigHelm.png",
"modifiers": {"def": 4},
"buy_cost": 120,
"sell_worth": 38,
"rarity": ItemRarity.RARE
})
_register_item("noyon_helm", {
"item_name": "Noyon Helm",
"description": "A noyon helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 46,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Mongol/NoyonHelm.png",
"modifiers": {"def": 4},
"buy_cost": 115,
"sell_worth": 36,
"rarity": ItemRarity.UNCOMMON
})
_register_item("daimyo_helm", {
"item_name": "Daimyo Helm",
"description": "A daimyo's helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 69,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/DaimyoHelm.png",
"modifiers": {"def": 5, "str": 1},
"buy_cost": 190,
"sell_worth": 60,
"rarity": ItemRarity.RARE
})
_register_item("blue_ninja_bandana", {
"item_name": "Blue Ninja Bandana",
"description": "A blue ninja bandana",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 68,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/NinjaBandanaBlue.png",
"modifiers": {"def": 1, "dex": 1},
"buy_cost": 40,
"sell_worth": 13,
"rarity": ItemRarity.COMMON
})
_register_item("ronin_strawhat", {
"item_name": "Ronin Straw-hat",
"description": "A ronin's straw hat",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 67,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/RoninStrawhatBlue.png",
"modifiers": {"def": 1},
"buy_cost": 22,
"sell_worth": 7,
"rarity": ItemRarity.COMMON
})
_register_item("samurai_helm", {
"item_name": "Samurai Helm",
"description": "A samurai helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 66,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/SamuraiHelm.png",
"modifiers": {"def": 4, "str": 1},
"buy_cost": 145,
"sell_worth": 46,
"rarity": ItemRarity.RARE
})
_register_item("shogun_helm", {
"item_name": "Shogun Helm",
"description": "A shogun's helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 65,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/ShogunHelmPurple.png",
"modifiers": {"def": 5, "str": 1},
"buy_cost": 200,
"sell_worth": 64,
"rarity": ItemRarity.RARE
})
_register_item("straw_hat", {
"item_name": "Straw-hat",
"description": "A simple straw hat",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 64,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/TravellerStrawhat.png",
"modifiers": {"def": 1},
"buy_cost": 18,
"sell_worth": 6,
"rarity": ItemRarity.COMMON
})
_register_item("travelers_strawhat", {
"item_name": "Traveler's Straw-hat",
"description": "A traveler's straw hat",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 63,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/TravellerStrawhatCyan.png",
"modifiers": {"def": 1}, "modifiers": {"def": 1},
"buy_cost": 20, "buy_cost": 20,
"sell_worth": 6, "sell_worth": 6,
"rarity": ItemRarity.COMMON "rarity": ItemRarity.COMMON
}) })
_register_item("villagers_strawhat", {
_register_item("leather_helm", { "item_name": "Villager's Straw-hat",
"item_name": "Leather Helm", "description": "A villager's straw hat",
"description": "A nice leather helm",
"item_type": Item.ItemType.Equippable, "item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR, "equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE, "weapon_type": Item.WeaponType.NONE,
"spriteFrame": 1 * 20 + 11, # 11,1 "spriteFrame": 62,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/NoviceHelm.png", "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/VillagerStrawhat.png",
"modifiers": {"def": 2}, "modifiers": {"def": 1},
"buy_cost": 50, "buy_cost": 16,
"sell_worth": 15, "sell_worth": 5,
"rarity": ItemRarity.COMMON "rarity": ItemRarity.COMMON
}) })
_register_item("yabusame_strawhat", {
_register_item("nomads_helm", { "item_name": "Yabusame Straw-hat",
"item_name": "Nomad's Helm", "description": "A yabusame straw hat",
"description": "A helm for travelers",
"item_type": Item.ItemType.Equippable, "item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR, "equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE, "weapon_type": Item.WeaponType.NONE,
"spriteFrame": 1 * 20 + 12, # 12,1 "spriteFrame": 61,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RangerHatGreen.png", "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/YabusameStrawhatBlue.png",
"modifiers": {"def": 1},
"buy_cost": 24,
"sell_worth": 8,
"rarity": ItemRarity.COMMON
})
_register_item("musketeers_hat", {
"item_name": "Musketeer's Hat",
"description": "A musketeer's hat",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 60,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/French/MusketeerHatPurple.png",
"modifiers": {"def": 2},
"buy_cost": 55,
"sell_worth": 18,
"rarity": ItemRarity.UNCOMMON
})
_register_item("archers_hat", {
"item_name": "Archer's Hat",
"description": "An archer's hat",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 89,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/ArcherHatCyan.png",
"modifiers": {"def": 2, "dex": 1}, "modifiers": {"def": 2, "dex": 1},
"buy_cost": 60, "buy_cost": 58,
"sell_worth": 18, "sell_worth": 18,
"rarity": ItemRarity.COMMON "rarity": ItemRarity.COMMON
}) })
_register_item("hunters_hat", {
_register_item("strong_helm", { "item_name": "Hunter's Hat",
"item_name": "Strong Helm", "description": "A hunter's hat",
"description": "A reinforced helm",
"item_type": Item.ItemType.Equippable, "item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR, "equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE, "weapon_type": Item.WeaponType.NONE,
"spriteFrame": 1 * 20 + 13, # 13,1 "spriteFrame": 88,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/KnightHelm.png", "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/HunterHatRed.png",
"modifiers": {"def": 3}, "modifiers": {"def": 2, "dex": 1},
"buy_cost": 80, "buy_cost": 60,
"sell_worth": 24, "sell_worth": 19,
"rarity": ItemRarity.UNCOMMON "rarity": ItemRarity.COMMON
}) })
_register_item("rogues_hat", {
_register_item("plate_helm", { "item_name": "Rogue's Hat",
"item_name": "Plate Helm", "description": "A rogue's hat",
"description": "Heavy plate helm",
"item_type": Item.ItemType.Equippable, "item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR, "equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE, "weapon_type": Item.WeaponType.NONE,
"spriteFrame": 1 * 20 + 14, # 14,1 "spriteFrame": 87,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierSteelHelmBlue.png", "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RogueHatGreen.png",
"modifiers": {"def": 4}, "modifiers": {"def": 2, "dex": 1},
"buy_cost": 120, "buy_cost": 62,
"sell_worth": 36, "sell_worth": 20,
"rarity": ItemRarity.UNCOMMON "rarity": ItemRarity.COMMON
}) })
_register_item("warriors_helm", { _register_item("dark_knight_helm", {
"item_name": "Warrior's Helm", "item_name": "Dark Knight Helm",
"description": "A helm for true warriors", "description": "A dark knight helm",
"item_type": Item.ItemType.Equippable, "item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR, "equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE, "weapon_type": Item.WeaponType.NONE,
"spriteFrame": 1 * 20 + 15, # 15,1 "spriteFrame": 29,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/DragonKnightHelm.png", "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/DarkKnightHelm.png",
"modifiers": {"def": 5, "str": 1}, "modifiers": {"def": 5, "str": 1},
"buy_cost": 180, "buy_cost": 175,
"sell_worth": 54, "sell_worth": 55,
"rarity": ItemRarity.RARE "rarity": ItemRarity.RARE
}) })
_register_item("dragon_knight_helm", {
"item_name": "Dragon Knight Helm",
"description": "A dragon knight helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 21,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/DragonKnightHelm.png",
"modifiers": {"def": 5, "str": 1},
"buy_cost": 185,
"sell_worth": 58,
"rarity": ItemRarity.RARE
})
_register_item("grunt_helm", {
"item_name": "Grunt Helm",
"description": "A grunt helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 28,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/GruntHelm.png",
"modifiers": {"def": 3},
"buy_cost": 75,
"sell_worth": 24,
"rarity": ItemRarity.COMMON
})
_register_item("knight_helm", {
"item_name": "Knight Helm",
"description": "A knight helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 20,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/KnightHelm.png",
"modifiers": {"def": 4},
"buy_cost": 110,
"sell_worth": 34,
"rarity": ItemRarity.UNCOMMON
})
_register_item("novice_helm", {
"item_name": "Novice Helm",
"description": "A novice helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 45,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/NoviceHelm.png",
"modifiers": {"def": 2},
"buy_cost": 45,
"sell_worth": 14,
"rarity": ItemRarity.COMMON
})
_register_item("paladins_helm", { _register_item("paladins_helm", {
"item_name": "Paladin's Helm", "item_name": "Paladin's Helm",
"description": "A blessed paladin helm", "description": "A blessed paladin helm",
"item_type": Item.ItemType.Equippable, "item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR, "equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE, "weapon_type": Item.WeaponType.NONE,
"spriteFrame": 1 * 20 + 16, # 16,1 "spriteFrame": 44,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/PaladinHelmCyan.png", "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/PaladinHelmCyan.png",
"modifiers": {"def": 5, "wis": 1}, "modifiers": {"def": 5, "wis": 1},
"buy_cost": 200, "buy_cost": 200,
"sell_worth": 60, "sell_worth": 60,
"rarity": ItemRarity.RARE "rarity": ItemRarity.RARE
}) })
_register_item("scout_helm", {
"item_name": "Scout Helm",
"description": "A scout helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 43,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/ScoutHelmGreen.png",
"modifiers": {"def": 3, "dex": 1},
"buy_cost": 95,
"sell_worth": 30,
"rarity": ItemRarity.UNCOMMON
})
_register_item("soldier_bronze_helm", {
"item_name": "Soldier Bronze Helm",
"description": "A bronze soldier helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 42,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierBronzeHelmBlue.png",
"modifiers": {"def": 4},
"buy_cost": 105,
"sell_worth": 32,
"rarity": ItemRarity.UNCOMMON
})
_register_item("soldier_iron_helm", {
"item_name": "Soldier Iron Helm",
"description": "An iron soldier helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 42,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierBronzeHelmBlue.png",
"modifiers": {"def": 4},
"buy_cost": 110,
"sell_worth": 34,
"rarity": ItemRarity.UNCOMMON,
"colorReplacements": [{"original": _sb_o[0], "replace": _sb_iron[0]}, {"original": _sb_o[1], "replace": _sb_iron[1]}, {"original": _sb_o[2], "replace": _sb_iron[2]}, {"original": _sb_o[3], "replace": _sb_iron[3]}, {"original": _sb_o[4], "replace": _sb_iron[4]}, {"original": _sb_o[5], "replace": _sb_iron[5]}, {"original": _sb_o[6], "replace": _sb_iron[6]}, {"original": _sb_o[7], "replace": _sb_iron[7]}, {"original": _sb_o[8], "replace": _sb_iron[8]}]
})
_register_item("soldier_steel_helm", {
"item_name": "Soldier Steel Helm",
"description": "A steel soldier helm",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 42,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierBronzeHelmBlue.png",
"modifiers": {"def": 4},
"buy_cost": 125,
"sell_worth": 38,
"rarity": ItemRarity.UNCOMMON,
"colorReplacements": [{"original": _sb_o[0], "replace": _sb_steel[0]}, {"original": _sb_o[1], "replace": _sb_steel[1]}, {"original": _sb_o[2], "replace": _sb_steel[2]}, {"original": _sb_o[3], "replace": _sb_steel[3]}, {"original": _sb_o[4], "replace": _sb_steel[4]}, {"original": _sb_o[5], "replace": _sb_steel[5]}, {"original": _sb_o[6], "replace": _sb_steel[6]}, {"original": _sb_o[7], "replace": _sb_steel[7]}, {"original": _sb_o[8], "replace": _sb_steel[8]}]
})
_register_item("assassin_bandana", {
"item_name": "Assassin Bandana",
"description": "An assassin bandana",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 6,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/AssassinBandanaBlack.png",
"modifiers": {"def": 1, "dex": 1},
"buy_cost": 48,
"sell_worth": 15,
"rarity": ItemRarity.COMMON
})
_register_item("stalker_hood", {
"item_name": "Stalker Hood",
"description": "A stalker hood",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 7,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/StalkerHoodBlack.png",
"modifiers": {"def": 2, "dex": 1},
"buy_cost": 72,
"sell_worth": 22,
"rarity": ItemRarity.UNCOMMON
})
_register_item("thief_bandana", {
"item_name": "Thief Bandana",
"description": "A thief bandana",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.HEADGEAR,
"weapon_type": Item.WeaponType.NONE,
"spriteFrame": 8,
"equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/ThiefBandanaGreen.png",
"modifiers": {"def": 1, "dex": 1},
"buy_cost": 42,
"sell_worth": 13,
"rarity": ItemRarity.COMMON
})
# ACCESSORY items (row 1) # ACCESSORY items (row 1)
_register_item("amulet_of_strength", { _register_item("amulet_of_strength", {
"item_name": "Amulet of Strength", "item_name": "Amulet of Strength",
@@ -1219,7 +1737,21 @@ static func _load_all_items():
}) })
# SPELLBOOKS (row 11, columns 13-14) # SPELLBOOKS (row 11, columns 13-14)
# Sprite 233 = 11 * 20 + 13 # Sprite 233 = 11 * 20 + 13 — same base as Tome of Healing, blue colorReplacements
var _tf_o = [
Color(225.0/255.0, 130.0/255.0, 137.0/255.0),
Color(174.0/255.0, 108.0/255.0, 55.0/255.0),
Color(245.0/255.0, 183.0/255.0, 132.0/255.0),
Color(130.0/255.0, 60.0/255.0, 61.0/255.0),
Color(197.0/255.0, 151.0/255.0, 130.0/255.0)
]
var _tf_blue = [
Color(0.35, 0.6, 0.95),
Color(0.2, 0.4, 0.75),
Color(0.5, 0.75, 1.0),
Color(0.15, 0.35, 0.6),
Color(0.4, 0.6, 0.85)
]
_register_item("tome_of_frostspike", { _register_item("tome_of_frostspike", {
"item_name": "Tome of Frostspike", "item_name": "Tome of Frostspike",
"description": "A spellbook containing frost magic", "description": "A spellbook containing frost magic",
@@ -1233,7 +1765,11 @@ static func _load_all_items():
"weight": 1.5, "weight": 1.5,
"rarity": ItemRarity.UNCOMMON, "rarity": ItemRarity.UNCOMMON,
"colorReplacements": [ "colorReplacements": [
{"old_color": Color(1.0, 1.0, 1.0), "new_color": Color(0.7, 0.9, 1.0)} # Light blue tint for frost {"original": _tf_o[0], "replace": _tf_blue[0]},
{"original": _tf_o[1], "replace": _tf_blue[1]},
{"original": _tf_o[2], "replace": _tf_blue[2]},
{"original": _tf_o[3], "replace": _tf_blue[3]},
{"original": _tf_o[4], "replace": _tf_blue[4]}
] ]
}) })
@@ -1254,6 +1790,42 @@ static func _load_all_items():
{"old_color": Color(1.0, 1.0, 1.0), "new_color": Color(1.0, 0.8, 0.6)} # Warm orange tint for fire {"old_color": Color(1.0, 1.0, 1.0), "new_color": Color(1.0, 0.8, 0.6)} # Warm orange tint for fire
] ]
}) })
# Tome of Healing - frame 233 (11*20+13), green colorReplacements
var _th_o = [
Color(225.0/255.0, 130.0/255.0, 137.0/255.0),
Color(174.0/255.0, 108.0/255.0, 55.0/255.0),
Color(245.0/255.0, 183.0/255.0, 132.0/255.0),
Color(130.0/255.0, 60.0/255.0, 61.0/255.0),
Color(197.0/255.0, 151.0/255.0, 130.0/255.0)
]
var _th_green = [
Color(0.35, 0.85, 0.4),
Color(0.2, 0.6, 0.25),
Color(0.5, 0.95, 0.55),
Color(0.15, 0.5, 0.2),
Color(0.4, 0.75, 0.45)
]
_register_item("tome_of_healing", {
"item_name": "Tome of Healing",
"description": "A spellbook containing healing magic",
"item_type": Item.ItemType.Equippable,
"equipment_type": Item.EquipmentType.OFFHAND,
"weapon_type": Item.WeaponType.SPELLBOOK,
"spriteFrame": 233,
"modifiers": {},
"buy_cost": 100,
"sell_worth": 30,
"weight": 1.5,
"rarity": ItemRarity.UNCOMMON,
"colorReplacements": [
{"original": _th_o[0], "replace": _th_green[0]},
{"original": _th_o[1], "replace": _th_green[1]},
{"original": _th_o[2], "replace": _th_green[2]},
{"original": _th_o[3], "replace": _th_green[3]},
{"original": _th_o[4], "replace": _th_green[4]}
]
})
# BOMB item (sprite index 199 = row 9, col 19) # BOMB item (sprite index 199 = row 9, col 19)
_register_item("bomb", { _register_item("bomb", {
@@ -1443,3 +2015,35 @@ static func get_random_container_item() -> Item:
rarity = ItemRarity.RARE rarity = ItemRarity.RARE
return get_random_item_by_rarity(rarity) return get_random_item_by_rarity(rarity)
# Ensure sprite uses cloth shader and apply item colorReplacements/tint (same as loot + character sprites).
# Use for inventory UI, loot, or any item icon sprite.
static func apply_item_colors_to_sprite(sprite: Sprite2D, item: Item) -> void:
if not sprite:
return
var shader_res = load("res://shaders/cloth.gdshader") as Shader
if not shader_res:
return
var mat: ShaderMaterial
if sprite.material is ShaderMaterial:
mat = sprite.material as ShaderMaterial
if mat.shader != shader_res:
mat = ShaderMaterial.new()
mat.shader = shader_res
sprite.material = mat
else:
mat = ShaderMaterial.new()
mat.shader = shader_res
sprite.material = mat
for i in range(7):
mat.set_shader_parameter("original_" + str(i), Color(0, 0, 0, 0))
mat.set_shader_parameter("replace_" + str(i), Color(0, 0, 0, 0))
mat.set_shader_parameter("tint", Color(1, 1, 1, 1))
if item and item.colorReplacements and item.colorReplacements.size() > 0:
for idx in range(item.colorReplacements.size()):
if idx >= 7:
break
var cr: Dictionary = item.colorReplacements[idx]
if cr.get("original") != null and cr.get("replace") != null:
mat.set_shader_parameter("original_" + str(idx), cr["original"] as Color)
mat.set_shader_parameter("replace_" + str(idx), cr["replace"] as Color)

View File

@@ -8,7 +8,7 @@ enum LootType {
BANANA, BANANA,
CHERRY, CHERRY,
KEY, KEY,
ITEM # Item instance (equipment, consumables, etc.) ITEM # Item instance (equipment, consumables, etc.)
} }
@export var loot_type: LootType = LootType.COIN @export var loot_type: LootType = LootType.COIN
@@ -41,7 +41,7 @@ var correction_smoothing: float = 0.3 # Lerp factor for smooth correction (0-1,
var coin_value: int = 1 var coin_value: int = 1
var heal_amount: float = 20.0 var heal_amount: float = 20.0
var collected: bool = false var collected: bool = false
var item: Item = null # Item instance (for LootType.ITEM) var item: Item = null # Item instance (for LootType.ITEM)
@onready var sprite = $Sprite2D @onready var sprite = $Sprite2D
@onready var shadow = $Shadow @onready var shadow = $Shadow
@@ -152,6 +152,7 @@ func _setup_sprite():
sprite.hframes = item.spriteFrames.x if item.spriteFrames.x > 0 else 20 sprite.hframes = item.spriteFrames.x if item.spriteFrames.x > 0 else 20
sprite.vframes = item.spriteFrames.y if item.spriteFrames.y > 0 else 14 sprite.vframes = item.spriteFrames.y if item.spriteFrames.y > 0 else 14
sprite.frame = item.spriteFrame sprite.frame = item.spriteFrame
ItemDatabase.apply_item_colors_to_sprite(sprite, item)
print("Loot: Set up item sprite for ", item.item_name, " frame=", sprite.frame) print("Loot: Set up item sprite for ", item.item_name, " frame=", sprite.frame)
# Add quantity badge if quantity > 1 # Add quantity badge if quantity > 1
@@ -194,8 +195,8 @@ func _create_quantity_badge(quantity: int):
quantity_badge.add_theme_color_override("font_color", Color.WHITE) quantity_badge.add_theme_color_override("font_color", Color.WHITE)
quantity_badge.add_theme_color_override("font_outline_color", Color.BLACK) quantity_badge.add_theme_color_override("font_outline_color", Color.BLACK)
quantity_badge.add_theme_constant_override("outline_size", 2) quantity_badge.add_theme_constant_override("outline_size", 2)
quantity_badge.z_index = 100 # Above the sprite quantity_badge.z_index = 100 # Above the sprite
quantity_badge.position = Vector2(6, -8) # Bottom right of sprite quantity_badge.position = Vector2(6, -8) # Bottom right of sprite
add_child(quantity_badge) add_child(quantity_badge)
func _physics_process(delta): func _physics_process(delta):
@@ -240,7 +241,7 @@ func _physics_process(delta):
bounce_timer = 0.08 # Matches old code timing bounce_timer = 0.08 # Matches old code timing
# Simple bounce (matches old code) # Simple bounce (matches old code)
velocity_z = -velocity_z * bounce_restitution velocity_z = - velocity_z * bounce_restitution
is_airborne = true # Still bouncing is_airborne = true # Still bouncing
else: else:
# Velocity too small or collected - stop bouncing # Velocity too small or collected - stop bouncing
@@ -267,9 +268,9 @@ func _physics_process(delta):
if collider and not collider.is_in_group("player"): if collider and not collider.is_in_group("player"):
# Check if velocity is too small before bouncing (prevent infinite micro-bounces) # Check if velocity is too small before bouncing (prevent infinite micro-bounces)
var velocity_magnitude = velocity.length() var velocity_magnitude = velocity.length()
if velocity_magnitude < 15.0: # If velocity is very small, stop bouncing if velocity_magnitude < 15.0: # If velocity is very small, stop bouncing
velocity = Vector2.ZERO velocity = Vector2.ZERO
continue # Skip bounce and sound continue # Skip bounce and sound
# Bounce off walls (matches old code - no aggressive velocity reduction) # Bounce off walls (matches old code - no aggressive velocity reduction)
var normal = collision.get_normal() var normal = collision.get_normal()
@@ -329,7 +330,7 @@ func _physics_process(delta):
sfx_coin_bounce.volume_db = -1.0 + (-10.0 - (abs(velocity_z) * 0.1)) sfx_coin_bounce.volume_db = -1.0 + (-10.0 - (abs(velocity_z) * 0.1))
sfx_coin_bounce.play() sfx_coin_bounce.play()
bounce_timer = 0.08 bounce_timer = 0.08
velocity_z = -velocity_z * bounce_restitution velocity_z = - velocity_z * bounce_restitution
is_airborne = true is_airborne = true
else: else:
velocity_z = 0.0 velocity_z = 0.0
@@ -405,7 +406,7 @@ func _on_pickup_area_body_entered(body):
var dropped_by_peer_id = get_meta("dropped_by_peer_id") var dropped_by_peer_id = get_meta("dropped_by_peer_id")
var drop_time = get_meta("drop_time") var drop_time = get_meta("drop_time")
var current_time = Time.get_ticks_msec() var current_time = Time.get_ticks_msec()
var time_since_drop = (current_time - drop_time) / 1000.0 # Convert to seconds var time_since_drop = (current_time - drop_time) / 1000.0 # Convert to seconds
# Check if this player dropped the item and cooldown hasn't expired # Check if this player dropped the item and cooldown hasn't expired
if body.has_method("get_multiplayer_authority"): if body.has_method("get_multiplayer_authority"):
@@ -465,7 +466,6 @@ func _pickup(player: Node):
func _process_pickup_on_server(player: Node): func _process_pickup_on_server(player: Node):
# Internal function to process pickup on server (called from _request_pickup RPC) # Internal function to process pickup on server (called from _request_pickup RPC)
# This skips the authority check since we've already validated the request # This skips the authority check since we've already validated the request
# Mark as collected immediately to prevent duplicate pickups # Mark as collected immediately to prevent duplicate pickups
# (Note: This may already be set by _request_pickup, but set it here too for safety) # (Note: This may already be set by _request_pickup, but set it here too for safety)
if not collected: if not collected:
@@ -624,16 +624,30 @@ func _process_pickup_on_server(player: Node):
player.character_stats.add_item(item) player.character_stats.add_item(item)
print(name, " picked up item: ", item.item_name, " (added to inventory)") print(name, " picked up item: ", item.item_name, " (added to inventory)")
# 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) # Show floating text with item name (uppercase)
var items_texture = load(item.spritePath) var items_texture = load(item.spritePath)
var display_text = item.item_name.to_upper() # Always uppercase var display_text = item.item_name.to_upper() # Always uppercase
var text_color = Color.WHITE var text_color = Color.WHITE
# Color code based on item type # Color code based on item type
if item.item_type == Item.ItemType.Equippable: if item.item_type == Item.ItemType.Equippable:
text_color = Color.CYAN # Cyan for equipment text_color = Color.CYAN # Cyan for equipment
elif item.item_type == Item.ItemType.Restoration: elif item.item_type == Item.ItemType.Restoration:
text_color = Color.GREEN # Green for consumables text_color = Color.GREEN # Green for consumables
_show_floating_text(player, display_text, text_color, 0.5, 0.5, items_texture, item.spriteFrames.x, item.spriteFrames.y, item.spriteFrame) _show_floating_text(player, display_text, text_color, 0.5, 0.5, items_texture, item.spriteFrames.x, item.spriteFrames.y, item.spriteFrame)
@@ -650,7 +664,7 @@ func _process_pickup_on_server(player: Node):
await sfx_loot_collect.finished await sfx_loot_collect.finished
queue_free() queue_free()
var processing_pickup: bool = false # Mutex to prevent concurrent pickup processing var processing_pickup: bool = false # Mutex to prevent concurrent pickup processing
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _request_pickup(player_peer_id: int): func _request_pickup(player_peer_id: int):
@@ -666,7 +680,7 @@ func _request_pickup(player_peer_id: int):
var dropped_by_peer_id = get_meta("dropped_by_peer_id") var dropped_by_peer_id = get_meta("dropped_by_peer_id")
var drop_time = get_meta("drop_time") var drop_time = get_meta("drop_time")
var current_time = Time.get_ticks_msec() var current_time = Time.get_ticks_msec()
var time_since_drop = (current_time - drop_time) / 1000.0 # Convert to seconds var time_since_drop = (current_time - drop_time) / 1000.0 # Convert to seconds
if player_peer_id == dropped_by_peer_id and time_since_drop < 5.0: if player_peer_id == dropped_by_peer_id and time_since_drop < 5.0:
# Player can't pick up their own dropped item for 5 seconds # Player can't pick up their own dropped item for 5 seconds
@@ -719,7 +733,7 @@ func _sync_remove():
# Clients remove loot when any player picks it up # Clients remove loot when any player picks it up
# Only process if we're not the authority (i.e., we're a client) # Only process if we're not the authority (i.e., we're a client)
if multiplayer.is_server(): if multiplayer.is_server():
return # Server ignores its own updates return # Server ignores its own updates
print("Loot: Client received removal sync for loot at ", global_position) print("Loot: Client received removal sync for loot at ", global_position)
@@ -760,7 +774,7 @@ func _sync_remove():
func _sync_show_floating_text(loot_type_value: int, text: String, color_value: Color, _value: int, sprite_frame_value: int, player_peer_id: int): func _sync_show_floating_text(loot_type_value: int, text: String, color_value: Color, _value: int, sprite_frame_value: int, player_peer_id: int):
# Client receives floating text sync from server # Client receives floating text sync from server
if multiplayer.is_server(): if multiplayer.is_server():
return # Server ignores this (it's the sender) return # Server ignores this (it's the sender)
# Find player by peer ID # Find player by peer ID
var player = null var player = null
@@ -771,7 +785,7 @@ func _sync_show_floating_text(loot_type_value: int, text: String, color_value: C
break break
if not player or not is_instance_valid(player): if not player or not is_instance_valid(player):
return # Can't find player return # Can't find player
# Determine texture and parameters based on loot type # Determine texture and parameters based on loot type
var item_texture: Texture2D = null var item_texture: Texture2D = null

View File

@@ -0,0 +1,183 @@
extends Control
# Off-screen player indicators: show other players at the edge of the screen when outside camera.
# Uses the same colors as chat (player_colors by peer_id).
# Match chat_ui.gd player colors exactly
var _player_colors: Array = [
Color.RED,
Color.GREEN,
Color.BLUE,
Color.ORANGE,
Color(0.5, 0.0, 0.5) # Purple
]
const INDICATOR_RADIUS: float = 8.0
const EDGE_MARGIN: float = 24.0 # Inset from viewport edge so circles stay visible
const NAME_FONT_SIZE: int = 12
const NAME_OFFSET: float = 14.0 # Gap between circle and name
var _game_world: Node = null
var _camera: Camera2D = null
var _local_player_set: Dictionary = {} # player instance id -> true
var _indicators: Array = [] # { position: Vector2, color: Color, name: String, direction: Vector2 }
var _font: Font = null
func _ready() -> void:
_update_size()
if get_viewport():
get_viewport().size_changed.connect(_update_size)
_game_world = get_tree().get_first_node_in_group("game_world")
if _game_world and _game_world.has_node("Camera2D"):
_camera = _game_world.get_node("Camera2D")
_font = ThemeDB.fallback_font
mouse_filter = Control.MOUSE_FILTER_IGNORE
func _update_size() -> void:
var vp = get_viewport()
if not vp:
return
var rect = vp.get_visible_rect()
position = rect.position
size = rect.size
custom_minimum_size = rect.size
func _process(_delta: float) -> void:
_indicators.clear()
if not _game_world or not _camera or not multiplayer or not multiplayer.has_multiplayer_peer():
queue_redraw()
return
var vp = get_viewport()
if not vp:
return
var vp_rect = vp.get_visible_rect()
var vp_size = vp_rect.size
var center = vp_rect.get_center()
# Local players (don't show indicators for them)
_local_player_set.clear()
var local_players: Array = []
if _game_world.has_node("PlayerManager"):
var pm = _game_world.get_node("PlayerManager")
if pm.has_method("get_local_players"):
local_players = pm.get_local_players()
for p in local_players:
if is_instance_valid(p):
_local_player_set[p.get_instance_id()] = true
var zoom_x: float = _camera.zoom.x if _camera.zoom.x > 0.0 else 1.0
for player in get_tree().get_nodes_in_group("player"):
if not is_instance_valid(player):
continue
if _local_player_set.has(player.get_instance_id()):
continue
if "is_dead" in player and player.is_dead:
continue
if not player.has_method("get_multiplayer_authority"):
continue
var world_pos: Vector2 = player.global_position
var screen_pos: Vector2 = (world_pos - _camera.global_position) * zoom_x + center
# Check if on screen (with small margin)
var m = EDGE_MARGIN
if screen_pos.x >= m and screen_pos.x <= vp_size.x - m and screen_pos.y >= m and screen_pos.y <= vp_size.y - m:
continue
var edge_pos: Vector2 = _clamp_to_viewport_edge(center, screen_pos, vp_rect)
var peer_id: int = player.get_multiplayer_authority()
var col: Color = _player_colors[peer_id % _player_colors.size()]
var dir: Vector2 = (edge_pos - center).normalized()
var pname: String = player.name
if player.get("character_stats") and player.character_stats:
var cn: String = player.character_stats.character_name
if not cn.is_empty():
pname = cn
_indicators.append({ "position": edge_pos, "color": col, "name": pname, "direction": dir })
queue_redraw()
func _clamp_to_viewport_edge(center: Vector2, point: Vector2, vp_rect: Rect2) -> Vector2:
var sz = vp_rect.size
var mx = EDGE_MARGIN
var cx = center.x
var cy = center.y
var px = point.x
var py = point.y
var dx = px - cx
var dy = py - cy
var len_sq = dx * dx + dy * dy
if len_sq < 0.0001:
return center
var inv = 1.0 / sqrt(len_sq)
var ux = dx * inv
var uy = dy * inv
# Intersect ray center + t * (ux, uy) with inset rect [mx, my] to [w-mx, h-mx]
var w = sz.x
var h = sz.y
var t_min = INF
var hit = center
# Left edge x = mx
if abs(ux) > 0.0001:
var t = (mx - cx) / ux
if t > 0:
var y = cy + t * uy
if y >= mx and y <= h - mx:
if t < t_min:
t_min = t
hit = Vector2(mx, y)
# Right edge x = w - mx
if abs(ux) > 0.0001:
var t = (w - mx - cx) / ux
if t > 0:
var y = cy + t * uy
if y >= mx and y <= h - mx:
if t < t_min:
t_min = t
hit = Vector2(w - mx, y)
# Top edge y = mx
if abs(uy) > 0.0001:
var t = (mx - cy) / uy
if t > 0:
var x = cx + t * ux
if x >= mx and x <= w - mx:
if t < t_min:
t_min = t
hit = Vector2(x, mx)
# Bottom edge y = h - mx
if abs(uy) > 0.0001:
var t = (h - mx - cy) / uy
if t > 0:
var x = cx + t * ux
if x >= mx and x <= w - mx:
if t < t_min:
t_min = t
hit = Vector2(x, h - mx)
return hit
func _draw() -> void:
for d in _indicators:
var pos: Vector2 = d["position"]
var col: Color = d["color"]
var pname: String = d["name"]
var dir: Vector2 = d["direction"]
# Slight dark outline so visible on any background
draw_circle(pos, INDICATOR_RADIUS + 1.0, Color(0.15, 0.15, 0.15, 0.7))
draw_circle(pos, INDICATOR_RADIUS, col)
# Player name inward from circle, same color as indicator
if _font and not pname.is_empty():
var text_anchor: Vector2 = pos - dir * (INDICATOR_RADIUS + NAME_OFFSET)
var ts: Vector2 = _font.get_string_size(pname, HORIZONTAL_ALIGNMENT_LEFT, -1, NAME_FONT_SIZE)
var base_y: float = text_anchor.y
var text_pos: Vector2
if dir.x >= 0:
text_pos = Vector2(text_anchor.x - ts.x, base_y)
else:
text_pos = Vector2(text_anchor.x, base_y)
draw_string_outline(_font, text_pos, pname, HORIZONTAL_ALIGNMENT_LEFT, -1, NAME_FONT_SIZE, 1, Color(0.1, 0.1, 0.1, 0.9))
draw_string(_font, text_pos, pname, HORIZONTAL_ALIGNMENT_LEFT, -1, NAME_FONT_SIZE, col)

View File

@@ -0,0 +1 @@
uid://j468bxsdgknu

View File

@@ -41,6 +41,7 @@ var just_grabbed_this_frame = false # Prevents immediate release bug - persists
var grab_released_while_lifting = false # Track if grab was released while lifting (for drop-on-second-press logic) var grab_released_while_lifting = false # Track if grab was released while lifting (for drop-on-second-press logic)
var grab_start_time = 0.0 # Time when we first grabbed (to detect quick tap) var grab_start_time = 0.0 # Time when we first grabbed (to detect quick tap)
var grab_tap_threshold = 0.2 # Seconds to distinguish tap from hold var grab_tap_threshold = 0.2 # Seconds to distinguish tap from hold
var is_shielding: bool = false # True when holding grab with shield in offhand and nothing to grab/lift
var last_movement_direction = Vector2.DOWN # Track last direction for placing objects var last_movement_direction = Vector2.DOWN # Track last direction for placing objects
var push_axis = Vector2.ZERO # Locked axis for pushing/pulling var push_axis = Vector2.ZERO # Locked axis for pushing/pulling
var push_direction_locked: int = Direction.DOWN # Locked facing direction when pushing var push_direction_locked: int = Direction.DOWN # Locked facing direction when pushing
@@ -86,6 +87,7 @@ 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
var spell_incantation_played: bool = false # Track if incantation sound has been played var spell_incantation_played: bool = false # Track if incantation sound has been played
var current_spell_element: String = "fire" # "fire" or "healing" for cursor/tint
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
@@ -96,6 +98,10 @@ var direction_lock_timer: float = 0.0 # Lock facing direction when attacking
var locked_facing_direction: Vector2 = Vector2.ZERO # Locked facing direction during attack var locked_facing_direction: Vector2 = Vector2.ZERO # Locked facing direction during attack
var damage_direction_lock_timer: float = 0.0 # Lock facing when taking damage (enemies/players) var damage_direction_lock_timer: float = 0.0 # Lock facing when taking damage (enemies/players)
var damage_direction_lock_duration: float = 0.3 # Duration to block direction change after damage var damage_direction_lock_duration: float = 0.3 # Duration to block direction change after damage
var shield_block_cooldown_timer: float = 0.0 # After blocking, cannot block again until this reaches 0
var shield_block_cooldown_duration: float = 1.2 # Seconds before can block again
var shield_block_direction: Vector2 = Vector2.DOWN # Facing direction when we started blocking (locked)
var was_shielding_last_frame: bool = false # For detecting shield activate transition
var empty_bow_shot_attempts: int = 0 var empty_bow_shot_attempts: int = 0
var _arrow_spawn_seq: int = 0 # Per-player sequence for stable arrow names (multiplayer sync) var _arrow_spawn_seq: int = 0 # Per-player sequence for stable arrow names (multiplayer sync)
var sword_slash_scene = preload("res://scenes/sword_slash.tscn") # Old rotating version (kept for reference) var sword_slash_scene = preload("res://scenes/sword_slash.tscn") # Old rotating version (kept for reference)
@@ -103,6 +109,8 @@ var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn") # New
var staff_projectile_scene = preload("res://scenes/attack_staff.tscn") # Staff magic ball projectile var staff_projectile_scene = preload("res://scenes/attack_staff.tscn") # Staff magic ball projectile
var attack_arrow_scene = preload("res://scenes/attack_arrow.tscn") var attack_arrow_scene = preload("res://scenes/attack_arrow.tscn")
var flame_spell_scene = preload("res://scenes/attack_spell_flame.tscn") # Flame spell for Tome of Flames var flame_spell_scene = preload("res://scenes/attack_spell_flame.tscn") # Flame spell for Tome of Flames
var frostspike_spell_scene = preload("res://scenes/attack_spell_frostspike.tscn") # Frostspike for Tome of Frostspike
var healing_effect_scene = preload("res://scenes/healing_effect.tscn") # Visual effect for Tome of Healing
var attack_bomb_scene = preload("res://scenes/attack_bomb.tscn") # Bomb projectile var attack_bomb_scene = preload("res://scenes/attack_bomb.tscn") # Bomb projectile
var blood_scene = preload("res://scenes/blood_clot.tscn") var blood_scene = preload("res://scenes/blood_clot.tscn")
@@ -135,6 +143,8 @@ var is_airborne: bool = false
@onready var sprite_eyelashes = $Sprite2DEyeLashes @onready var sprite_eyelashes = $Sprite2DEyeLashes
@onready var sprite_addons = $Sprite2DAddons @onready var sprite_addons = $Sprite2DAddons
@onready var sprite_headgear = $Sprite2DHeadgear @onready var sprite_headgear = $Sprite2DHeadgear
@onready var sprite_shield = $Sprite2DShield
@onready var sprite_shield_holding = $Sprite2DShieldHolding
@onready var sprite_weapon = $Sprite2DWeapon @onready var sprite_weapon = $Sprite2DWeapon
@onready var cone_light = $ConeLight @onready var cone_light = $ConeLight
@@ -478,6 +488,10 @@ func _duplicate_sprite_materials():
sprite_headgear.material = sprite_headgear.material.duplicate() sprite_headgear.material = sprite_headgear.material.duplicate()
if sprite_weapon and sprite_weapon.material: if sprite_weapon and sprite_weapon.material:
sprite_weapon.material = sprite_weapon.material.duplicate() sprite_weapon.material = sprite_weapon.material.duplicate()
if sprite_shield and sprite_shield.material:
sprite_shield.material = sprite_shield.material.duplicate()
if sprite_shield_holding and sprite_shield_holding.material:
sprite_shield_holding.material = sprite_shield_holding.material.duplicate()
func _initialize_character_stats(): func _initialize_character_stats():
# Create character_stats if it doesn't exist # Create character_stats if it doesn't exist
@@ -693,12 +707,15 @@ func _setup_player_appearance():
character_stats.equipment["offhand"] = starting_bomb character_stats.equipment["offhand"] = starting_bomb
print("Dwarf player ", name, " spawned with 5 bombs") print("Dwarf player ", name, " spawned with 5 bombs")
# Give Human race starting spellbook (Tome of Flames) # Give Human race (Wizard) starting spellbook (Tome of Flames) and Hat
if selected_race == "Human": if selected_race == "Human":
var starting_tome = ItemDatabase.create_item("tome_of_flames") var starting_tome = ItemDatabase.create_item("tome_of_flames")
if starting_tome: if starting_tome:
character_stats.equipment["offhand"] = starting_tome character_stats.equipment["offhand"] = starting_tome
print("Human player ", name, " spawned with Tome of Flames") var starting_hat = ItemDatabase.create_item("hat")
if starting_hat:
character_stats.equipment["headgear"] = starting_hat
print("Human player ", name, " spawned with Tome of Flames and Hat")
# Randomize skin (human only for players) # Randomize skin (human only for players)
# Weighted random: Human1 has highest chance, Human7 has lowest chance # Weighted random: Human1 has highest chance, Human7 has lowest chance
@@ -1129,6 +1146,8 @@ func _apply_appearance_to_sprites():
_apply_weapon_color_replacements(sprite_weapon, equipped_weapon) _apply_weapon_color_replacements(sprite_weapon, equipped_weapon)
else: else:
_clear_weapon_color_replacements(sprite_weapon) _clear_weapon_color_replacements(sprite_weapon)
_update_shield_visibility()
# Appearance applied (verbose logging removed) # Appearance applied (verbose logging removed)
@@ -1458,6 +1477,10 @@ func _update_animation(delta):
sprite_addons.frame = frame_index sprite_addons.frame = frame_index
if sprite_headgear: if sprite_headgear:
sprite_headgear.frame = frame_index sprite_headgear.frame = frame_index
if sprite_shield:
sprite_shield.frame = frame_index
if sprite_shield_holding:
sprite_shield_holding.frame = frame_index
# Update weapon sprite - use BOW_STRING animation if charging bow # Update weapon sprite - use BOW_STRING animation if charging bow
if sprite_weapon: if sprite_weapon:
@@ -1511,6 +1534,10 @@ func _update_facing_from_mouse(mouse_direction: Vector2):
if is_pushing: if is_pushing:
return return
# Don't update if shielding (locked block direction)
if is_shielding:
return
# Don't update if direction is locked (during attack) # Don't update if direction is locked (during attack)
if direction_lock_timer > 0.0: if direction_lock_timer > 0.0:
return return
@@ -1699,7 +1726,7 @@ func _update_z_physics(delta):
# Apply to all sprite layers # Apply to all sprite layers
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair, for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons, sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
sprite_headgear, sprite_weapon]: sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon]:
if sprite_layer: if sprite_layer:
sprite_layer.position.y = y_offset sprite_layer.position.y = y_offset
if position_z > 0: if position_z > 0:
@@ -1729,6 +1756,28 @@ func _physics_process(delta):
if is_airborne: if is_airborne:
_update_z_physics(delta) _update_z_physics(delta)
# Spell charge visuals (incantation, tint pulse) - run for ALL players so others see the pulse
if is_charging_spell:
var charge_time = (Time.get_ticks_msec() / 1000.0) - spell_charge_start_time
var charge_progress = clamp(charge_time / spell_charge_duration, 0.0, 1.0)
spell_charge_particle_timer += delta
_update_spell_charge_particles(charge_progress)
_update_spell_charge_incantation(charge_progress)
if charge_progress >= 1.0:
spell_charge_tint_pulse_time += delta * spell_charge_tint_pulse_speed_charged
_apply_spell_charge_tint()
if not spell_incantation_played and has_node("SfxSpellIncantation"):
$SfxSpellIncantation.play()
spell_incantation_played = true
else:
spell_charge_tint_pulse_time = 0.0
_clear_spell_charge_tint()
if has_node("SfxSpellIncantation"):
$SfxSpellIncantation.stop()
spell_incantation_played = false
else:
spell_charge_tint_pulse_time = 0.0
if is_local_player and is_multiplayer_authority(): if is_local_player and is_multiplayer_authority():
# Skip all input and logic if dead # Skip all input and logic if dead
if is_dead: if is_dead:
@@ -1759,37 +1808,10 @@ func _physics_process(delta):
if damage_direction_lock_timer <= 0.0: if damage_direction_lock_timer <= 0.0:
damage_direction_lock_timer = 0.0 damage_direction_lock_timer = 0.0
# Update spell charging if shield_block_cooldown_timer > 0.0:
if is_charging_spell: shield_block_cooldown_timer -= delta
var charge_time = (Time.get_ticks_msec() / 1000.0) - spell_charge_start_time if shield_block_cooldown_timer <= 0.0:
var charge_progress = clamp(charge_time / spell_charge_duration, 0.0, 1.0) shield_block_cooldown_timer = 0.0
# Update particles (spawn and animate)
spell_charge_particle_timer += delta
_update_spell_charge_particles(charge_progress)
_update_spell_charge_incantation(charge_progress)
# Update tint pulse timer when fully charged
if charge_progress >= 1.0:
# Use much faster pulse speed when fully charged
spell_charge_tint_pulse_time += delta * spell_charge_tint_pulse_speed_charged
_apply_spell_charge_tint()
# Play incantation sound when fully charged (only once)
if not spell_incantation_played:
if has_node("SfxSpellIncantation"):
$SfxSpellIncantation.play()
spell_incantation_played = true
else:
spell_charge_tint_pulse_time = 0.0
_clear_spell_charge_tint()
# Stop incantation if not fully charged
if has_node("SfxSpellIncantation"):
$SfxSpellIncantation.stop()
spell_incantation_played = false
else:
# Reset pulse timer when not charging
spell_charge_tint_pulse_time = 0.0
# Update bow charge tint (when fully charged) # Update bow charge tint (when fully charged)
if is_charging_bow: if is_charging_bow:
@@ -1881,9 +1903,15 @@ func _physics_process(delta):
break break
if being_held_by_someone: if being_held_by_someone:
is_shielding = false
was_shielding_last_frame = false
_update_shield_visibility()
# Handle struggle mechanic # Handle struggle mechanic
_handle_struggle(delta) _handle_struggle(delta)
elif is_knocked_back: elif is_knocked_back:
is_shielding = false
was_shielding_last_frame = false
_update_shield_visibility()
# During knockback, no input control - just let velocity carry the player # During knockback, no input control - just let velocity carry the player
# Apply friction to slow down knockback # Apply friction to slow down knockback
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0) velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
@@ -1895,6 +1923,9 @@ func _physics_process(delta):
_handle_movement(delta) _handle_movement(delta)
_handle_interactions() _handle_interactions()
else: else:
is_shielding = false
was_shielding_last_frame = false
_update_shield_visibility()
# Reset struggle when airborne # Reset struggle when airborne
struggle_time = 0.0 struggle_time = 0.0
struggle_direction = Vector2.ZERO struggle_direction = Vector2.ZERO
@@ -2100,8 +2131,10 @@ func _handle_input():
# Update full 360-degree facing direction for attacks (gamepad/keyboard input) # Update full 360-degree facing direction for attacks (gamepad/keyboard input)
# Only update if mouse control is not active (i.e., mouse is outside window or using gamepad) # Only update if mouse control is not active (i.e., mouse is outside window or using gamepad)
# Don't update if direction is locked (during attack) # Don't update if direction is locked (during attack) or shielding
if not is_pushing and (not mouse_control_active or input_device != -1) and direction_lock_timer <= 0.0: if is_shielding:
facing_direction_vector = shield_block_direction
elif not is_pushing and (not mouse_control_active or input_device != -1) and direction_lock_timer <= 0.0:
facing_direction_vector = input_vector.normalized() facing_direction_vector = input_vector.normalized()
elif direction_lock_timer > 0.0 and locked_facing_direction.length() > 0.0: elif direction_lock_timer > 0.0 and locked_facing_direction.length() > 0.0:
# Use locked direction during attack # Use locked direction during attack
@@ -2109,8 +2142,13 @@ func _handle_input():
# Update facing direction for animations (except when pushing - locked direction) # Update facing direction for animations (except when pushing - locked direction)
# Only update from movement input if mouse control is not active or using gamepad # Only update from movement input if mouse control is not active or using gamepad
# Don't update if direction is locked (during attack) # Don't update if direction is locked (during attack) or shielding
if not is_pushing and (not mouse_control_active or input_device != -1) and direction_lock_timer <= 0.0: if is_shielding:
var new_direction = _get_direction_from_vector(shield_block_direction) as Direction
if new_direction != current_direction:
current_direction = new_direction
_update_cone_light_rotation()
elif not is_pushing and (not mouse_control_active or input_device != -1) and direction_lock_timer <= 0.0:
var new_direction = _get_direction_from_vector(input_vector) as Direction var new_direction = _get_direction_from_vector(input_vector) as Direction
# Update direction and cone light rotation if changed # Update direction and cone light rotation if changed
@@ -2164,6 +2202,12 @@ func _handle_input():
if push_direction_locked != current_direction: if push_direction_locked != current_direction:
current_direction = push_direction_locked as Direction current_direction = push_direction_locked as Direction
_update_cone_light_rotation() _update_cone_light_rotation()
elif is_shielding:
# Keep locked block direction when shielding and idle
var new_direction = _get_direction_from_vector(shield_block_direction) as Direction
if new_direction != current_direction:
current_direction = new_direction
_update_cone_light_rotation()
else: else:
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF" and current_animation != "CONJURE" and current_animation != "LIFT" and current_animation != "FINISH_SPELL": if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF" and current_animation != "CONJURE" and current_animation != "LIFT" and current_animation != "FINISH_SPELL":
_set_animation("IDLE") _set_animation("IDLE")
@@ -2192,6 +2236,7 @@ func _handle_input():
# Reduce speed by half when pushing/pulling # Reduce speed by half when pushing/pulling
# Reduce speed by 50% when charging bow # Reduce speed by 50% when charging bow
# Reduce speed by 80% when charging spell (20% speed) # Reduce speed by 80% when charging spell (20% speed)
# Reduce speed to 60% when shielding
# Calculate speed with encumbrance penalty # Calculate speed with encumbrance penalty
var speed_multiplier = 1.0 var speed_multiplier = 1.0
if is_pushing: if is_pushing:
@@ -2200,6 +2245,8 @@ func _handle_input():
speed_multiplier = 0.5 speed_multiplier = 0.5
elif is_charging_spell: elif is_charging_spell:
speed_multiplier = 0.2 # 20% speed (80% reduction) speed_multiplier = 0.2 # 20% speed (80% reduction)
elif is_shielding:
speed_multiplier = 0.6 # 60% speed when blocking with shield
var base_speed = move_speed * speed_multiplier var base_speed = move_speed * speed_multiplier
var current_speed = base_speed var current_speed = base_speed
@@ -2262,6 +2309,25 @@ func _handle_interactions():
else: else:
grab_just_released = false grab_just_released = false
# Update is_shielding: hold grab with shield in offhand and nothing to grab/lift
var would_shield = (not is_dead and grab_button_down and _has_shield_in_offhand() \
and not held_object and not is_lifting and not is_pushing \
and not _has_nearby_grabbable() and not is_disarming)
if would_shield and shield_block_cooldown_timer > 0.0:
is_shielding = false
if has_node("SfxDenyActivateShield"):
$SfxDenyActivateShield.play()
elif would_shield:
if not was_shielding_last_frame:
shield_block_direction = facing_direction_vector.normalized() if facing_direction_vector.length() > 0.1 else last_movement_direction.normalized()
if has_node("SfxActivateShield"):
$SfxActivateShield.play()
is_shielding = true
else:
is_shielding = false
was_shielding_last_frame = is_shielding
_update_shield_visibility()
# Cancel bow charging if grab is pressed # Cancel bow charging if grab is pressed
if grab_just_pressed and is_charging_bow: if grab_just_pressed and is_charging_bow:
is_charging_bow = false is_charging_bow = false
@@ -2275,17 +2341,23 @@ func _handle_interactions():
print(name, " cancelled bow charge") print(name, " cancelled bow charge")
# Check for spell casting (with Tome of Flames) # Check for spell casting (Tome of Flames, Frostspike, or Healing)
# Handle spell charging (Tome of Flames)
if character_stats and character_stats.equipment.has("offhand"): if character_stats and character_stats.equipment.has("offhand"):
var offhand_item = character_stats.equipment["offhand"] var offhand_item = character_stats.equipment["offhand"]
if offhand_item and offhand_item.weapon_type == Item.WeaponType.SPELLBOOK: if offhand_item and offhand_item.weapon_type == Item.WeaponType.SPELLBOOK:
if offhand_item.item_name == "Tome of Flames": var is_fire = offhand_item.item_name == "Tome of Flames"
# Check for valid target position var is_frost = offhand_item.item_name == "Tome of Frostspike"
var is_heal = offhand_item.item_name == "Tome of Healing"
if is_fire or is_frost or is_heal:
var game_world = get_tree().get_first_node_in_group("game_world") var game_world = get_tree().get_first_node_in_group("game_world")
var target_pos = Vector2.ZERO var target_pos = Vector2.ZERO
if game_world and game_world.has_method("get_grid_locked_cursor_position"): var heal_target: Node = null
if (is_fire or is_frost) and game_world and game_world.has_method("get_grid_locked_cursor_position"):
target_pos = game_world.get_grid_locked_cursor_position() target_pos = game_world.get_grid_locked_cursor_position()
elif is_heal:
heal_target = _get_heal_target()
var has_valid_target = ((is_fire or is_frost) and target_pos != Vector2.ZERO) or (is_heal and heal_target != null)
# Check if there's a grabbable object nearby - prioritize grabbing over spell casting # Check if there's a grabbable object nearby - prioritize grabbing over spell casting
var nearby_grabbable = null var nearby_grabbable = null
@@ -2307,133 +2379,95 @@ func _handle_interactions():
nearby_grabbable = body nearby_grabbable = body
break break
# Only start charging spell if no grabbable object is nearby and not lifting/grabbing if grab_just_pressed and not is_charging_spell and has_valid_target and not nearby_grabbable and not is_lifting and not held_object:
if grab_just_pressed and not is_charging_spell and target_pos != Vector2.ZERO and not nearby_grabbable and not is_lifting and not held_object:
is_charging_spell = true is_charging_spell = true
current_spell_element = "healing" if is_heal else ("frost" if is_frost else "fire")
spell_charge_start_time = Time.get_ticks_msec() / 1000.0 spell_charge_start_time = Time.get_ticks_msec() / 1000.0
spell_incantation_played = false # Reset flag when starting new charge spell_incantation_played = false
_start_spell_charge_particles() _start_spell_charge_particles()
_start_spell_charge_incantation() _start_spell_charge_incantation()
# Play spell charging sound (incantation plays when fully charged)
if has_node("SfxSpellCharge"): if has_node("SfxSpellCharge"):
$SfxSpellCharge.play() $SfxSpellCharge.play()
# Sync spell charge start to other clients
if multiplayer.has_multiplayer_peer(): if multiplayer.has_multiplayer_peer():
_sync_spell_charge_start.rpc() _sync_spell_charge_start.rpc()
print(name, " started charging spell (", current_spell_element, ")")
print(name, " started charging spell")
# Skip regular grab handling
just_grabbed_this_frame = false just_grabbed_this_frame = false
return return
# Release spell
elif grab_just_released and is_charging_spell: elif grab_just_released and is_charging_spell:
var charge_time = (Time.get_ticks_msec() / 1000.0) - spell_charge_start_time var charge_time = (Time.get_ticks_msec() / 1000.0) - spell_charge_start_time
# Minimum charge time: 0.2 seconds, otherwise cancel
if charge_time < 0.2: if charge_time < 0.2:
is_charging_spell = false is_charging_spell = false
current_spell_element = "fire"
spell_incantation_played = false spell_incantation_played = false
_stop_spell_charge_particles() _stop_spell_charge_particles()
_stop_spell_charge_incantation() _stop_spell_charge_incantation()
_clear_spell_charge_tint() _clear_spell_charge_tint()
# Return to IDLE animation
_set_animation("IDLE") _set_animation("IDLE")
# Stop spell charging sounds
if has_node("SfxSpellCharge"): if has_node("SfxSpellCharge"):
$SfxSpellCharge.stop() $SfxSpellCharge.stop()
if has_node("SfxSpellIncantation"): if has_node("SfxSpellIncantation"):
$SfxSpellIncantation.stop() $SfxSpellIncantation.stop()
# Sync spell charge end to other clients
if multiplayer.has_multiplayer_peer(): if multiplayer.has_multiplayer_peer():
_sync_spell_charge_end.rpc() _sync_spell_charge_end.rpc()
print(name, " cancelled spell (released too quickly)")
print(name, " cancelled spell (released too quickly, need at least 0.2s)")
just_grabbed_this_frame = false just_grabbed_this_frame = false
return return
# Check if fully charged (1.0 seconds)
var is_fully_charged = charge_time >= spell_charge_duration var is_fully_charged = charge_time >= spell_charge_duration
if (is_fire or is_frost) and game_world and game_world.has_method("get_grid_locked_cursor_position"):
# Get target position again (in case it changed)
if game_world and game_world.has_method("get_grid_locked_cursor_position"):
target_pos = game_world.get_grid_locked_cursor_position() target_pos = game_world.get_grid_locked_cursor_position()
if is_heal:
heal_target = _get_heal_target()
has_valid_target = ((is_fire or is_frost) and target_pos != Vector2.ZERO) or (is_heal and heal_target != null)
# Cast spell if fully charged (will find valid position if target is blocked) if has_valid_target and is_fully_charged:
if target_pos != Vector2.ZERO and is_fully_charged: if is_fire:
# Cast spell (will find closest valid position if target is blocked) _cast_flame_spell(target_pos)
_cast_flame_spell(target_pos) elif is_frost:
# Play FINISH_SPELL animation after casting _cast_frostspike_spell(target_pos)
else:
_cast_heal_spell(heal_target)
_set_animation("FINISH_SPELL") _set_animation("FINISH_SPELL")
# Stop charging and clear tint (but let incantation sound finish)
is_charging_spell = false is_charging_spell = false
current_spell_element = "fire"
spell_incantation_played = false spell_incantation_played = false
_stop_spell_charge_particles() _stop_spell_charge_particles()
_stop_spell_charge_incantation() _stop_spell_charge_incantation()
_clear_spell_charge_tint() # This will restore original tints _clear_spell_charge_tint()
# Stop spell charging sound, but let incantation play to completion
if has_node("SfxSpellCharge"): if has_node("SfxSpellCharge"):
$SfxSpellCharge.stop() $SfxSpellCharge.stop()
# Don't stop SfxSpellIncantation - let it finish playing
# Sync spell charge end to other clients
if multiplayer.has_multiplayer_peer():
_sync_spell_charge_end.rpc()
else: else:
# Not fully charged or no target - just cancel without casting print(name, " spell not cast (charge: ", charge_time, "s, fully: ", is_fully_charged, ", target ok: ", has_valid_target, ")")
print(name, " spell not cast (charge: ", charge_time, "s, fully charged: ", is_fully_charged, ", target: ", target_pos, ")")
# Stop charging and clear tint
is_charging_spell = false is_charging_spell = false
current_spell_element = "fire"
spell_incantation_played = false spell_incantation_played = false
_stop_spell_charge_particles() _stop_spell_charge_particles()
_stop_spell_charge_incantation() _stop_spell_charge_incantation()
_clear_spell_charge_tint() # This will restore original tints _clear_spell_charge_tint()
# Return to IDLE animation
_set_animation("IDLE") _set_animation("IDLE")
# Stop spell charging sounds
if has_node("SfxSpellCharge"): if has_node("SfxSpellCharge"):
$SfxSpellCharge.stop() $SfxSpellCharge.stop()
if has_node("SfxSpellIncantation"): if has_node("SfxSpellIncantation"):
$SfxSpellIncantation.stop() $SfxSpellIncantation.stop()
# Sync spell charge end to other clients
if multiplayer.has_multiplayer_peer(): if multiplayer.has_multiplayer_peer():
_sync_spell_charge_end.rpc() _sync_spell_charge_end.rpc()
print(name, " released spell (", "healing" if is_heal else ("frost" if is_frost else "fire"), ", fully: ", is_fully_charged, ")")
print(name, " released spell (charge: ", charge_time, "s, fully charged: ", is_fully_charged, ")")
# Skip regular grab handling
just_grabbed_this_frame = false just_grabbed_this_frame = false
return return
# Cancel if no target position available or if player starts lifting/grabbing elif is_charging_spell and (not has_valid_target or is_lifting or held_object):
elif is_charging_spell and (target_pos == Vector2.ZERO or is_lifting or held_object):
is_charging_spell = false is_charging_spell = false
current_spell_element = "fire"
spell_incantation_played = false spell_incantation_played = false
_stop_spell_charge_particles() _stop_spell_charge_particles()
_stop_spell_charge_incantation() _stop_spell_charge_incantation()
_clear_spell_charge_tint() _clear_spell_charge_tint()
# Return to IDLE animation
_set_animation("IDLE") _set_animation("IDLE")
# Stop spell charging sounds
if has_node("SfxSpellCharge"): if has_node("SfxSpellCharge"):
$SfxSpellCharge.stop() $SfxSpellCharge.stop()
if has_node("SfxSpellIncantation"): if has_node("SfxSpellIncantation"):
$SfxSpellIncantation.stop() $SfxSpellIncantation.stop()
# Sync spell charge end to other clients
if multiplayer.has_multiplayer_peer(): if multiplayer.has_multiplayer_peer():
_sync_spell_charge_end.rpc() _sync_spell_charge_end.rpc()
print(name, " spell charge cancelled (no target)") print(name, " spell charge cancelled (no target)")
# Check for trap disarm (Dwarf only) # Check for trap disarm (Dwarf only)
@@ -2784,6 +2818,44 @@ func _get_nearby_disarmable_trap() -> Node:
return null return null
func _has_shield_in_offhand() -> bool:
if not character_stats or not character_stats.equipment.has("offhand"):
return false
var off = character_stats.equipment["offhand"]
return off != null and "shield" in off.item_name.to_lower()
func _has_nearby_grabbable() -> bool:
if not grab_area:
return false
var bodies = grab_area.get_overlapping_bodies()
for body in bodies:
if body == self:
continue
var is_grabbable = false
if body.has_method("can_be_grabbed"):
if body.can_be_grabbed():
is_grabbable = true
elif body.is_in_group("player") or ("peer_id" in body and body is CharacterBody2D):
is_grabbable = true
if is_grabbable and position.distance_to(body.position) < grab_range:
return true
return false
func _update_shield_visibility() -> void:
if not sprite_shield or not sprite_shield_holding:
return
var has_shield = _has_shield_in_offhand()
if not has_shield:
sprite_shield.visible = false
sprite_shield_holding.visible = false
return
if is_shielding:
sprite_shield.visible = false
sprite_shield_holding.visible = true
else:
sprite_shield.visible = true
sprite_shield_holding.visible = false
func _try_grab(): func _try_grab():
if not grab_area: if not grab_area:
return return
@@ -3026,6 +3098,9 @@ func reset_grab_state():
grab_start_time = 0.0 grab_start_time = 0.0
grab_released_while_lifting = false grab_released_while_lifting = false
was_dragging_last_frame = false was_dragging_last_frame = false
is_shielding = false
was_shielding_last_frame = false
_update_shield_visibility()
# Reset to idle animation # Reset to idle animation
if current_animation == "IDLE_HOLD" or current_animation == "RUN_HOLD" or current_animation == "LIFT" or current_animation == "IDLE_PUSH" or current_animation == "RUN_PUSH": if current_animation == "IDLE_HOLD" or current_animation == "RUN_HOLD" or current_animation == "LIFT" or current_animation == "IDLE_PUSH" or current_animation == "RUN_PUSH":
@@ -3411,6 +3486,7 @@ func _place_down_object():
if not attack_bomb_scene: if not attack_bomb_scene:
return return
var bomb = attack_bomb_scene.instantiate() var bomb = attack_bomb_scene.instantiate()
bomb.name = "PlacedBomb_" + bomb_name
get_parent().add_child(bomb) get_parent().add_child(bomb)
bomb.global_position = place_pos bomb.global_position = place_pos
bomb.setup(place_pos, self, Vector2.ZERO, false) # not thrown, fuse starts in _deferred_ready bomb.setup(place_pos, self, Vector2.ZERO, false) # not thrown, fuse starts in _deferred_ready
@@ -3813,8 +3889,10 @@ func _place_bomb(target_position: Vector2):
print(name, " cannot place bomb - no valid target position") print(name, " cannot place bomb - no valid target position")
return return
# Spawn bomb at target position # Unique id for sync (collect/remove on other clients)
var bomb_id = "DirectBomb_%d_%d" % [Time.get_ticks_msec(), get_multiplayer_authority()]
var bomb = attack_bomb_scene.instantiate() var bomb = attack_bomb_scene.instantiate()
bomb.name = bomb_id
get_parent().add_child(bomb) get_parent().add_child(bomb)
bomb.global_position = valid_target_pos bomb.global_position = valid_target_pos
@@ -3827,7 +3905,7 @@ func _place_bomb(target_position: Vector2):
# Sync bomb spawn to other clients # Sync bomb spawn to other clients
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
_rpc_to_ready_peers("_sync_place_bomb", [valid_target_pos]) _rpc_to_ready_peers("_sync_place_bomb", [bomb_id, valid_target_pos])
print(name, " placed bomb!") print(name, " placed bomb!")
@@ -3889,6 +3967,128 @@ func _sync_flame_spell(target_position: Vector2, spell_damage: float):
print(name, " (synced) spawned flame spell at ", target_position) print(name, " (synced) spawned flame spell at ", target_position)
func _cast_frostspike_spell(target_position: Vector2):
if not frostspike_spell_scene:
return
if not is_multiplayer_authority():
return
var game_world = get_tree().get_first_node_in_group("game_world")
var valid_target_pos = target_position
if game_world and game_world.has_method("_get_valid_spell_target_position"):
var found_pos = game_world._get_valid_spell_target_position(target_position)
if found_pos != Vector2.ZERO:
valid_target_pos = found_pos
else:
print(name, " cannot cast frostspike - no valid target position")
return
var spell_damage = 15.0
if character_stats:
spell_damage = character_stats.damage * 0.75
var frost = frostspike_spell_scene.instantiate()
frost.setup(valid_target_pos, self, spell_damage, true)
get_parent().add_child(frost)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_rpc_to_ready_peers("_sync_frostspike_spell", [valid_target_pos, spell_damage])
print(name, " cast frostspike at ", valid_target_pos)
@rpc("any_peer", "reliable")
func _sync_frostspike_spell(target_position: Vector2, spell_damage: float):
if is_multiplayer_authority():
return
if not frostspike_spell_scene:
return
var frost = frostspike_spell_scene.instantiate()
frost.setup(target_position, self, spell_damage, true)
get_parent().add_child(frost)
print(name, " (synced) spawned frostspike at ", target_position)
func _cast_heal_spell(target: Node):
if not target or not is_instance_valid(target):
return
if not character_stats:
return
var int_val = character_stats.baseStats.int + character_stats.get_pass("int")
var base_heal = 10.0
var amount = base_heal + int_val * 0.5
amount = max(1.0, floor(amount))
var cap = 0.0
if target.character_stats:
cap = target.character_stats.maxhp - target.character_stats.hp
amount = min(amount, max(0.0, cap))
if amount <= 0:
return
var me = multiplayer.get_unique_id()
var tid = target.get_multiplayer_authority()
if me == tid:
target.heal(amount)
_spawn_heal_effect_and_text(target, amount)
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
var gw = get_tree().get_first_node_in_group("game_world")
if gw and gw.has_method("_sync_heal_spell"):
_rpc_to_ready_peers("_sync_heal_spell_via_gw", [target.name, amount])
print(name, " cast heal on ", target.name, " for ", int(amount), " HP")
func _spawn_heal_effect_and_text(target: Node, amount: float):
if not target or not is_instance_valid(target):
return
var game_world = get_tree().get_first_node_in_group("game_world")
var entities = game_world.get_node_or_null("Entities") if game_world else null
var parent = entities if entities else target.get_parent()
if not parent:
return
if healing_effect_scene:
var eff = healing_effect_scene.instantiate()
parent.add_child(eff)
eff.global_position = target.global_position
if eff.has_method("setup"):
eff.setup(target)
var floating_text_scene = preload("res://scenes/floating_text.tscn")
if floating_text_scene:
var ft = floating_text_scene.instantiate()
parent.add_child(ft)
ft.global_position = Vector2(target.global_position.x, target.global_position.y - 20)
ft.setup("+" + str(int(amount)) + " HP", Color.GREEN, 0.5, 0.5, null, 1, 1, 0)
@rpc("any_peer", "reliable")
func _sync_heal_spell_via_gw(target_name: String, amount: float):
if is_multiplayer_authority():
return
var gw = get_tree().get_first_node_in_group("game_world")
if gw and gw.has_method("_apply_heal_spell_sync"):
gw._apply_heal_spell_sync(target_name, amount)
func _is_healing_spell() -> bool:
if not character_stats or not character_stats.equipment.has("offhand"):
return false
var off = character_stats.equipment["offhand"]
return off != null and off.item_name == "Tome of Healing"
func _is_frost_spell() -> bool:
if not character_stats or not character_stats.equipment.has("offhand"):
return false
var off = character_stats.equipment["offhand"]
return off != null and off.item_name == "Tome of Frostspike"
func _get_heal_target() -> Node:
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world or not game_world.has_node("Camera2D"):
return null
var cam = game_world.get_node("Camera2D")
var mouse_world = cam.get_global_mouse_position()
const HEAL_RANGE: float = 56.0
var best: Node = null
var best_d: float = HEAL_RANGE
for p in get_tree().get_nodes_in_group("player"):
if not is_instance_valid(p):
continue
if "is_dead" in p and p.is_dead:
continue
var d = p.global_position.distance_to(mouse_world)
if d < best_d:
best_d = d
best = p
return best
func _can_cast_spell_at(target_position: Vector2) -> bool: func _can_cast_spell_at(target_position: Vector2) -> bool:
# Check if spell can be cast at target position # Check if spell can be cast at target position
# Must be on floor tile and not blocked by walls # Must be on floor tile and not blocked by walls
@@ -4024,17 +4224,25 @@ func _stop_spell_charge_particles():
spell_charge_particles = null spell_charge_particles = null
func _start_spell_charge_incantation(): func _start_spell_charge_incantation():
# Play fire_charging on AnimationIncantation when starting spell charge
spell_incantation_fire_ready_shown = false spell_incantation_fire_ready_shown = false
if has_node("AnimationIncantation"): if has_node("AnimationIncantation"):
$AnimationIncantation.play("fire_charging") if _is_healing_spell():
$AnimationIncantation.play("healing_charging")
elif _is_frost_spell():
$AnimationIncantation.play("frost_charging")
else:
$AnimationIncantation.play("fire_charging")
func _update_spell_charge_incantation(charge_progress: float): func _update_spell_charge_incantation(charge_progress: float):
# Switch to fire_ready when fully charged (fire_charging already playing from start)
if not has_node("AnimationIncantation"): if not has_node("AnimationIncantation"):
return return
if charge_progress >= 1.0 and not spell_incantation_fire_ready_shown: if charge_progress >= 1.0 and not spell_incantation_fire_ready_shown:
$AnimationIncantation.play("fire_ready") if _is_healing_spell():
$AnimationIncantation.play("healing_ready")
elif _is_frost_spell():
$AnimationIncantation.play("frost_ready")
else:
$AnimationIncantation.play("fire_ready")
spell_incantation_fire_ready_shown = true spell_incantation_fire_ready_shown = true
func _stop_spell_charge_incantation(): func _stop_spell_charge_incantation():
@@ -4044,12 +4252,13 @@ func _stop_spell_charge_incantation():
$AnimationIncantation.play("idle") $AnimationIncantation.play("idle")
func _apply_spell_charge_tint(): func _apply_spell_charge_tint():
# Apply pulsing tint to all sprite layers when fully charged using shader parameters
# Pulse between original tint and spell charge tint
# IMPORTANT: Only apply to THIS player's sprites (not other players)
if not is_charging_spell: if not is_charging_spell:
return return
var tint = spell_charge_tint
if _is_healing_spell():
tint = Color(0.25, 2.0, 0.3, 2.0) # Green pulse for healing
elif _is_frost_spell():
tint = Color(0.25, 0.5, 2.0, 2.0) # Blue pulse for frost
var sprites = [ var sprites = [
{"sprite": sprite_body, "name": "body"}, {"sprite": sprite_body, "name": "body"},
{"sprite": sprite_boots, "name": "boots"}, {"sprite": sprite_boots, "name": "boots"},
@@ -4098,12 +4307,12 @@ func _apply_spell_charge_tint():
# Get original tint # Get original tint
var original_tint = original_sprite_tints.get(tint_key, Color.WHITE) var original_tint = original_sprite_tints.get(tint_key, Color.WHITE)
# Calculate fully charged tint (original * spell_charge_tint) # Calculate fully charged tint (original * tint)
var full_charged_tint = Color( var full_charged_tint = Color(
original_tint.r * spell_charge_tint.r, original_tint.r * tint.r,
original_tint.g * spell_charge_tint.g, original_tint.g * tint.g,
original_tint.b * spell_charge_tint.b, original_tint.b * tint.b,
original_tint.a * spell_charge_tint.a original_tint.a * tint.a
) )
# Interpolate between original and fully charged tint based on pulse # Interpolate between original and fully charged tint based on pulse
@@ -4285,15 +4494,13 @@ func _sync_spell_charge_start():
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _sync_spell_charge_end(): func _sync_spell_charge_end():
# Sync spell charge end to other clients
if not is_multiplayer_authority(): if not is_multiplayer_authority():
is_charging_spell = false is_charging_spell = false
current_spell_element = "fire"
spell_incantation_played = false spell_incantation_played = false
_stop_spell_charge_particles() _stop_spell_charge_particles()
_stop_spell_charge_incantation() _stop_spell_charge_incantation()
_clear_spell_charge_tint() _clear_spell_charge_tint()
# Return to IDLE animation
_set_animation("IDLE") _set_animation("IDLE")
# Stop spell charging sounds # Stop spell charging sounds
@@ -4741,6 +4948,7 @@ func _sync_bomb_dropped(bomb_name: String, place_pos: Vector2):
if not attack_bomb_scene: if not attack_bomb_scene:
return return
var bomb = attack_bomb_scene.instantiate() var bomb = attack_bomb_scene.instantiate()
bomb.name = "PlacedBomb_" + bomb_name
get_parent().add_child(bomb) get_parent().add_child(bomb)
bomb.global_position = place_pos bomb.global_position = place_pos
bomb.setup(place_pos, self, Vector2.ZERO, false) # not thrown, fuse lit bomb.setup(place_pos, self, Vector2.ZERO, false) # not thrown, fuse lit
@@ -4749,20 +4957,17 @@ func _sync_bomb_dropped(bomb_name: String, place_pos: Vector2):
print(name, " (synced) dropped bomb at ", place_pos) print(name, " (synced) dropped bomb at ", place_pos)
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _sync_place_bomb(target_pos: Vector2): func _sync_place_bomb(bomb_id: String, target_pos: Vector2):
# Sync bomb placement to other clients (Human/Elf) # Sync bomb placement to other clients (Human/Elf)
if not is_multiplayer_authority(): if not is_multiplayer_authority():
if not attack_bomb_scene: if not attack_bomb_scene:
return return
# Spawn bomb at target position
var bomb = attack_bomb_scene.instantiate() var bomb = attack_bomb_scene.instantiate()
bomb.name = bomb_id
get_parent().add_child(bomb) get_parent().add_child(bomb)
bomb.global_position = target_pos bomb.global_position = target_pos
# Setup bomb without throw (placed directly)
bomb.setup(target_pos, self, Vector2.ZERO, false) # false = not thrown bomb.setup(target_pos, self, Vector2.ZERO, false) # false = not thrown
print(name, " (synced) placed bomb at ", target_pos) print(name, " (synced) placed bomb at ", target_pos)
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
@@ -4778,6 +4983,7 @@ func _sync_throw_bomb(bomb_name: String, bomb_pos: Vector2, throw_force: Vector2
if not attack_bomb_scene: if not attack_bomb_scene:
return return
var bomb = attack_bomb_scene.instantiate() var bomb = attack_bomb_scene.instantiate()
bomb.name = "ThrownBomb_" + bomb_name
get_parent().add_child(bomb) get_parent().add_child(bomb)
bomb.global_position = bomb_pos bomb.global_position = bomb_pos
bomb.setup(bomb_pos, self, throw_force, true) # true = is_thrown bomb.setup(bomb_pos, self, throw_force, true) # true = is_thrown
@@ -4785,6 +4991,16 @@ func _sync_throw_bomb(bomb_name: String, bomb_pos: Vector2, throw_force: Vector2
bomb.get_node("Sprite2D").visible = true bomb.get_node("Sprite2D").visible = true
print(name, " (synced) threw bomb from ", bomb_pos) print(name, " (synced) threw bomb from ", bomb_pos)
@rpc("any_peer", "reliable")
func _sync_bomb_collected(bomb_name: String):
# Another peer collected this bomb remove our copy so it doesn't keep exploding
# Collector already removed and added to inventory locally; we just free our instance
var bombs = get_tree().get_nodes_in_group("attack_bomb")
for b in bombs:
if b.name == bomb_name and is_instance_valid(b):
b.queue_free()
return
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _sync_throw(obj_name: String, throw_pos: Vector2, force: Vector2, thrower_name: String): func _sync_throw(obj_name: String, throw_pos: Vector2, force: Vector2, thrower_name: String):
# Sync throw to all clients (RPC sender already threw on their side) # Sync throw to all clients (RPC sender already threw on their side)
@@ -5358,6 +5574,28 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
_rpc_to_ready_peers("_sync_damage", [0.0, attacker_position, false, false, true]) # is_dodged = true _rpc_to_ready_peers("_sync_damage", [0.0, attacker_position, false, false, true]) # is_dodged = true
return # No damage taken, exit early return # No damage taken, exit early
# Check for shield block (would have hit; enemy attack from blocked direction; no burn)
if not is_burn_damage and is_shielding and shield_block_cooldown_timer <= 0.0:
var dir_to_attacker = (attacker_position - global_position).normalized()
if dir_to_attacker.length() < 0.01:
dir_to_attacker = Vector2.RIGHT
var block_dir = shield_block_direction.normalized() if shield_block_direction.length() > 0.01 else Vector2.DOWN
var dot = block_dir.dot(dir_to_attacker)
if dot > 0.5: # Lenient: attacker in front (~60° cone)
# Blocked: no damage, small knockback, BLOCKED notification, cooldown
shield_block_cooldown_timer = shield_block_cooldown_duration
var direction_from_attacker = (global_position - attacker_position).normalized()
velocity = direction_from_attacker * 90.0 # Small knockback
is_knocked_back = true
knockback_time = 0.0
if has_node("SfxBlockWithShield"):
$SfxBlockWithShield.play()
_show_damage_number(0.0, attacker_position, false, false, false, true) # is_blocked = true
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
_rpc_to_ready_peers("_sync_damage", [0.0, attacker_position, false, false, false, true])
print(name, " BLOCKED attack from direction ", dir_to_attacker)
return
# If taking damage while holding something, drop/throw immediately # If taking damage while holding something, drop/throw immediately
if held_object: if held_object:
if is_lifting: if is_lifting:
@@ -5457,6 +5695,9 @@ func _die():
velocity = Vector2.ZERO velocity = Vector2.ZERO
is_knocked_back = false is_knocked_back = false
damage_direction_lock_timer = 0.0 damage_direction_lock_timer = 0.0
is_shielding = false
was_shielding_last_frame = false
_update_shield_visibility()
# CRITICAL: Release any held object/player BEFORE dying to restore their collision layers # CRITICAL: Release any held object/player BEFORE dying to restore their collision layers
if held_object: if held_object:
@@ -5528,7 +5769,7 @@ func _die():
fade_tween.set_parallel(true) fade_tween.set_parallel(true)
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair, for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons, sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
sprite_headgear, sprite_weapon, shadow]: sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
if sprite_layer: if sprite_layer:
fade_tween.tween_property(sprite_layer, "modulate:a", 0.0, 0.5) fade_tween.tween_property(sprite_layer, "modulate:a", 0.0, 0.5)
@@ -5601,7 +5842,7 @@ func _respawn():
# Restore visibility (fade all sprite layers back in) # Restore visibility (fade all sprite layers back in)
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair, for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons, sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
sprite_headgear, sprite_weapon, shadow]: sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
if sprite_layer: if sprite_layer:
sprite_layer.modulate.a = 1.0 sprite_layer.modulate.a = 1.0
@@ -5741,7 +5982,7 @@ func _sync_respawn(spawn_pos: Vector2):
# Restore visibility # Restore visibility
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair, for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons, sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
sprite_headgear, sprite_weapon, shadow]: sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
if sprite_layer: if sprite_layer:
sprite_layer.modulate.a = 1.0 sprite_layer.modulate.a = 1.0
@@ -5867,7 +6108,7 @@ func _sync_race_and_stats(race: String, base_stats: Dictionary):
"Human": "Human":
character_stats.setEars(0) character_stats.setEars(0)
# Give Human starting spellbook (Tome of Flames) to remote players # Give Human (Wizard) starting spellbook (Tome of Flames) and Hat to remote players
# (Authority players get this in _setup_player_appearance) # (Authority players get this in _setup_player_appearance)
# Check if equipment is missing - give it regardless of whether race changed # Check if equipment is missing - give it regardless of whether race changed
if not is_multiplayer_authority(): if not is_multiplayer_authority():
@@ -5875,17 +6116,25 @@ func _sync_race_and_stats(race: String, base_stats: Dictionary):
if character_stats.equipment["offhand"] == null: if character_stats.equipment["offhand"] == null:
needs_equipment = true needs_equipment = true
else: else:
# Check if offhand is not Tome of Flames
var offhand = character_stats.equipment["offhand"] var offhand = character_stats.equipment["offhand"]
if not offhand or offhand.item_name != "Tome of Flames": if not offhand or offhand.item_name != "Tome of Flames":
needs_equipment = true needs_equipment = true
if character_stats.equipment["headgear"] == null:
needs_equipment = true
else:
var headgear = character_stats.equipment["headgear"]
if not headgear or headgear.item_name != "Hat":
needs_equipment = true
if needs_equipment: if needs_equipment:
var starting_tome = ItemDatabase.create_item("tome_of_flames") var starting_tome = ItemDatabase.create_item("tome_of_flames")
if starting_tome: if starting_tome:
character_stats.equipment["offhand"] = starting_tome character_stats.equipment["offhand"] = starting_tome
_apply_appearance_to_sprites() var starting_hat = ItemDatabase.create_item("hat")
print("Human player ", name, " (remote) received Tome of Flames via race sync") if starting_hat:
character_stats.equipment["headgear"] = starting_hat
_apply_appearance_to_sprites()
print("Human player ", name, " (remote) received Tome of Flames and Hat via race sync")
_: _:
character_stats.setEars(0) character_stats.setEars(0)
@@ -5990,6 +6239,25 @@ func _sync_inventory(inventory_data: Array):
character_stats.character_changed.emit(character_stats) character_stats.character_changed.emit(character_stats)
print(name, " inventory synced: ", character_stats.inventory.size(), " items") print(name, " inventory synced: ", character_stats.inventory.size(), " items")
@rpc("any_peer", "reliable")
func _apply_inventory_and_equipment_from_server(inventory_data: Array, equipment_data: Dictionary):
# Joiner receives inventory+equipment push from server after loot pickup (or other server-driven change).
# Always apply no authority rejection. Used only when server adds items to a remote player.
if multiplayer.is_server():
return
if not character_stats:
return
character_stats.inventory.clear()
for item_data in inventory_data:
if item_data != null:
character_stats.inventory.append(Item.new(item_data))
for slot_name in character_stats.equipment.keys():
var item_data = equipment_data.get(slot_name, null)
character_stats.equipment[slot_name] = Item.new(item_data) if item_data != null else null
_apply_appearance_to_sprites()
character_stats.character_changed.emit(character_stats)
print(name, " inventory+equipment applied from server: ", character_stats.inventory.size(), " items")
func heal(amount: float): func heal(amount: float):
if is_dead: if is_dead:
return return
@@ -6030,9 +6298,9 @@ func _sync_keys(new_key_count: int):
keys = new_key_count keys = new_key_count
@rpc("authority", "reliable") @rpc("authority", "reliable")
func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false): func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false, is_blocked: bool = false):
# Show damage number (red, using dmg_numbers.png font) above player # Show damage number (red, using dmg_numbers.png font) above player
# Show even if amount is 0 for MISS/DODGED # Show even if amount is 0 for MISS/DODGED/BLOCKED
var damage_number_scene = preload("res://scenes/damage_number.tscn") var damage_number_scene = preload("res://scenes/damage_number.tscn")
if not damage_number_scene: if not damage_number_scene:
@@ -6049,6 +6317,9 @@ func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool =
elif is_miss: elif is_miss:
damage_label.label = "MISS" damage_label.label = "MISS"
damage_label.color = Color.GRAY damage_label.color = Color.GRAY
elif is_blocked:
damage_label.label = "BLOCKED"
damage_label.color = Color(0.4, 0.65, 1.0) # Light blue
else: else:
damage_label.label = str(int(amount)) damage_label.label = str(int(amount))
damage_label.color = Color.ORANGE if is_crit else Color.RED damage_label.color = Color.ORANGE if is_crit else Color.RED
@@ -6138,13 +6409,23 @@ func _on_level_up_stats(stats_increased: Array):
base_y_offset -= y_spacing base_y_offset -= y_spacing
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false): func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false, is_blocked: bool = false):
# This RPC only syncs visual effects, not damage application # This RPC only syncs visual effects, not damage application
# (damage is already applied via rpc_take_damage) # (damage is already applied via rpc_take_damage)
if not is_multiplayer_authority(): if not is_multiplayer_authority():
# If dodged, only show dodge text, no other effects # If dodged, only show dodge text, no other effects
if is_dodged: if is_dodged:
_show_damage_number(0.0, attacker_position, false, false, true) _show_damage_number(0.0, attacker_position, false, false, true, false)
return
# If blocked, show BLOCKED, small knockback, block sound; no damage effects
if is_blocked:
var block_knock_dir = (global_position - attacker_position).normalized()
velocity = block_knock_dir * 90.0
is_knocked_back = true
knockback_time = 0.0
if has_node("SfxBlockWithShield"):
$SfxBlockWithShield.play()
_show_damage_number(0.0, attacker_position, false, false, false, true)
return return
# Play damage sound and effects (rate limited to prevent spam when tab becomes active) # Play damage sound and effects (rate limited to prevent spam when tab becomes active)
@@ -6181,7 +6462,7 @@ func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = fa
tween.tween_property(sprite_body, "modulate", Color.WHITE, 0.1) tween.tween_property(sprite_body, "modulate", Color.WHITE, 0.1)
# Show damage number # Show damage number
_show_damage_number(_amount, attacker_position, is_crit, is_miss, false) _show_damage_number(_amount, attacker_position, is_crit, is_miss, false, false)
func on_grabbed(by_player): func on_grabbed(by_player):
print(name, " grabbed by ", by_player.name) print(name, " grabbed by ", by_player.name)