diff --git a/src/assets/audio/sfx/weapons/bow/bow_hit1.mp3 b/src/assets/audio/sfx/weapons/bow/bow_hit1.mp3 new file mode 100644 index 0000000..73007df Binary files /dev/null and b/src/assets/audio/sfx/weapons/bow/bow_hit1.mp3 differ diff --git a/src/assets/audio/sfx/weapons/bow/bow_hit1.mp3.import b/src/assets/audio/sfx/weapons/bow/bow_hit1.mp3.import new file mode 100644 index 0000000..ed6c5e9 --- /dev/null +++ b/src/assets/audio/sfx/weapons/bow/bow_hit1.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://dgl0rbq5xu58c" +path="res://.godot/imported/bow_hit1.mp3-476fb54f8187c5f01a54ae94a3069381.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/weapons/bow/bow_hit1.mp3" +dest_files=["res://.godot/imported/bow_hit1.mp3-476fb54f8187c5f01a54ae94a3069381.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/audio/sfx/weapons/bow/bow_hit2.mp3 b/src/assets/audio/sfx/weapons/bow/bow_hit2.mp3 new file mode 100644 index 0000000..ee74b9c Binary files /dev/null and b/src/assets/audio/sfx/weapons/bow/bow_hit2.mp3 differ diff --git a/src/assets/audio/sfx/weapons/bow/bow_hit2.mp3.import b/src/assets/audio/sfx/weapons/bow/bow_hit2.mp3.import new file mode 100644 index 0000000..f29178c --- /dev/null +++ b/src/assets/audio/sfx/weapons/bow/bow_hit2.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://bdxpyn6xks4kx" +path="res://.godot/imported/bow_hit2.mp3-7d578bec9a2aa746be41a000c47c524a.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/weapons/bow/bow_hit2.mp3" +dest_files=["res://.godot/imported/bow_hit2.mp3-7d578bec9a2aa746be41a000c47c524a.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/audio/sfx/weapons/bow/bow_hit3.mp3 b/src/assets/audio/sfx/weapons/bow/bow_hit3.mp3 new file mode 100644 index 0000000..ee74b9c Binary files /dev/null and b/src/assets/audio/sfx/weapons/bow/bow_hit3.mp3 differ diff --git a/src/assets/audio/sfx/weapons/bow/bow_hit3.mp3.import b/src/assets/audio/sfx/weapons/bow/bow_hit3.mp3.import new file mode 100644 index 0000000..a4f6118 --- /dev/null +++ b/src/assets/audio/sfx/weapons/bow/bow_hit3.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://xl0c4hddgu6p" +path="res://.godot/imported/bow_hit3.mp3-38811bc6d247e5a69ddec5030a1b0c85.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/weapons/bow/bow_hit3.mp3" +dest_files=["res://.godot/imported/bow_hit3.mp3-38811bc6d247e5a69ddec5030a1b0c85.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/audio/sfx/weapons/bow/bow_impact.mp3 b/src/assets/audio/sfx/weapons/bow/bow_impact.mp3 new file mode 100644 index 0000000..cd30190 Binary files /dev/null and b/src/assets/audio/sfx/weapons/bow/bow_impact.mp3 differ diff --git a/src/assets/audio/sfx/weapons/bow/bow_impact.mp3.import b/src/assets/audio/sfx/weapons/bow/bow_impact.mp3.import new file mode 100644 index 0000000..1c4043e --- /dev/null +++ b/src/assets/audio/sfx/weapons/bow/bow_impact.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://cidk2vlmt5gse" +path="res://.godot/imported/bow_impact.mp3-a822d7ab29d25d4d6f70ded4c4b90ae1.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/weapons/bow/bow_impact.mp3" +dest_files=["res://.godot/imported/bow_impact.mp3-a822d7ab29d25d4d6f70ded4c4b90ae1.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/audio/sfx/weapons/bow/bow_release2.mp3 b/src/assets/audio/sfx/weapons/bow/bow_release2.mp3 new file mode 100644 index 0000000..cf17493 Binary files /dev/null and b/src/assets/audio/sfx/weapons/bow/bow_release2.mp3 differ diff --git a/src/assets/audio/sfx/weapons/bow/bow_release2.mp3.import b/src/assets/audio/sfx/weapons/bow/bow_release2.mp3.import new file mode 100644 index 0000000..6e796f7 --- /dev/null +++ b/src/assets/audio/sfx/weapons/bow/bow_release2.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://b6mwlp2ap0wbj" +path="res://.godot/imported/bow_release2.mp3-3716aa2848dcccc35e4091a1798404ab.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/weapons/bow/bow_release2.mp3" +dest_files=["res://.godot/imported/bow_release2.mp3-3716aa2848dcccc35e4091a1798404ab.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/audio/sfx/weapons/bow/bow_release3.mp3 b/src/assets/audio/sfx/weapons/bow/bow_release3.mp3 new file mode 100644 index 0000000..d89e734 Binary files /dev/null and b/src/assets/audio/sfx/weapons/bow/bow_release3.mp3 differ diff --git a/src/assets/audio/sfx/weapons/bow/bow_release3.mp3.import b/src/assets/audio/sfx/weapons/bow/bow_release3.mp3.import new file mode 100644 index 0000000..026ee2f --- /dev/null +++ b/src/assets/audio/sfx/weapons/bow/bow_release3.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://d1ut5lnlch0k2" +path="res://.godot/imported/bow_release3.mp3-77df3c50d4604393ae47c3b2fdbe3adb.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/weapons/bow/bow_release3.mp3" +dest_files=["res://.godot/imported/bow_release3.mp3-77df3c50d4604393ae47c3b2fdbe3adb.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/audio/sfx/weapons/bow/bow_release_1.mp3 b/src/assets/audio/sfx/weapons/bow/bow_release_1.mp3 new file mode 100644 index 0000000..ff78705 Binary files /dev/null and b/src/assets/audio/sfx/weapons/bow/bow_release_1.mp3 differ diff --git a/src/assets/audio/sfx/weapons/bow/bow_release_1.mp3.import b/src/assets/audio/sfx/weapons/bow/bow_release_1.mp3.import new file mode 100644 index 0000000..8fc642c --- /dev/null +++ b/src/assets/audio/sfx/weapons/bow/bow_release_1.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://b6klanrso0vvq" +path="res://.godot/imported/bow_release_1.mp3-4a5c5e9ee81310c6d8c689dca7b70c8d.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/weapons/bow/bow_release_1.mp3" +dest_files=["res://.godot/imported/bow_release_1.mp3-4a5c5e9ee81310c6d8c689dca7b70c8d.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/audio/sfx/weapons/bow/bow_shoot.mp3 b/src/assets/audio/sfx/weapons/bow/bow_shoot.mp3 new file mode 100644 index 0000000..7b3dac9 Binary files /dev/null and b/src/assets/audio/sfx/weapons/bow/bow_shoot.mp3 differ diff --git a/src/assets/audio/sfx/weapons/bow/bow_shoot.mp3.import b/src/assets/audio/sfx/weapons/bow/bow_shoot.mp3.import new file mode 100644 index 0000000..04128e8 --- /dev/null +++ b/src/assets/audio/sfx/weapons/bow/bow_shoot.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://cwgh6e1auq4hi" +path="res://.godot/imported/bow_shoot.mp3-96542acce0715d2c60b0b2b6674527c7.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/weapons/bow/bow_shoot.mp3" +dest_files=["res://.godot/imported/bow_shoot.mp3-96542acce0715d2c60b0b2b6674527c7.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/audio/sfx/weapons/bow/bow_shoot2.mp3 b/src/assets/audio/sfx/weapons/bow/bow_shoot2.mp3 new file mode 100644 index 0000000..41424b0 Binary files /dev/null and b/src/assets/audio/sfx/weapons/bow/bow_shoot2.mp3 differ diff --git a/src/assets/audio/sfx/weapons/bow/bow_shoot2.mp3.import b/src/assets/audio/sfx/weapons/bow/bow_shoot2.mp3.import new file mode 100644 index 0000000..d08099d --- /dev/null +++ b/src/assets/audio/sfx/weapons/bow/bow_shoot2.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://c3pbc1nlgf2uy" +path="res://.godot/imported/bow_shoot2.mp3-4f953e5f72066f9f2e50fdc79968d909.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/weapons/bow/bow_shoot2.mp3" +dest_files=["res://.godot/imported/bow_shoot2.mp3-4f953e5f72066f9f2e50fdc79968d909.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/audio/sfx/weapons/bow/buckle_bow.mp3 b/src/assets/audio/sfx/weapons/bow/buckle_bow.mp3 new file mode 100644 index 0000000..e1917a7 Binary files /dev/null and b/src/assets/audio/sfx/weapons/bow/buckle_bow.mp3 differ diff --git a/src/assets/audio/sfx/weapons/bow/buckle_bow.mp3.import b/src/assets/audio/sfx/weapons/bow/buckle_bow.mp3.import new file mode 100644 index 0000000..d93ad3e --- /dev/null +++ b/src/assets/audio/sfx/weapons/bow/buckle_bow.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://cgya50qrx8gms" +path="res://.godot/imported/buckle_bow.mp3-cfc5ff3fd11457403ea20bba39956b64.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/weapons/bow/buckle_bow.mp3" +dest_files=["res://.godot/imported/buckle_bow.mp3-cfc5ff3fd11457403ea20bba39956b64.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/audio/sfx/weapons/bow/namnlös.mp3 b/src/assets/audio/sfx/weapons/bow/namnlös.mp3 new file mode 100644 index 0000000..ee74b9c Binary files /dev/null and b/src/assets/audio/sfx/weapons/bow/namnlös.mp3 differ diff --git a/src/assets/audio/sfx/weapons/bow/namnlös.mp3.import b/src/assets/audio/sfx/weapons/bow/namnlös.mp3.import new file mode 100644 index 0000000..af26117 --- /dev/null +++ b/src/assets/audio/sfx/weapons/bow/namnlös.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://fuytnjt12y1r" +path="res://.godot/imported/namnlös.mp3-f82b5b1850a2f9f3e25ab453314c001c.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/weapons/bow/namnlös.mp3" +dest_files=["res://.godot/imported/namnlös.mp3-f82b5b1850a2f9f3e25ab453314c001c.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/audio/sfx/wizard/incantations/exo-splosion.mp3 b/src/assets/audio/sfx/wizard/incantations/exo-splosion.mp3 new file mode 100644 index 0000000..d4df037 Binary files /dev/null and b/src/assets/audio/sfx/wizard/incantations/exo-splosion.mp3 differ diff --git a/src/assets/audio/sfx/wizard/incantations/exo-splosion.mp3.import b/src/assets/audio/sfx/wizard/incantations/exo-splosion.mp3.import new file mode 100644 index 0000000..932531e --- /dev/null +++ b/src/assets/audio/sfx/wizard/incantations/exo-splosion.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://cfppof6cgm56a" +path="res://.godot/imported/exo-splosion.mp3-0700dbdecd62fd0dae19f5eb96b3e3d2.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/wizard/incantations/exo-splosion.mp3" +dest_files=["res://.godot/imported/exo-splosion.mp3-0700dbdecd62fd0dae19f5eb96b3e3d2.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/audio/sfx/wizard/incantations/flame-u.mp3 b/src/assets/audio/sfx/wizard/incantations/flame-u.mp3 new file mode 100644 index 0000000..7e1900d Binary files /dev/null and b/src/assets/audio/sfx/wizard/incantations/flame-u.mp3 differ diff --git a/src/assets/audio/sfx/wizard/incantations/flame-u.mp3.import b/src/assets/audio/sfx/wizard/incantations/flame-u.mp3.import new file mode 100644 index 0000000..57f3014 --- /dev/null +++ b/src/assets/audio/sfx/wizard/incantations/flame-u.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://b3yy3ohbhettx" +path="res://.godot/imported/flame-u.mp3-5a63fc6b70f4d9e856832e86b5c7127e.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/wizard/incantations/flame-u.mp3" +dest_files=["res://.godot/imported/flame-u.mp3-5a63fc6b70f4d9e856832e86b5c7127e.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/audio/sfx/wizard/incantations/indignation.mp3 b/src/assets/audio/sfx/wizard/incantations/indignation.mp3 new file mode 100644 index 0000000..4825b59 Binary files /dev/null and b/src/assets/audio/sfx/wizard/incantations/indignation.mp3 differ diff --git a/src/assets/audio/sfx/wizard/incantations/indignation.mp3.import b/src/assets/audio/sfx/wizard/incantations/indignation.mp3.import new file mode 100644 index 0000000..2a7a97e --- /dev/null +++ b/src/assets/audio/sfx/wizard/incantations/indignation.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://0xm3gyh8051h" +path="res://.godot/imported/indignation.mp3-38f0d18f01596b101ac929b743c845ec.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/wizard/incantations/indignation.mp3" +dest_files=["res://.godot/imported/indignation.mp3-38f0d18f01596b101ac929b743c845ec.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/audio/sfx/wizard/incantations/spell_effect.mp3 b/src/assets/audio/sfx/wizard/incantations/spell_effect.mp3 new file mode 100644 index 0000000..9084520 Binary files /dev/null and b/src/assets/audio/sfx/wizard/incantations/spell_effect.mp3 differ diff --git a/src/assets/audio/sfx/wizard/incantations/spell_effect.mp3.import b/src/assets/audio/sfx/wizard/incantations/spell_effect.mp3.import new file mode 100644 index 0000000..2c42d06 --- /dev/null +++ b/src/assets/audio/sfx/wizard/incantations/spell_effect.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://jri0jevyl5d3" +path="res://.godot/imported/spell_effect.mp3-07edd758f03d0cb5fe272a5562ddc335.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/wizard/incantations/spell_effect.mp3" +dest_files=["res://.godot/imported/spell_effect.mp3-07edd758f03d0cb5fe272a5562ddc335.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Hairstyles/FHairstyle3White.png b/src/assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Hairstyles/FHairstyle3White.png index 3790ecc..fb05283 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Hairstyles/FHairstyle3White.png and b/src/assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Hairstyles/FHairstyle3White.png differ diff --git a/src/assets/gfx/Puny-Characters/WeaponOverlayer.png b/src/assets/gfx/Puny-Characters/WeaponOverlayer.png index f66687d..62e1695 100644 Binary files a/src/assets/gfx/Puny-Characters/WeaponOverlayer.png and b/src/assets/gfx/Puny-Characters/WeaponOverlayer.png differ diff --git a/src/assets/gfx/RPG DUNGEON VOL 3.png b/src/assets/gfx/RPG DUNGEON VOL 3.png index d520669..301b011 100644 Binary files a/src/assets/gfx/RPG DUNGEON VOL 3.png and b/src/assets/gfx/RPG DUNGEON VOL 3.png differ diff --git a/src/assets/gfx/fx/burn.png b/src/assets/gfx/fx/burn.png new file mode 100644 index 0000000..35cebe6 Binary files /dev/null and b/src/assets/gfx/fx/burn.png differ diff --git a/src/assets/gfx/fx/burn.png.import b/src/assets/gfx/fx/burn.png.import new file mode 100644 index 0000000..7334fb9 --- /dev/null +++ b/src/assets/gfx/fx/burn.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://di2yxjrvw7hi0" +path="res://.godot/imported/burn.png-d83aa157ca1f58cbb469aa9293eb2e7a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/gfx/fx/burn.png" +dest_files=["res://.godot/imported/burn.png-d83aa157ca1f58cbb469aa9293eb2e7a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/assets/gfx/fx/explosion.png b/src/assets/gfx/fx/explosion.png new file mode 100644 index 0000000..b861c18 Binary files /dev/null and b/src/assets/gfx/fx/explosion.png differ diff --git a/src/assets/gfx/fx/explosion.png.import b/src/assets/gfx/fx/explosion.png.import new file mode 100644 index 0000000..37e425d --- /dev/null +++ b/src/assets/gfx/fx/explosion.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cbfyeow1v5vf" +path="res://.godot/imported/explosion.png-866d55c49d5e6cd502d6b8f3c389cf2f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/gfx/fx/explosion.png" +dest_files=["res://.godot/imported/explosion.png-866d55c49d5e6cd502d6b8f3c389cf2f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/assets/gfx/fx/lightning1.png b/src/assets/gfx/fx/lightning1.png new file mode 100644 index 0000000..cb9218e Binary files /dev/null and b/src/assets/gfx/fx/lightning1.png differ diff --git a/src/assets/gfx/fx/lightning1.png.import b/src/assets/gfx/fx/lightning1.png.import new file mode 100644 index 0000000..82c960f --- /dev/null +++ b/src/assets/gfx/fx/lightning1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dcnp3xxxkxvou" +path="res://.godot/imported/lightning1.png-0e25a600051273da79e7c57c0d4f2c1a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/gfx/fx/lightning1.png" +dest_files=["res://.godot/imported/lightning1.png-0e25a600051273da79e7c57c0d4f2c1a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/assets/gfx/fx/lightning2.png b/src/assets/gfx/fx/lightning2.png new file mode 100644 index 0000000..910b3a8 Binary files /dev/null and b/src/assets/gfx/fx/lightning2.png differ diff --git a/src/assets/gfx/fx/lightning2.png.import b/src/assets/gfx/fx/lightning2.png.import new file mode 100644 index 0000000..a80cbbd --- /dev/null +++ b/src/assets/gfx/fx/lightning2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cbo4vqifr1qbb" +path="res://.godot/imported/lightning2.png-33f12035b9169d598e355a94cd5e4260.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/gfx/fx/lightning2.png" +dest_files=["res://.godot/imported/lightning2.png-33f12035b9169d598e355a94cd5e4260.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/assets/gfx/fx/lightning3.png b/src/assets/gfx/fx/lightning3.png new file mode 100644 index 0000000..0a8c1b5 Binary files /dev/null and b/src/assets/gfx/fx/lightning3.png differ diff --git a/src/assets/gfx/fx/lightning3.png.import b/src/assets/gfx/fx/lightning3.png.import new file mode 100644 index 0000000..a70de7c --- /dev/null +++ b/src/assets/gfx/fx/lightning3.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d1hq1q3isscm7" +path="res://.godot/imported/lightning3.png-1e328886a874a028282238adf48ff44c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/gfx/fx/lightning3.png" +dest_files=["res://.godot/imported/lightning3.png-1e328886a874a028282238adf48ff44c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/assets/gfx/fx/lightning4.png b/src/assets/gfx/fx/lightning4.png new file mode 100644 index 0000000..b73bc0a Binary files /dev/null and b/src/assets/gfx/fx/lightning4.png differ diff --git a/src/assets/gfx/fx/lightning4.png.import b/src/assets/gfx/fx/lightning4.png.import new file mode 100644 index 0000000..ccc3742 --- /dev/null +++ b/src/assets/gfx/fx/lightning4.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://jur4f4d4yjem" +path="res://.godot/imported/lightning4.png-a7d7ab97864dc0c6e131cdf30f1a9515.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/gfx/fx/lightning4.png" +dest_files=["res://.godot/imported/lightning4.png-a7d7ab97864dc0c6e131cdf30f1a9515.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/assets/gfx/fx/lightning5.png b/src/assets/gfx/fx/lightning5.png new file mode 100644 index 0000000..620cdb2 Binary files /dev/null and b/src/assets/gfx/fx/lightning5.png differ diff --git a/src/assets/gfx/fx/lightning5.png.import b/src/assets/gfx/fx/lightning5.png.import new file mode 100644 index 0000000..0f3571c --- /dev/null +++ b/src/assets/gfx/fx/lightning5.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://m5qoox3y452v" +path="res://.godot/imported/lightning5.png-499a1138ebdc5b3e011b6ac9d4f23b9a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/gfx/fx/lightning5.png" +dest_files=["res://.godot/imported/lightning5.png-499a1138ebdc5b3e011b6ac9d4f23b9a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/assets/gfx/fx/lightning6.png b/src/assets/gfx/fx/lightning6.png new file mode 100644 index 0000000..6bd68be Binary files /dev/null and b/src/assets/gfx/fx/lightning6.png differ diff --git a/src/assets/gfx/fx/lightning6.png.import b/src/assets/gfx/fx/lightning6.png.import new file mode 100644 index 0000000..a3194f9 --- /dev/null +++ b/src/assets/gfx/fx/lightning6.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ysa0im6rm7m7" +path="res://.godot/imported/lightning6.png-fdec43eec00eb3e78dfcd906fe6aba71.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/gfx/fx/lightning6.png" +dest_files=["res://.godot/imported/lightning6.png-fdec43eec00eb3e78dfcd906fe6aba71.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/assets/gfx/fx/lightning7.png b/src/assets/gfx/fx/lightning7.png new file mode 100644 index 0000000..0d6a1f1 Binary files /dev/null and b/src/assets/gfx/fx/lightning7.png differ diff --git a/src/assets/gfx/fx/lightning7.png.import b/src/assets/gfx/fx/lightning7.png.import new file mode 100644 index 0000000..ad9736e --- /dev/null +++ b/src/assets/gfx/fx/lightning7.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dv2874j8slec0" +path="res://.godot/imported/lightning7.png-6d0213aaa6fd5b90e8a6f42d0cedaff4.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/gfx/fx/lightning7.png" +dest_files=["res://.godot/imported/lightning7.png-6d0213aaa6fd5b90e8a6f42d0cedaff4.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/assets/gfx/fx/magic/red_star.png b/src/assets/gfx/fx/magic/red_star.png new file mode 100644 index 0000000..2dee972 Binary files /dev/null and b/src/assets/gfx/fx/magic/red_star.png differ diff --git a/src/assets/gfx/fx/magic/red_star.png.import b/src/assets/gfx/fx/magic/red_star.png.import new file mode 100644 index 0000000..744cf15 --- /dev/null +++ b/src/assets/gfx/fx/magic/red_star.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b6jq2xdkwlckv" +path="res://.godot/imported/red_star.png-7377430fa454b1d679af156e73fca052.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/gfx/fx/magic/red_star.png" +dest_files=["res://.godot/imported/red_star.png-7377430fa454b1d679af156e73fca052.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/assets/gfx/fx/sparks_64x64_x4_256.png b/src/assets/gfx/fx/sparks_64x64_x4_256.png new file mode 100644 index 0000000..cedfde7 Binary files /dev/null and b/src/assets/gfx/fx/sparks_64x64_x4_256.png differ diff --git a/src/assets/gfx/fx/sparks_64x64_x4_256.png.import b/src/assets/gfx/fx/sparks_64x64_x4_256.png.import new file mode 100644 index 0000000..ff88e3a --- /dev/null +++ b/src/assets/gfx/fx/sparks_64x64_x4_256.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://rh731dlsygsq" +path="res://.godot/imported/sparks_64x64_x4_256.png-e0462725852ee1181490a3ea96243748.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/gfx/fx/sparks_64x64_x4_256.png" +dest_files=["res://.godot/imported/sparks_64x64_x4_256.png-e0462725852ee1181490a3ea96243748.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/assets/gfx/pickups/items_n_shit.png b/src/assets/gfx/pickups/items_n_shit.png index 4294cbd..f368042 100644 Binary files a/src/assets/gfx/pickups/items_n_shit.png and b/src/assets/gfx/pickups/items_n_shit.png differ diff --git a/src/scenes/attack_spell_flame.tscn b/src/scenes/attack_spell_flame.tscn new file mode 100644 index 0000000..32a6a7b --- /dev/null +++ b/src/scenes/attack_spell_flame.tscn @@ -0,0 +1,86 @@ +[gd_scene format=3 uid="uid://b8k3m2n4p5q6r"] + +[ext_resource type="Texture2D" uid="uid://dwthqtcn5sqoj" path="res://assets/gfx/fx/flames/nedladdning (14).png" id="1_52nqi"] +[ext_resource type="Script" uid="uid://pufa280hfhiy" path="res://scripts/attack_spell_flame.gd" id="1_flame"] +[ext_resource type="Shader" uid="uid://c40fb6mfe76g3" path="res://shaders/fire_light.gdshader" id="3_g5mbl"] +[ext_resource type="AudioStream" uid="uid://d1ylxthy45aw" path="res://assets/audio/sfx/environment/fireplace/fireplace-01.mp3" id="4_n6h4x"] +[ext_resource type="AudioStream" uid="uid://dd6xrtcx2vtlx" path="res://assets/audio/sfx/environment/fireplace/fireplace-02.mp3" id="5_2hde6"] +[ext_resource type="AudioStream" uid="uid://bjlq8la2amdut" path="res://assets/audio/sfx/environment/fireplace/fireplace-03.mp3" id="6_sy6oy"] +[ext_resource type="AudioStream" uid="uid://cxw5pdksu1o8v" path="res://assets/audio/sfx/environment/fireplace/fireplace-04.mp3" id="7_l6fjl"] +[ext_resource type="AudioStream" uid="uid://cxtp23isjsfvw" path="res://assets/audio/sfx/environment/fireplace/fireplace-05.mp3" id="8_rlm0w"] +[ext_resource type="AudioStream" uid="uid://rtn5i86tlqyh" path="res://assets/audio/sfx/environment/fireplace/fireplace-06.mp3" id="9_8o5sr"] +[ext_resource type="AudioStream" uid="uid://jri0jevyl5d3" path="res://assets/audio/sfx/wizard/incantations/spell_effect.mp3" id="10_2hde6"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_rach5"] +shader = ExtResource("3_g5mbl") +shader_parameter/brightness_multiplier = 3.0 + +[sub_resource type="Gradient" id="Gradient_lquwl"] +offsets = PackedFloat32Array(0.743, 0.74418604) +colors = PackedColorArray(1, 1, 1, 1, 0, 0, 0, 0) + +[sub_resource type="GradientTexture2D" id="GradientTexture2D_w00nx"] +gradient = SubResource("Gradient_lquwl") +fill = 1 +fill_from = Vector2(0.508547, 0.487179) +fill_to = Vector2(0.974359, 0.0470085) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_flame"] +size = Vector2(16, 16) + +[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_1qtic"] +streams_count = 6 +stream_0/stream = ExtResource("4_n6h4x") +stream_1/stream = ExtResource("5_2hde6") +stream_2/stream = ExtResource("6_sy6oy") +stream_3/stream = ExtResource("7_l6fjl") +stream_4/stream = ExtResource("8_rlm0w") +stream_5/stream = ExtResource("9_8o5sr") + +[node name="FlameSpell" type="Node2D" unique_id=250449910] +script = ExtResource("1_flame") + +[node name="Sprite2D" type="Sprite2D" parent="." unique_id=546834923] +texture = ExtResource("1_52nqi") +hframes = 4 +vframes = 4 +frame = 4 + +[node name="ExplosionInit" type="Sprite2D" parent="." unique_id=123456789] +visible = false +texture = ExtResource("1_52nqi") +hframes = 4 +vframes = 4 + +[node name="TorchLight" type="PointLight2D" parent="." unique_id=1247002844] +modulate = Color(1.353256, 1.353256, 1.353256, 1) +z_index = 10 +material = SubResource("ShaderMaterial_rach5") +position = Vector2(0, -1) +blend_mode = 2 +texture = SubResource("GradientTexture2D_w00nx") + +[node name="Area2D" type="Area2D" parent="." unique_id=556563630] +collision_layer = 4 +collision_mask = 3 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D" unique_id=520125161] +shape = SubResource("RectangleShape2D_flame") +debug_color = Color(0.70196074, 0, 0.09064378, 0.41960785) + +[node name="LifetimeTimer" type="Timer" parent="." unique_id=77775231] +wait_time = 2.0 +one_shot = true + +[node name="SfxFire" type="AudioStreamPlayer2D" parent="." unique_id=8963001] +stream = SubResource("AudioStreamRandomizer_1qtic") +volume_db = 4.996 +max_distance = 1040.0 +attenuation = 2.2973964 +panning_strength = 1.09 + +[node name="SfxInit" type="AudioStreamPlayer2D" parent="." unique_id=467371620] +stream = ExtResource("10_2hde6") +attenuation = 1.5157177 +panning_strength = 1.04 +bus = &"Sfx" diff --git a/src/scenes/debuff_burn.tscn b/src/scenes/debuff_burn.tscn new file mode 100644 index 0000000..9dfd3a3 --- /dev/null +++ b/src/scenes/debuff_burn.tscn @@ -0,0 +1,14 @@ +[gd_scene format=3 uid="uid://debuff_burn_12345"] + +[ext_resource type="Texture2D" path="res://assets/gfx/fx/burn.png" id="1_burn"] +[ext_resource type="Script" path="res://scripts/debuff_burn.gd" id="1_script"] + +[node name="DebuffBurn" type="Node2D"] +script = ExtResource("1_script") + +[node name="Sprite2D" type="Sprite2D" parent="."] +texture = ExtResource("1_burn") +hframes = 4 +vframes = 4 +frame = 0 +z_index = 5 diff --git a/src/scenes/player.tscn b/src/scenes/player.tscn index 2c7c1ca..1e5d62a 100644 --- a/src/scenes/player.tscn +++ b/src/scenes/player.tscn @@ -29,8 +29,13 @@ [ext_resource type="AudioStream" uid="uid://bdhmel5vyixng" path="res://assets/audio/sfx/player/take_damage/player_damaged_07.wav.mp3" id="26_gl8cc"] [ext_resource type="AudioStream" uid="uid://4vulahdsj4i2" path="res://assets/audio/sfx/swoosh/throw_01.wav.mp3" id="27_31cv2"] [ext_resource type="AudioStream" uid="uid://w6yon88kjfml" path="res://assets/audio/sfx/nickes/lift_sfx.ogg" id="28_pf23h"] -[ext_resource type="AudioStream" uid="uid://d4kjyb1olr74s" path="res://assets/audio/sfx/weapons/bow/bow_draw-02.mp3" id="30_gl8cc"] +[ext_resource type="AudioStream" uid="uid://b6klanrso0vvq" path="res://assets/audio/sfx/weapons/bow/bow_release_1.mp3" id="30_md1ol"] [ext_resource type="AudioStream" uid="uid://mbtpqlb5n3gd" path="res://assets/audio/sfx/weapons/bow/bow_no_arrow.mp3" id="31_487ah"] +[ext_resource type="AudioStream" uid="uid://b6mwlp2ap0wbj" path="res://assets/audio/sfx/weapons/bow/bow_release2.mp3" id="31_bj30b"] +[ext_resource type="AudioStream" uid="uid://cgya50qrx8gms" path="res://assets/audio/sfx/weapons/bow/buckle_bow.mp3" id="32_gl8cc"] +[ext_resource type="AudioStream" uid="uid://d1ut5lnlch0k2" path="res://assets/audio/sfx/weapons/bow/bow_release3.mp3" id="32_jc3p3"] +[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"] [sub_resource type="Gradient" id="Gradient_wqfne"] colors = PackedColorArray(0, 0, 0, 1, 1, 0.13732082, 0.092538536, 1) @@ -275,6 +280,14 @@ stream_4/stream = ExtResource("24_wqfne") stream_5/stream = ExtResource("25_wnwbv") stream_6/stream = ExtResource("26_gl8cc") +[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_bj30b"] +playback_mode = 1 +random_pitch = 1.0123794 +streams_count = 3 +stream_0/stream = ExtResource("30_md1ol") +stream_1/stream = ExtResource("31_bj30b") +stream_2/stream = ExtResource("32_jc3p3") + [sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_hhpqf"] random_pitch = 1.0630184 streams_count = 1 @@ -457,7 +470,7 @@ shadow_enabled = true max_distance = 100.0 [node name="SfxBowShoot" type="AudioStreamPlayer2D" parent="." unique_id=340970961] -stream = ExtResource("30_gl8cc") +stream = SubResource("AudioStreamRandomizer_bj30b") pitch_scale = 1.33 attenuation = 6.7271657 @@ -465,3 +478,18 @@ attenuation = 6.7271657 stream = SubResource("AudioStreamRandomizer_hhpqf") max_distance = 1455.0 attenuation = 7.4642572 + +[node name="SfxBuckleBow" type="AudioStreamPlayer2D" parent="." unique_id=1991119205] +stream = ExtResource("32_gl8cc") +attenuation = 7.727478 +panning_strength = 1.03 + +[node name="SfxSpellCharge" type="AudioStreamPlayer2D" parent="." unique_id=282455991] +stream = ExtResource("35_bj30b") + +[node name="SfxSpellIncantation" type="AudioStreamPlayer2D" parent="." unique_id=300820616] +stream = ExtResource("36_jc3p3") +volume_db = 5.729 +attenuation = 7.727487 +panning_strength = 1.04 +bus = &"Sfx" diff --git a/src/scripts/attack_spell_flame.gd b/src/scripts/attack_spell_flame.gd new file mode 100644 index 0000000..5d1cc59 --- /dev/null +++ b/src/scripts/attack_spell_flame.gd @@ -0,0 +1,215 @@ +extends Node2D + +# Flame Spell - Spawns at target position and deals damage over time + +@export var damage: float = 15.0 +@export var damage_interval: float = 0.5 # Deal damage every 0.5 seconds +@export var lifetime: float = 2.0 # Spell lasts 2 seconds + +var player_owner: Node = null +var hit_targets = {} # Track what we've already hit +var first_hit_targets = {} # Track targets that haven't taken initial damage yet +var damage_timer: float = 0.0 +var animation_timer: float = 0.0 +var current_frame: int = 4 # Start at frame 4 (first burning frame) +var is_fading_out: bool = false # True when playing fade-out frames (2, 1, 0) +var fade_out_frame_index: int = 0 # Index into fade-out frames [2, 1, 0] +var initial_damage_window: float = 0.2 # Initial 0.2 seconds for bonus damage +var initial_damage_multiplier: float = 3.0 # Initial damage is 3x base damage +var spell_start_time: float = 0.0 # Time when spell started +var explosion_init_frame: int = 0 # Current frame of explosion init animation +var explosion_init_timer: float = 0.0 # Timer for explosion init animation + +@onready var sprite = $Sprite2D +@onready var explosion_init_sprite = $ExplosionInit +@onready var hit_area = $Area2D +@onready var lifetime_timer = $LifetimeTimer + +func _ready(): + # Connect area signals + if hit_area and not hit_area.body_entered.is_connected(_on_body_entered): + hit_area.body_entered.connect(_on_body_entered) + + # Start lifetime timer + lifetime_timer.wait_time = lifetime + lifetime_timer.timeout.connect(_on_lifetime_expired) + lifetime_timer.start() + + # Track spell start time + spell_start_time = Time.get_ticks_msec() / 1000.0 + + # Show explosion init animation first + if explosion_init_sprite: + explosion_init_sprite.visible = true + explosion_init_sprite.frame = 0 + explosion_init_frame = 0 + explosion_init_timer = 0.0 + + # Hide main sprite during explosion init + if sprite: + sprite.visible = false + + # Play sounds + if has_node("SfxInit"): + $SfxInit.play() + if has_node("SfxFire"): + $SfxFire.play() + +func setup(target_position: Vector2, owner_player: Node, damage_value: float = 15.0): + global_position = target_position + player_owner = owner_player + damage = damage_value + +func _process(delta): + damage_timer += delta + animation_timer += delta + explosion_init_timer += delta + + # Handle explosion init animation (4 frames, then hide) + if explosion_init_sprite and explosion_init_sprite.visible: + # Play 4 frames of explosion init (0, 1, 2, 3) at ~30 FPS + if explosion_init_timer >= 0.1: + explosion_init_timer = 0.0 + explosion_init_frame += 1 + if explosion_init_frame < 4: + explosion_init_sprite.frame = explosion_init_frame + else: + # Hide explosion init and show main sprite + explosion_init_sprite.visible = false + if sprite: + sprite.visible = true + _start_sprite_animation() + + # Check if spell is about to expire (last 0.5 seconds) + var time_remaining = lifetime_timer.time_left + var fade_out_start_time = 0.5 # Start fade-out 0.5 seconds before expiration + + if not is_fading_out and time_remaining <= fade_out_start_time: + # Start fade-out sequence + is_fading_out = true + fade_out_frame_index = 0 + animation_timer = 0.0 + # Fade-out frames: 2, 1, 0 + var fade_out_frames = [2, 1, 0] + current_frame = fade_out_frames[0] + if sprite: + sprite.frame = current_frame + + # Animate sprite frames (only if explosion init is done) + if sprite and sprite.visible: + if is_fading_out: + # Play fade-out frames (2, 1, 0) - slower transition (0.15s per frame) + if animation_timer >= 0.15: # Slower for fade-out + animation_timer = 0.0 + var fade_out_frames = [2, 1, 0] + fade_out_frame_index += 1 + if fade_out_frame_index < fade_out_frames.size(): + current_frame = fade_out_frames[fade_out_frame_index] + sprite.frame = current_frame + # If we've played all fade-out frames, keep on frame 0 until expiration + elif fade_out_frame_index >= fade_out_frames.size(): + current_frame = 0 + sprite.frame = 0 + else: + # Normal animation: cycle through frames 4-15 (~30 FPS) + if animation_timer >= 0.03333333: + animation_timer = 0.0 + current_frame += 1 + if current_frame > 15: + current_frame = 4 # Loop back to first burning frame + sprite.frame = current_frame + + # Deal periodic damage to targets in area (only if not fading out and explosion init is done) + if sprite and sprite.visible and not is_fading_out and damage_timer >= damage_interval: + damage_timer = 0.0 + _deal_periodic_damage() + +func _start_sprite_animation(): + # Initialize sprite to first burning frame + if sprite: + sprite.frame = 4 # First frame of burning animation + current_frame = 4 + +func _deal_periodic_damage(): + # Get all bodies in the area + var bodies = hit_area.get_overlapping_bodies() + + for body in bodies: + if body == player_owner: + continue + + # CRITICAL: Only the spell owner (authority) should deal damage + if player_owner and not player_owner.is_multiplayer_authority(): + continue + + # Check if this is the first hit on this target (for initial damage bonus) + var is_first_hit = not (body in first_hit_targets) + if is_first_hit: + first_hit_targets[body] = true + + # Calculate damage - initial damage is much higher for first hit + var final_damage = damage + var int_bonus_damage = 0.0 # Declare outside if block for use in print statements + if is_first_hit: + # Initial damage is multiplied and gets INT bonus + final_damage = damage * initial_damage_multiplier + if player_owner and player_owner.character_stats: + var int_stat = player_owner.character_stats.baseStats.int + player_owner.character_stats.get_pass("int") + int_bonus_damage = int_stat * 0.5 # 0.5 damage per INT point + final_damage += int_bonus_damage + + # Deal damage to players + 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 player_peer_id = body.get_multiplayer_authority() + + # Apply burn debuff with 50% chance + var apply_burn = randf() < 0.5 + + if player_peer_id != 0: + if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id(): + body.rpc_take_damage(final_damage, attacker_pos, false, apply_burn) # Pass burn flag + else: + body.rpc_take_damage.rpc_id(player_peer_id, final_damage, attacker_pos, false, apply_burn) + else: + body.rpc_take_damage.rpc(final_damage, attacker_pos, false, apply_burn) + + if is_first_hit: + var int_bonus = int_bonus_damage if player_owner and player_owner.character_stats else 0.0 + print("Flame spell INITIAL hit player: ", body.name, " for ", final_damage, " damage (base: ", damage, " x ", initial_damage_multiplier, " + INT bonus: ", int_bonus, ")") + else: + print("Flame spell hit player: ", body.name, " for ", final_damage, " damage!") + + # Deal damage to enemies + 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 + + # Apply burn debuff with 50% chance + var apply_burn = randf() < 0.5 + + # Use game_world's _request_enemy_damage for damage, but we need to handle burn separately + # Since _request_enemy_damage doesn't support burn, we'll call rpc_take_damage directly + var enemy_peer_id = body.get_multiplayer_authority() + if enemy_peer_id != 0: + if multiplayer.is_server() and enemy_peer_id == multiplayer.get_unique_id(): + body.rpc_take_damage(final_damage, attacker_pos, false, false, apply_burn) # is_critical=false, is_burn_damage=false, apply_burn_debuff + else: + body.rpc_take_damage.rpc_id(enemy_peer_id, final_damage, attacker_pos, false, false, apply_burn) + else: + body.rpc_take_damage.rpc(final_damage, attacker_pos, false, false, apply_burn) + + if is_first_hit: + var int_bonus = int_bonus_damage if player_owner and player_owner.character_stats else 0.0 + print("Flame spell INITIAL hit enemy: ", body.name, " for ", final_damage, " damage (base: ", damage, " x ", initial_damage_multiplier, " + INT bonus: ", int_bonus, ")") + else: + print("Flame spell hit enemy: ", body.name, " for ", final_damage, " damage!") + +func _on_body_entered(_body): + # Track bodies that enter the area (for periodic damage) + # Don't add to hit_targets here - we want to deal damage multiple times + pass + +func _on_lifetime_expired(): + # Spell expires - fade out and remove + hit_area.set_deferred("monitoring", false) + queue_free() diff --git a/src/scripts/attack_spell_flame.gd.uid b/src/scripts/attack_spell_flame.gd.uid new file mode 100644 index 0000000..0cb15d1 --- /dev/null +++ b/src/scripts/attack_spell_flame.gd.uid @@ -0,0 +1 @@ +uid://pufa280hfhiy diff --git a/src/scripts/debuff_burn.gd b/src/scripts/debuff_burn.gd new file mode 100644 index 0000000..3496962 --- /dev/null +++ b/src/scripts/debuff_burn.gd @@ -0,0 +1,29 @@ +extends Node2D + +# Burn Debuff Visual - Shows burning animation on player/enemy + +@onready var sprite = $Sprite2D + +var animation_timer: float = 0.0 +var current_frame: int = 0 + +func _ready(): + if sprite: + sprite.frame = 0 + current_frame = 0 + animation_timer = 0.0 + # Ensure sprite is visible and positioned correctly + sprite.z_index = 5 # Above player/enemy sprites + sprite.visible = true + print("DebuffBurn: sprite visible: ", sprite.visible, ", z_index: ", sprite.z_index) + +func _process(delta): + if not sprite: + return + + # Animate through frames 0-15 (4x4 grid) at ~10 FPS + animation_timer += delta + if animation_timer >= 0.1: # ~10 FPS + animation_timer = 0.0 + current_frame = (current_frame + 1) % 16 + sprite.frame = current_frame diff --git a/src/scripts/debuff_burn.gd.uid b/src/scripts/debuff_burn.gd.uid new file mode 100644 index 0000000..0ce868e --- /dev/null +++ b/src/scripts/debuff_burn.gd.uid @@ -0,0 +1 @@ +uid://cvtox8ek8u7oq diff --git a/src/scripts/enemy_base.gd b/src/scripts/enemy_base.gd index 40bb28d..dfa34cb 100644 --- a/src/scripts/enemy_base.gd +++ b/src/scripts/enemy_base.gd @@ -21,6 +21,13 @@ var knockback_time: float = 0.0 var knockback_duration: float = 0.3 var knockback_force: float = 125.0 # Scaled down for 1x scale +# 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_damage_per_second: float = 1.0 # 1 HP per second +var burn_debuff_visual: Node2D = null # Visual indicator for burn debuff +var burn_damage_timer: float = 0.0 # Timer for burn damage ticks + # Z-axis for flying enemies var position_z: float = 0.0 var velocity_z: float = 0.0 @@ -95,6 +102,19 @@ func _physics_process(delta): # Only server (authority) runs AI and physics if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority(): # Clients only interpolate position (handled by _sync_position) + # But still update burn visual animation on clients + if burn_debuff_visual and is_instance_valid(burn_debuff_visual): + if burn_debuff_visual is Sprite2D: + var burn_sprite = burn_debuff_visual as Sprite2D + var anim_timer = burn_sprite.get_meta("burn_animation_timer", 0.0) + anim_timer += delta + if anim_timer >= 0.1: # ~10 FPS + anim_timer = 0.0 + var frame = burn_sprite.get_meta("burn_animation_frame", 0) + frame = (frame + 1) % 16 + burn_sprite.frame = frame + burn_sprite.set_meta("burn_animation_frame", frame) + burn_sprite.set_meta("burn_animation_timer", anim_timer) return # Update attack timer @@ -125,6 +145,53 @@ func _physics_process(delta): # Check collisions with interactable objects _check_interactable_object_collision() + # Update burn debuff + if burn_debuff_timer > 0.0: + burn_debuff_timer -= delta + burn_damage_timer += delta + + # Deal burn damage every second (no knockback) + if burn_damage_timer >= 1.0: + burn_damage_timer = 0.0 + # Deal burn damage directly (no knockback, no animation) + if character_stats: + var old_hp = character_stats.hp + character_stats.modify_health(-burn_debuff_damage_per_second) + current_health = character_stats.hp + if character_stats.hp <= 0: + character_stats.no_health.emit() + var actual_damage = old_hp - character_stats.hp + LogManager.log(str(name) + " takes " + str(actual_damage) + " burn damage! Health: " + str(current_health) + "/" + str(character_stats.maxhp), LogManager.CATEGORY_ENEMY) + # Show damage number for burn damage + _show_damage_number(actual_damage, global_position, false, false, false) # Show burn damage number + # Sync burn damage visual to clients + if multiplayer.has_multiplayer_peer() and is_inside_tree(): + var enemy_name = name + var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1 + var game_world = get_tree().get_first_node_in_group("game_world") + if game_world and game_world.has_method("_sync_enemy_damage_visual"): + game_world._rpc_to_ready_peers("_sync_enemy_damage_visual", [enemy_name, enemy_index, actual_damage, global_position, false]) + + # Animate burn visual if it's a sprite (only on authority/server) + if is_multiplayer_authority() and burn_debuff_visual and is_instance_valid(burn_debuff_visual): + if burn_debuff_visual is Sprite2D: + var burn_sprite = burn_debuff_visual as Sprite2D + var anim_timer = burn_sprite.get_meta("burn_animation_timer", 0.0) + anim_timer += delta + if anim_timer >= 0.1: # ~10 FPS + anim_timer = 0.0 + var frame = burn_sprite.get_meta("burn_animation_frame", 0) + frame = (frame + 1) % 16 + burn_sprite.frame = frame + burn_sprite.set_meta("burn_animation_frame", frame) + burn_sprite.set_meta("burn_animation_timer", anim_timer) + + # Remove burn debuff when timer expires + if burn_debuff_timer <= 0.0: + burn_debuff_timer = 0.0 + burn_damage_timer = 0.0 + _remove_burn_debuff() + # Sync position and animation to clients (only server sends) if multiplayer.has_multiplayer_peer() and is_multiplayer_authority(): # Get state value if enemy has a state variable (for bats/slimes) @@ -284,7 +351,7 @@ func _find_nearest_player_to_position(pos: Vector2, max_range: float = 100.0) -> return nearest -func take_damage(amount: float, from_position: Vector2, is_critical: bool = false): +func take_damage(amount: float, from_position: Vector2, is_critical: bool = false, is_burn_damage: bool = false, apply_burn_debuff: bool = false): # Only process damage on server/authority if not is_multiplayer_authority(): return @@ -335,11 +402,17 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals actual_damage = amount LogManager.log(str(name) + " took " + str(amount) + " damage! Health: " + str(current_health) + " (critical: " + str(is_critical) + ")", LogManager.CATEGORY_ENEMY) - # Calculate knockback direction (away from attacker) - var knockback_direction = (global_position - from_position).normalized() - velocity = knockback_direction * knockback_force - is_knocked_back = true - knockback_time = 0.0 + # Only apply knockback if not burn damage + if not is_burn_damage: + # Calculate knockback direction (away from attacker) + var knockback_direction = (global_position - from_position).normalized() + velocity = knockback_direction * knockback_force + is_knocked_back = true + knockback_time = 0.0 + + # Apply burn debuff if requested + if apply_burn_debuff: + _apply_burn_debuff() _on_take_damage(from_position) @@ -379,10 +452,10 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals call_deferred("_notify_doors_enemy_died") @rpc("any_peer", "reliable") -func rpc_take_damage(amount: float, from_position: Vector2, is_critical: bool = false): +func rpc_take_damage(amount: float, from_position: Vector2, is_critical: bool = false, is_burn_damage: bool = false, apply_burn_debuff: bool = false): # RPC version - only process on server/authority if is_multiplayer_authority(): - take_damage(amount, from_position, is_critical) + take_damage(amount, from_position, is_critical, is_burn_damage, apply_burn_debuff) func _show_damage_number(amount: float, from_position: Vector2, is_critical: bool = false, is_miss: bool = false, is_dodged: bool = false): # Show damage number (red/orange for crits, cyan for dodge, gray for miss, using dmg_numbers.png font) above enemy @@ -475,6 +548,91 @@ func _set_animation(_anim_name: String): # (e.g., enemy_humanoid.gd uses player-like animation system) pass +func _apply_burn_debuff(): + # Apply burn debuff to enemy + if burn_debuff_timer > 0.0: + # Already burning - refresh duration + burn_debuff_timer = burn_debuff_duration + burn_damage_timer = 0.0 # Reset damage timer + LogManager.log(str(name) + " burn debuff refreshed", LogManager.CATEGORY_ENEMY) + return + + # Start burn debuff + burn_debuff_timer = burn_debuff_duration + burn_damage_timer = 0.0 + LogManager.log(str(name) + " applied burn debuff (" + str(burn_debuff_duration) + " seconds)", LogManager.CATEGORY_ENEMY) + + # Create visual indicator + _create_burn_debuff_visual() + + # Sync burn debuff to clients + if multiplayer.has_multiplayer_peer() and is_inside_tree(): + var enemy_name = name + var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1 + var game_world = get_tree().get_first_node_in_group("game_world") + if game_world and game_world.has_method("_sync_enemy_burn_debuff"): + game_world._rpc_to_ready_peers("_sync_enemy_burn_debuff", [enemy_name, enemy_index, true]) + +func _create_burn_debuff_visual(): + # Remove existing visual if any + if burn_debuff_visual and is_instance_valid(burn_debuff_visual): + burn_debuff_visual.queue_free() + + # Load burn debuff scene + var burn_debuff_scene = load("res://scenes/debuff_burn.tscn") + if burn_debuff_scene: + burn_debuff_visual = burn_debuff_scene.instantiate() + add_child(burn_debuff_visual) + # Position on enemy (centered) + burn_debuff_visual.position = Vector2(0, 0) + burn_debuff_visual.z_index = 5 # Above enemy sprite + LogManager.log(str(name) + " created burn debuff visual", LogManager.CATEGORY_ENEMY) + else: + # Fallback: create simple sprite if scene doesn't exist + var burn_texture = load("res://assets/gfx/fx/burn.png") + if burn_texture: + var burn_sprite = Sprite2D.new() + burn_sprite.name = "BurnDebuffSprite" + burn_sprite.texture = burn_texture + burn_sprite.hframes = 4 + burn_sprite.vframes = 4 + burn_sprite.frame = 0 + burn_sprite.position = Vector2(0, 0) + burn_sprite.z_index = 5 # Above enemy sprite + burn_sprite.set_meta("burn_animation_frame", 0) + burn_sprite.set_meta("burn_animation_timer", 0.0) + add_child(burn_sprite) + burn_debuff_visual = burn_sprite + +func _remove_burn_debuff(): + # Remove burn debuff visual + if burn_debuff_visual and is_instance_valid(burn_debuff_visual): + burn_debuff_visual.queue_free() + burn_debuff_visual = null + LogManager.log(str(name) + " burn debuff removed", LogManager.CATEGORY_ENEMY) + + # Sync burn debuff removal to clients + if multiplayer.has_multiplayer_peer() and is_inside_tree(): + var enemy_name = name + var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1 + var game_world = get_tree().get_first_node_in_group("game_world") + if game_world and game_world.has_method("_sync_enemy_burn_debuff"): + game_world._rpc_to_ready_peers("_sync_enemy_burn_debuff", [enemy_name, enemy_index, false]) + +func _sync_burn_debuff(apply: bool): + # Client-side sync of burn debuff visual + if apply: + if burn_debuff_timer <= 0.0: + # Only create visual if not already burning + _create_burn_debuff_visual() + burn_debuff_timer = burn_debuff_duration + burn_damage_timer = 0.0 + else: + # Remove visual + _remove_burn_debuff() + burn_debuff_timer = 0.0 + burn_damage_timer = 0.0 + func _die(): if is_dead: return diff --git a/src/scripts/game_world.gd b/src/scripts/game_world.gd index c141936..1461cec 100644 --- a/src/scripts/game_world.gd +++ b/src/scripts/game_world.gd @@ -14,6 +14,7 @@ const REFERENCE_ASPECT: float = 16.0 / 9.0 # Mouse cursor system var cursor_sprite: Sprite2D = null # Free movement cursor (frame 0) var grid_cursor_sprite: Sprite2D = null # Grid-locked cursor (frame 1) +var spell_cursor_sprite: Sprite2D = null # Spell targeting cursor (frame 1, tinted by element) var cursor_layer: CanvasLayer = null const CURSOR_LAYER_Z: int = 2000 # Very high Z index for cursor var use_mouse_control: bool = true # Enable/disable mouse control @@ -1214,6 +1215,35 @@ func _sync_enemy_damage_visual(enemy_name: String, enemy_index: int, damage_amou # This is okay, just log it print("GameWorld: Could not find enemy for damage visual sync: name=", enemy_name, " index=", enemy_index) +@rpc("authority", "reliable") +func _sync_enemy_burn_debuff(enemy_name: String, enemy_index: int, apply: bool): + # Clients receive enemy burn debuff sync from server + # Find the enemy by name or index + if multiplayer.is_server(): + return # Server ignores this (it's the sender) + + var entities_node = get_node_or_null("Entities") + if not entities_node: + return + + # Try to find enemy by name first, then by index + var enemy = null + for child in entities_node.get_children(): + if child.is_in_group("enemy"): + if child.name == enemy_name: + enemy = child + break + elif child.has_meta("enemy_index") and child.get_meta("enemy_index") == enemy_index: + enemy = child + break + + if enemy and enemy.has_method("_sync_burn_debuff"): + # Call the enemy's _sync_burn_debuff method directly (not via RPC) + enemy._sync_burn_debuff(apply) + else: + # Enemy not found - might already be freed or never spawned + print("GameWorld: Could not find enemy for burn debuff sync: name=", enemy_name, " index=", enemy_index) + @rpc("authority", "reliable") func _sync_enemy_attack(enemy_name: String, enemy_index: int, direction: int, attack_dir: Vector2): # Clients receive enemy attack sync from server @@ -1319,7 +1349,7 @@ func _request_enemy_damage(enemy_name: String, enemy_index: int, damage: float, if enemy and enemy.has_method("rpc_take_damage"): # Call the enemy's rpc_take_damage method directly (it will handle authority check) - enemy.rpc_take_damage(damage, attacker_position, is_critical) + enemy.rpc_take_damage(damage, attacker_position, is_critical, false, false) # is_burn_damage=false, apply_burn_debuff=false else: # Enemy not found - might already be freed print("GameWorld: Could not find enemy for damage request: name=", enemy_name, " index=", enemy_index) @@ -1764,6 +1794,17 @@ func _init_mouse_cursor(): grid_cursor_sprite.modulate.a = 0.3 # 30% opacity cursor_layer.add_child(grid_cursor_sprite) + # Create spell targeting cursor sprite (frame 1, will be tinted by element) + spell_cursor_sprite = Sprite2D.new() + spell_cursor_sprite.name = "MouseCursorSpell" + spell_cursor_sprite.texture = cursor_texture + spell_cursor_sprite.hframes = 2 + spell_cursor_sprite.vframes = 1 + spell_cursor_sprite.frame = 1 # Frame 1 = grid-locked + spell_cursor_sprite.modulate.a = 0.5 # 50% opacity + spell_cursor_sprite.visible = false # Hidden by default + cursor_layer.add_child(spell_cursor_sprite) + # Hide system cursor Input.mouse_mode = Input.MOUSE_MODE_HIDDEN @@ -1777,6 +1818,19 @@ func _update_mouse_cursor(delta: float): if not grid_cursor_sprite or not is_instance_valid(grid_cursor_sprite): return + if not spell_cursor_sprite or not is_instance_valid(spell_cursor_sprite): + return + + # Check if player is charging a spell + var is_charging_spell = false + var spell_element = "fire" # Default to fire, can be extended later + if local_players.size() > 0: + var player = local_players[0] + if player and is_instance_valid(player) and player.is_local_player: + if "is_charging_spell" in player: + is_charging_spell = player.is_charging_spell + # TODO: Get spell element from player/equipment when element system is added + # Update pulse time for grid cursor color animation cursor_pulse_time += delta * CURSOR_PULSE_SPEED @@ -1792,6 +1846,7 @@ func _update_mouse_cursor(delta: float): var cursor_scale = camera.zoom.x # Use x zoom (should be same as y) cursor_sprite.scale = Vector2.ONE * cursor_scale grid_cursor_sprite.scale = Vector2.ONE * cursor_scale + spell_cursor_sprite.scale = Vector2.ONE * cursor_scale # Check if we should show grid-locked cursor (when mouse is over game world tiles) var show_grid_cursor = false @@ -1815,16 +1870,49 @@ func _update_mouse_cursor(delta: float): # Update free cursor position (always follows mouse) cursor_sprite.position = mouse_pos - # Update grid cursor visibility and pulsing color - grid_cursor_sprite.visible = show_grid_cursor - if show_grid_cursor: - # Pulse color: oscillate between normal and brighter color - var pulse_value = (sin(cursor_pulse_time) + 1.0) / 2.0 # 0.0 to 1.0 - # Interpolate between normal (1,1,1) and brighter (1.5, 1.2, 1.0) for a warm pulse - var base_color = Color(1.0, 1.0, 1.0) - var pulse_color = Color(1.5, 1.2, 1.0) - grid_cursor_sprite.modulate = base_color.lerp(pulse_color, pulse_value * 0.5) # 50% of the way to pulse color - grid_cursor_sprite.modulate.a = 0.3 # Keep opacity at 30% + # Update spell cursor if charging spell + if is_charging_spell: + # Hide normal grid cursor + grid_cursor_sprite.visible = false + + # Show spell cursor at valid spell target position + var spell_target_pos = _get_valid_spell_target_position(world_pos) + if spell_target_pos != Vector2.ZERO: + spell_cursor_sprite.visible = true + # Convert world position to screen position + var viewport_size = get_viewport().get_visible_rect().size + var viewport_center = viewport_size / 2.0 + var spell_screen_pos = (spell_target_pos - camera.position) * camera.zoom.x + viewport_center + spell_cursor_sprite.position = spell_screen_pos + + # Tint by element + match spell_element: + "fire": + spell_cursor_sprite.modulate = Color(1.0, 0.3, 0.3, 0.5) # Red + "water", "ice": + spell_cursor_sprite.modulate = Color(0.3, 0.5, 1.0, 0.5) # Blue + "electric": + spell_cursor_sprite.modulate = Color(1.0, 1.0, 0.3, 0.5) # Yellow + "earth": + spell_cursor_sprite.modulate = Color(0.3, 1.0, 0.3, 0.5) # Green + "wind": + spell_cursor_sprite.modulate = Color(1.0, 1.0, 1.0, 0.5) # White + _: + spell_cursor_sprite.modulate = Color(1.0, 0.3, 0.3, 0.5) # Default red + else: + spell_cursor_sprite.visible = false + else: + # Show normal grid cursor when not charging spell + spell_cursor_sprite.visible = false + grid_cursor_sprite.visible = show_grid_cursor + if show_grid_cursor: + # Pulse color: oscillate between normal and brighter color + var pulse_value = (sin(cursor_pulse_time) + 1.0) / 2.0 # 0.0 to 1.0 + # Interpolate between normal (1,1,1) and brighter (1.5, 1.2, 1.0) for a warm pulse + var base_color = Color(1.0, 1.0, 1.0) + var pulse_color = Color(1.5, 1.2, 1.0) + grid_cursor_sprite.modulate = base_color.lerp(pulse_color, pulse_value * 0.5) # 50% of the way to pulse color + grid_cursor_sprite.modulate.a = 0.3 # Keep opacity at 30% # Update player facing direction based on mouse position (use world position) # Only update if mouse is inside the window viewport @@ -1837,8 +1925,18 @@ func _update_mouse_cursor(delta: float): if mouse_in_window: # Mouse is in window - use mouse for direction control var player_pos = player.global_position - # Use grid-locked position if available, otherwise use free mouse position - var target_world_pos = grid_locked_world_pos if show_grid_cursor else world_pos + # Use spell cursor position if charging spell, otherwise use grid-locked or free mouse position + var target_world_pos = world_pos + if is_charging_spell and spell_cursor_sprite.visible: + # Use spell cursor position for facing direction + var viewport_size = get_viewport().get_visible_rect().size + var viewport_center = viewport_size / 2.0 + # Convert spell cursor screen position back to world position + var spell_screen_pos = spell_cursor_sprite.position + target_world_pos = (spell_screen_pos - viewport_center) / camera.zoom.x + camera.position + elif show_grid_cursor: + target_world_pos = grid_locked_world_pos + var mouse_direction = (target_world_pos - player_pos).normalized() # Only update facing if mouse is far enough from player @@ -1849,6 +1947,104 @@ func _update_mouse_cursor(delta: float): if "mouse_control_active" in player: player.mouse_control_active = false +func get_grid_locked_cursor_position() -> Vector2: + # Get the grid-locked cursor world position for spell casting + if not dungeon_tilemap_layer: + return Vector2.ZERO + + var _mouse_pos = get_viewport().get_mouse_position() + var world_pos = camera.get_global_mouse_position() + + var tile_pos = dungeon_tilemap_layer.local_to_map(world_pos - dungeon_tilemap_layer.global_position) + var tile_data = dungeon_tilemap_layer.get_cell_source_id(tile_pos) + if tile_data >= 0: # Valid tile + # Return tile center world position + return dungeon_tilemap_layer.map_to_local(tile_pos) + dungeon_tilemap_layer.global_position + + return Vector2.ZERO # No valid grid position + +func _get_valid_spell_target_position(target_world_pos: Vector2) -> Vector2: + # Get valid spell target position (closest valid floor tile, or in front of wall if blocked) + # Returns Vector2.ZERO if no valid position found + if not dungeon_tilemap_layer: + return Vector2.ZERO + + # Get player position for raycast + var player_pos = Vector2.ZERO + if local_players.size() > 0: + var player = local_players[0] + if player and is_instance_valid(player) and player.is_local_player: + player_pos = player.global_position + + # Get tile position from world position + var tile_pos = dungeon_tilemap_layer.local_to_map(target_world_pos - dungeon_tilemap_layer.global_position) + var tile_center = dungeon_tilemap_layer.map_to_local(tile_pos) + dungeon_tilemap_layer.global_position + + # Check if target is on a floor tile and not blocked by wall + if _is_valid_spell_target(tile_center, player_pos): + return tile_center + + # If target is invalid, find closest valid position along the line from player to target + var direction = (target_world_pos - player_pos).normalized() + var max_distance = player_pos.distance_to(target_world_pos) + var step_size = 16.0 # One tile + var steps = int(max_distance / step_size) + 1 + + # Search backwards from target towards player to find first valid position + for i in range(steps + 1): + var check_distance = max_distance - (i * step_size) + if check_distance < 0: + break + + var check_pos = player_pos + (direction * check_distance) + var check_tile_pos = dungeon_tilemap_layer.local_to_map(check_pos - dungeon_tilemap_layer.global_position) + var check_tile_center = dungeon_tilemap_layer.map_to_local(check_tile_pos) + dungeon_tilemap_layer.global_position + + if _is_valid_spell_target(check_tile_center, player_pos): + return check_tile_center + + return Vector2.ZERO # No valid position found + +func _is_valid_spell_target(target_pos: Vector2, player_pos: Vector2) -> bool: + # Check if target position is valid for spell casting (floor tile, not blocked by wall) + if dungeon_data.is_empty() or not dungeon_data.has("grid"): + return false + + # Check if target is on a floor tile + var tile_size = 16 + var tile_x = int(target_pos.x / tile_size) + var tile_y = int(target_pos.y / tile_size) + var grid = dungeon_data.grid + var map_size = dungeon_data.map_size + + # Check bounds + if tile_x < 0 or tile_y < 0 or tile_x >= map_size.x or tile_y >= map_size.y: + return false + + # Check if it's a floor tile (grid value 1) or corridor (grid value 3) + if grid[tile_x][tile_y] != 1 and grid[tile_x][tile_y] != 3: + return false + + # Check if there's a wall between player and target using raycast + var space_state = get_world_2d().direct_space_state + var query = PhysicsRayQueryParameters2D.new() + query.from = player_pos + query.to = target_pos + query.collision_mask = 64 # Layer 7 = walls (bit 6 = 64) + + # Exclude player if we have a reference + if local_players.size() > 0: + var player = local_players[0] + if player and is_instance_valid(player): + query.exclude = [player.get_rid()] + + var result = space_state.intersect_ray(query) + if result: + # Hit something - wall blocks spell casting + return false + + return true + func _init_fog_of_war(): if dungeon_data.is_empty() or not dungeon_data.has("map_size"): return diff --git a/src/scripts/item.gd b/src/scripts/item.gd index 7b2538e..b73ac00 100644 --- a/src/scripts/item.gd +++ b/src/scripts/item.gd @@ -31,7 +31,8 @@ enum WeaponType { DAGGER, STAFF, SPEAR, - MACE + MACE, + SPELLBOOK } var use_function = null diff --git a/src/scripts/item_database.gd b/src/scripts/item_database.gd index de5b7e9..cea90be 100644 --- a/src/scripts/item_database.gd +++ b/src/scripts/item_database.gd @@ -1217,6 +1217,43 @@ static func _load_all_items(): "sell_worth": 36, "rarity": ItemRarity.CONSUMABLE }) + + # SPELLBOOKS (row 11, columns 13-14) + # Sprite 233 = 11 * 20 + 13 + _register_item("tome_of_frostspike", { + "item_name": "Tome of Frostspike", + "description": "A spellbook containing frost magic", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.OFFHAND, + "weapon_type": Item.WeaponType.SPELLBOOK, + "spriteFrame": 11 * 20 + 13, # 233 + "modifiers": {}, + "buy_cost": 100, + "sell_worth": 30, + "weight": 1.5, + "rarity": ItemRarity.UNCOMMON, + "colorReplacements": [ + {"old_color": Color(1.0, 1.0, 1.0), "new_color": Color(0.7, 0.9, 1.0)} # Light blue tint for frost + ] + }) + + # Sprite 234 = 11 * 20 + 14 + _register_item("tome_of_flames", { + "item_name": "Tome of Flames", + "description": "A spellbook containing fire magic", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.OFFHAND, + "weapon_type": Item.WeaponType.SPELLBOOK, + "spriteFrame": 11 * 20 + 14, # 234 + "modifiers": {}, + "buy_cost": 100, + "sell_worth": 30, + "weight": 1.5, + "rarity": ItemRarity.UNCOMMON, + "colorReplacements": [ + {"old_color": Color(1.0, 1.0, 1.0), "new_color": Color(1.0, 0.8, 0.6)} # Warm orange tint for fire + ] + }) # Register an item in the database static func _register_item(item_id: String, item_data: Dictionary): diff --git a/src/scripts/network_manager.gd b/src/scripts/network_manager.gd index 4620dac..6b9f5eb 100644 --- a/src/scripts/network_manager.gd +++ b/src/scripts/network_manager.gd @@ -428,7 +428,7 @@ func _on_server_disconnected(): reconnection_room_id = room_id # Get current level from game_world var game_world = get_tree().get_first_node_in_group("game_world") - if game_world and game_world.has("current_level"): + if game_world and "current_level" in game_world: reconnection_level = game_world.current_level log_print("NetworkManager: Stored reconnection info - room_id: " + reconnection_room_id + ", level: " + str(reconnection_level)) else: diff --git a/src/scripts/player.gd b/src/scripts/player.gd index 9c8957f..6f4c2a6 100644 --- a/src/scripts/player.gd +++ b/src/scripts/player.gd @@ -34,6 +34,8 @@ var grab_distance: float = 0.0 # Distance from player to object when grabbed (fo var can_grab = true var is_lifting = false # True when object is lifted above head var is_pushing = false # True when holding button to push/pull +var is_pulling = false # True when pulling (moving backwards while pushing) +var is_disarming = false # True when disarming a trap var grab_button_pressed_time = 0.0 var just_grabbed_this_frame = false # Prevents immediate release bug - persists across frames var grab_start_time = 0.0 # Time when we first grabbed (to detect quick tap) @@ -67,10 +69,30 @@ var is_attacking: bool = false var is_charging_bow: bool = false # True when holding attack with bow+arrows var bow_charge_start_time: float = 0.0 var bow_charge_percentage: float = 1.0 # 0.5, 0.75, or 1.0 based on charge time +var is_charging_spell: bool = false # True when holding grab with spellbook +var spell_charge_start_time: float = 0.0 +var spell_charge_duration: float = 1.0 # Time to fully charge spell (1 second) +var spell_charge_particles: Node2D = null # Particle system for charging +var spell_charge_particle_timer: float = 0.0 # Timer for spawning particles +var spell_charge_tint: Color = Color(2.0, 0.2, 0.2, 2.0) # Tint color when fully charged +var spell_charge_tint_pulse_time: float = 0.0 # Timer for pulsing tint animation +var spell_charge_tint_pulse_speed: float = 8.0 # Speed of tint pulse animation while charging +var spell_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 spell_incantation_played: bool = false # Track if incantation sound has been played +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_damage_per_second: float = 1.0 # 1 HP per second +var burn_debuff_visual: Node2D = null # Visual indicator for burn debuff +var burn_damage_timer: float = 0.0 # Timer for burn damage ticks +var movement_lock_timer: float = 0.0 # Lock movement when bow is released +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 sword_slash_scene = preload("res://scenes/sword_slash.tscn") # Old rotating version (kept for reference) var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn") # New projectile version 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 flame_spell_scene = preload("res://scenes/attack_spell_flame.tscn") # Flame spell for Tome of Flames var blood_scene = preload("res://scenes/blood_clot.tscn") # Simulated Z-axis for height (when thrown) @@ -198,6 +220,12 @@ const ANIMATIONS = { "CONJURE": { "frames": [19], "frameDurations": [400], + "loop": true, + "nextAnimation": null + }, + "FINISH_SPELL": { + "frames": [21], + "frameDurations": [200], "loop": false, "nextAnimation": "IDLE" }, @@ -248,6 +276,12 @@ const ANIMATIONS = { "frameDurations": [260, 260, 260, 260], "loop": true, "nextAnimation": null + }, + "RUN_PULL": { + "frames": [32, 32, 32, 33], + "frameDurations": [260, 260, 260, 260], + "loop": true, + "nextAnimation": null } } @@ -410,21 +444,25 @@ func _ready(): character_stats.character_changed.emit(character_stats) func _duplicate_sprite_materials(): - # Duplicate shader materials for sprites that use tint parameters + # Duplicate shader materials for ALL sprites that use tint parameters # This prevents shared material state between players - if sprite_hair and sprite_hair.material: - sprite_hair.material = sprite_hair.material.duplicate() - if sprite_facial_hair and sprite_facial_hair.material: - sprite_facial_hair.material = sprite_facial_hair.material.duplicate() - if sprite_eyes and sprite_eyes.material: - sprite_eyes.material = sprite_eyes.material.duplicate() - if sprite_eyelashes and sprite_eyelashes.material: - sprite_eyelashes.material = sprite_eyelashes.material.duplicate() - # Also duplicate materials for equipment sprites that use color replacements + # Each player needs their own material instances to avoid cross-player tint effects + if sprite_body and sprite_body.material: + sprite_body.material = sprite_body.material.duplicate() if sprite_boots and sprite_boots.material: sprite_boots.material = sprite_boots.material.duplicate() if sprite_armour and sprite_armour.material: sprite_armour.material = sprite_armour.material.duplicate() + if sprite_facial_hair and sprite_facial_hair.material: + sprite_facial_hair.material = sprite_facial_hair.material.duplicate() + if sprite_hair and sprite_hair.material: + sprite_hair.material = sprite_hair.material.duplicate() + if sprite_eyes and sprite_eyes.material: + sprite_eyes.material = sprite_eyes.material.duplicate() + if sprite_eyelashes and sprite_eyelashes.material: + sprite_eyelashes.material = sprite_eyelashes.material.duplicate() + if sprite_addons and sprite_addons.material: + sprite_addons.material = sprite_addons.material.duplicate() if sprite_headgear and sprite_headgear.material: sprite_headgear.material = sprite_headgear.material.duplicate() if sprite_weapon and sprite_weapon.material: @@ -636,6 +674,13 @@ func _setup_player_appearance(): character_stats.equipment["offhand"] = starting_arrows print("Elf player ", name, " spawned with short bow and 3 arrows") + # Give Human race starting spellbook (Tome of Flames) + if selected_race == "Human": + var starting_tome = ItemDatabase.create_item("tome_of_flames") + if starting_tome: + character_stats.equipment["offhand"] = starting_tome + print("Human player ", name, " spawned with Tome of Flames") + # Randomize skin (human only for players) # Weighted random: Human1 has highest chance, Human7 has lowest chance # Weights: Human1=7, Human2=6, Human3=5, Human4=4, Human5=3, Human6=2, Human7=1 (total=28) @@ -1443,6 +1488,10 @@ func _update_facing_from_mouse(mouse_direction: Vector2): if is_pushing: return + # Don't update if direction is locked (during attack) + if direction_lock_timer > 0.0: + return + # Mark that mouse control is active (prevents movement keys from overriding attack direction) mouse_control_active = true @@ -1665,6 +1714,95 @@ func _physics_process(delta): is_knocked_back = false knockback_time = 0.0 + # Update movement lock timer (for bow release) + if movement_lock_timer > 0.0: + movement_lock_timer -= delta + if movement_lock_timer <= 0.0: + movement_lock_timer = 0.0 + + # Update direction lock timer (for attacks) + if direction_lock_timer > 0.0: + direction_lock_timer -= delta + if direction_lock_timer <= 0.0: + direction_lock_timer = 0.0 + + # Update spell charging + 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) + + # Update particles (spawn and animate) + spell_charge_particle_timer += delta + _update_spell_charge_particles(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 burn debuff (works on both authority and clients) + if burn_debuff_timer > 0.0: + burn_debuff_timer -= delta + + # Only deal damage on authority (where we have authority) + if is_multiplayer_authority(): + burn_damage_timer += delta + + # Deal burn damage every second (no knockback) + if burn_damage_timer >= 1.0: + burn_damage_timer = 0.0 + # Deal burn damage directly (no knockback, no animation) + if character_stats: + var old_hp = character_stats.hp + character_stats.modify_health(-burn_debuff_damage_per_second) + if character_stats.hp <= 0: + character_stats.no_health.emit() + character_stats.character_changed.emit(character_stats) + var actual_damage = old_hp - character_stats.hp + print(name, " takes ", actual_damage, " burn damage! Health: ", character_stats.hp, "/", character_stats.maxhp) + # Show damage number for burn damage + _show_damage_number(actual_damage, global_position, false, false, false) # Show burn damage number + # Sync burn damage visual to other clients + if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree(): + _rpc_to_ready_peers("_sync_damage", [actual_damage, global_position]) + + # Animate burn visual if it's a sprite (works on both authority and clients) + if burn_debuff_visual and is_instance_valid(burn_debuff_visual): + if burn_debuff_visual is Sprite2D: + var sprite = burn_debuff_visual as Sprite2D + var anim_timer = sprite.get_meta("burn_animation_timer", 0.0) + anim_timer += delta + if anim_timer >= 0.1: # ~10 FPS + anim_timer = 0.0 + var frame = sprite.get_meta("burn_animation_frame", 0) + frame = (frame + 1) % 16 + sprite.frame = frame + sprite.set_meta("burn_animation_frame", frame) + sprite.set_meta("burn_animation_timer", anim_timer) + + # Remove burn debuff when timer expires (works on both authority and clients) + if burn_debuff_timer <= 0.0: + burn_debuff_timer = 0.0 + burn_damage_timer = 0.0 + _remove_burn_debuff() + # Skip input if controls are disabled (e.g., when inventory is open) # But still allow knockback to continue (handled above) var skip_input = controls_disabled @@ -1890,24 +2028,48 @@ func _handle_input(): # Trying to push forward into wall - block movement completely input_vector = Vector2.ZERO + # Detect if pulling (moving backwards while pushing) + is_pulling = false + if is_pushing and held_object and input_vector.length() > 0.1: + var movement_direction = input_vector.normalized() + var push_direction = push_axis.normalized() + var dot_product = movement_direction.dot(push_direction) + if dot_product < -0.1: # Moving opposite to push direction = pulling + is_pulling = true + + # Prevent movement during disarming (unless cancelled or finished) + if is_disarming: + input_vector = Vector2.ZERO + # Track last movement direction if moving if input_vector.length() > 0.1: last_movement_direction = input_vector.normalized() # 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) - if not is_pushing and (not mouse_control_active or input_device != -1): + # Don't update if direction is locked (during attack) + if not is_pushing and (not mouse_control_active or input_device != -1) and direction_lock_timer <= 0.0: facing_direction_vector = input_vector.normalized() + elif direction_lock_timer > 0.0 and locked_facing_direction.length() > 0.0: + # Use locked direction during attack + facing_direction_vector = locked_facing_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 - if not is_pushing and (not mouse_control_active or input_device != -1): + # Don't update if direction is locked (during attack) + if 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 # Update direction and cone light rotation if changed if new_direction != current_direction: current_direction = new_direction _update_cone_light_rotation() + elif direction_lock_timer > 0.0 and locked_facing_direction.length() > 0.0: + # Use locked direction for animations during attack + var new_direction = _get_direction_from_vector(locked_facing_direction) as Direction + if new_direction != current_direction: + current_direction = new_direction + _update_cone_light_rotation() elif is_pushing: # Keep locked direction when pushing if push_direction_locked != current_direction: @@ -1915,15 +2077,32 @@ func _handle_input(): _update_cone_light_rotation() # Set animation based on state - if is_lifting: + if is_charging_spell: + # Use LIFT animation when charging spell and moving + if current_animation != "LIFT" and current_animation != "FINISH_SPELL": + _set_animation("LIFT") + elif is_disarming: + # Use RUN_PULL animation when disarming + _set_animation("RUN_PULL") + elif is_lifting: _set_animation("RUN_HOLD") elif is_pushing: - _set_animation("RUN_PUSH") - elif current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF": + if is_pulling: + _set_animation("RUN_PULL") + else: + _set_animation("RUN_PUSH") + elif current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF" and current_animation != "CONJURE" and current_animation != "LIFT" and current_animation != "FINISH_SPELL": _set_animation("RUN") else: # Idle animations - if is_lifting: + if is_charging_spell: + # Use CONJURE animation when charging spell and standing still + if current_animation != "CONJURE" and current_animation != "FINISH_SPELL": + _set_animation("CONJURE") + elif is_disarming: + # Use RUN_PULL animation when disarming (even when idle) + _set_animation("RUN_PULL") + elif is_lifting: if current_animation != "LIFT" and current_animation != "IDLE_HOLD": _set_animation("IDLE_HOLD") elif is_pushing: @@ -1933,7 +2112,7 @@ func _handle_input(): current_direction = push_direction_locked as Direction _update_cone_light_rotation() else: - if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF": + 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") # Handle drag sound for interactable objects @@ -1958,14 +2137,29 @@ func _handle_input(): was_dragging_last_frame = is_dragging_now # Reduce speed by half when pushing/pulling + # Reduce speed by 50% when charging bow + # Reduce speed by 80% when charging spell (20% speed) # Calculate speed with encumbrance penalty - var base_speed = move_speed * (0.5 if is_pushing else 1.0) + var speed_multiplier = 1.0 + if is_pushing: + speed_multiplier = 0.5 + elif is_charging_bow: + speed_multiplier = 0.5 + elif is_charging_spell: + speed_multiplier = 0.2 # 20% speed (80% reduction) + + var base_speed = move_speed * speed_multiplier var current_speed = base_speed # Apply encumbrance penalty (1/4 speed if over-encumbered) if character_stats and character_stats.is_over_encumbered(): current_speed = base_speed * 0.25 - velocity = input_vector * current_speed + + # Lock movement if movement_lock_timer is active + if movement_lock_timer > 0.0: + velocity = Vector2.ZERO + else: + velocity = input_vector * current_speed func _handle_movement(_delta): # Simple top-down movement is handled by velocity set in _handle_input @@ -2025,25 +2219,168 @@ func _handle_interactions(): print(name, " cancelled bow charge") + # Check for spell casting (with Tome of Flames) + # Handle spell charging (Tome of Flames) + if character_stats and character_stats.equipment.has("offhand"): + var offhand_item = character_stats.equipment["offhand"] + if offhand_item and offhand_item.weapon_type == Item.WeaponType.SPELLBOOK: + if offhand_item.item_name == "Tome of Flames": + # Check for valid target position + var game_world = get_tree().get_first_node_in_group("game_world") + var target_pos = Vector2.ZERO + if game_world and game_world.has_method("get_grid_locked_cursor_position"): + target_pos = game_world.get_grid_locked_cursor_position() + + # Check if there's a grabbable object nearby - prioritize grabbing over spell casting + var nearby_grabbable = null + if grab_area: + 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: + var distance = position.distance_to(body.position) + if distance < grab_range: + nearby_grabbable = body + 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 target_pos != Vector2.ZERO and not nearby_grabbable and not is_lifting and not held_object: + is_charging_spell = true + spell_charge_start_time = Time.get_ticks_msec() / 1000.0 + spell_incantation_played = false # Reset flag when starting new charge + _start_spell_charge_particles() + + # Play spell charging sound (incantation plays when fully charged) + if has_node("SfxSpellCharge"): + $SfxSpellCharge.play() + + # Sync spell charge start to other clients + if multiplayer.has_multiplayer_peer(): + _sync_spell_charge_start.rpc() + + print(name, " started charging spell") + # Skip regular grab handling + just_grabbed_this_frame = false + return + # Release spell + elif grab_just_released and is_charging_spell: + 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: + is_charging_spell = false + spell_incantation_played = false + _stop_spell_charge_particles() + _clear_spell_charge_tint() + + # Stop spell charging sounds + if has_node("SfxSpellCharge"): + $SfxSpellCharge.stop() + if has_node("SfxSpellIncantation"): + $SfxSpellIncantation.stop() + + # Sync spell charge end to other clients + if multiplayer.has_multiplayer_peer(): + _sync_spell_charge_end.rpc() + + print(name, " cancelled spell (released too quickly, need at least 0.2s)") + just_grabbed_this_frame = false + return + + # Check if fully charged (1.0 seconds) + var is_fully_charged = charge_time >= spell_charge_duration + + # 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() + + # Cast spell if fully charged (will find valid position if target is blocked) + if target_pos != Vector2.ZERO and is_fully_charged: + # Cast spell (will find closest valid position if target is blocked) + _cast_flame_spell(target_pos) + # Play FINISH_SPELL animation after casting + _set_animation("FINISH_SPELL") + else: + # Not fully charged or no target - just cancel without casting + 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 + spell_incantation_played = false + _stop_spell_charge_particles() + _clear_spell_charge_tint() # This will restore original tints + + # Stop spell charging sounds + if has_node("SfxSpellCharge"): + $SfxSpellCharge.stop() + if has_node("SfxSpellIncantation"): + $SfxSpellIncantation.stop() + + # Sync spell charge end to other clients + if multiplayer.has_multiplayer_peer(): + _sync_spell_charge_end.rpc() + + print(name, " released spell (charge: ", charge_time, "s, fully charged: ", is_fully_charged, ")") + # Skip regular grab handling + just_grabbed_this_frame = false + return + # Cancel if no target position available or if player starts lifting/grabbing + elif is_charging_spell and (target_pos == Vector2.ZERO or is_lifting or held_object): + is_charging_spell = false + spell_incantation_played = false + _stop_spell_charge_particles() + _clear_spell_charge_tint() + + # Stop spell charging sounds + if has_node("SfxSpellCharge"): + $SfxSpellCharge.stop() + if has_node("SfxSpellIncantation"): + $SfxSpellIncantation.stop() + + # Sync spell charge end to other clients + if multiplayer.has_multiplayer_peer(): + _sync_spell_charge_end.rpc() + + print(name, " spell charge cancelled (no target)") + # Check for trap disarm (Dwarf only) if character_stats and character_stats.race == "Dwarf": var nearby_trap = _get_nearby_disarmable_trap() if nearby_trap: - if grab_just_pressed: + # Check if we're currently disarming this trap + var currently_disarming = (nearby_trap.disarming_player == self) + + if grab_just_pressed and not currently_disarming: # Start disarming + is_disarming = true nearby_trap.disarming_player = self nearby_trap.disarm_progress = 0.0 print(name, " (Dwarf) started disarming trap") - elif grab_just_released: + elif grab_just_released and currently_disarming: # Cancel disarm if released early - if nearby_trap.disarming_player == self: - nearby_trap._cancel_disarm() - print(name, " (Dwarf) cancelled disarm") + is_disarming = false + nearby_trap._cancel_disarm() + print(name, " (Dwarf) cancelled disarm") + elif not currently_disarming: + # Not disarming anymore - reset flag + is_disarming = false + # Don't process regular grab actions if near trap if grab_button_down: # Skip grab handling below just_grabbed_this_frame = false return + else: + # No nearby trap - reset disarming flag + is_disarming = false # Track how long grab button is held if grab_button_down: @@ -2179,6 +2516,8 @@ func _handle_interactions(): # Handle bow charging if has_bow_and_arrows and not is_lifting and not is_pushing: if attack_just_pressed and can_attack and not is_charging_bow: + if !$SfxBuckleBow.playing: + $SfxBuckleBow.play() # Start charging bow is_charging_bow = true bow_charge_start_time = Time.get_ticks_msec() / 1000.0 @@ -2189,6 +2528,7 @@ func _handle_interactions(): print(name, " started charging bow") elif attack_just_released and is_charging_bow: + $SfxBuckleBow.stop() # Calculate charge time var charge_time = (Time.get_ticks_msec() / 1000.0) - bow_charge_start_time @@ -2211,6 +2551,9 @@ func _handle_interactions(): # Release bow and shoot is_charging_bow = false + # Lock movement for 0.15 seconds when bow is released + movement_lock_timer = 0.15 + # Sync bow charge end to other clients if multiplayer.has_multiplayer_peer(): _sync_bow_charge_end.rpc() @@ -2220,6 +2563,7 @@ func _handle_interactions(): else: # Reset charging if conditions changed (no bow/arrows, started lifting/pushing) if is_charging_bow: + $SfxBuckleBow.stop() is_charging_bow = false # Sync bow charge end to other clients @@ -2840,6 +3184,10 @@ func _perform_attack(): else: _set_animation("SWORD") + # Lock facing direction for 0.15 seconds when attack starts + locked_facing_direction = facing_direction_vector.normalized() + direction_lock_timer = 0.15 + # Use full 360-degree facing direction for attack var attack_direction = facing_direction_vector.normalized() @@ -2880,6 +3228,8 @@ func _perform_attack(): # Only spawn arrow if we have arrows if arrows and arrows.quantity > 0: if attack_arrow_scene: + + $SfxBowShoot.play() var arrow_projectile = attack_arrow_scene.instantiate() get_parent().add_child(arrow_projectile) # Spawn arrow 4 pixels in the direction player is looking @@ -2946,6 +3296,418 @@ func _perform_attack(): can_attack = true is_attacking = false +func _cast_flame_spell(target_position: Vector2): + # Cast flame spell at target position (grid-locked cursor) + # If target is blocked, find closest valid position + if not flame_spell_scene: + return + + # Only authority can spawn spells + if not is_multiplayer_authority(): + return + + # Find valid spell target position (closest valid if target is blocked) + 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: + # No valid position found, cancel spell + print(name, " cannot cast spell - no valid target position") + return + + # Calculate damage from character_stats + var spell_damage = 15.0 # Base damage + if character_stats: + spell_damage = character_stats.damage * 0.75 # 75% of normal damage + + # Spawn flame spell at valid target position + var flame_spell = flame_spell_scene.instantiate() + get_parent().add_child(flame_spell) + flame_spell.setup(valid_target_pos, self, spell_damage) + + # Play fire sound from the spell scene + if flame_spell.has_node("SfxFire"): + flame_spell.get_node("SfxFire").play() + + # Sync spell spawn to other clients + if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree(): + _rpc_to_ready_peers("_sync_flame_spell", [valid_target_pos, spell_damage]) + + print(name, " cast flame spell at ", valid_target_pos, " (requested: ", target_position, ")") + +@rpc("any_peer", "reliable") +func _sync_flame_spell(target_position: Vector2, spell_damage: float): + # Client receives flame spell spawn sync + if is_multiplayer_authority(): + return # Authority already spawned it + + if not flame_spell_scene: + return + + # Spawn flame spell on client + var flame_spell = flame_spell_scene.instantiate() + get_parent().add_child(flame_spell) + flame_spell.setup(target_position, self, spell_damage) + + print(name, " (synced) spawned flame spell at ", target_position) + +func _can_cast_spell_at(target_position: Vector2) -> bool: + # Check if spell can be cast at target position + # Must be on floor tile and not blocked by walls + + # Get game world for dungeon data + var game_world = get_tree().get_first_node_in_group("game_world") + if not game_world: + return false + + # Access dungeon_data property + if not "dungeon_data" in game_world: + return false + + var dungeon_data = game_world.dungeon_data + if dungeon_data.is_empty() or not dungeon_data.has("grid"): + return false + + # Check if target is on a floor tile + var tile_size = 16 + var tile_x = int(target_position.x / tile_size) + var tile_y = int(target_position.y / tile_size) + var grid = dungeon_data.grid + var map_size = dungeon_data.map_size + + # Check bounds + if tile_x < 0 or tile_y < 0 or tile_x >= map_size.x or tile_y >= map_size.y: + return false + + # Check if it's a floor tile (grid value 1) or corridor (grid value 3) + # Allow casting on both floor and corridor tiles + var grid_value = grid[tile_x][tile_y] + if grid_value != 1 and grid_value != 3: + return false + + # Check if there's a wall between player and target using raycast + var space_state = get_world_2d().direct_space_state + var query = PhysicsRayQueryParameters2D.new() + query.from = global_position + query.to = target_position + query.collision_mask = 64 # Layer 7 = walls (bit 6 = 64) + query.exclude = [get_rid()] + + var result = space_state.intersect_ray(query) + if result: + # Hit something - check if it's a wall + if result.has("collider"): + # Wall blocks spell casting + return false + + return true + +func _start_spell_charge_particles(): + # Create particle system for spell charging + if spell_charge_particles: + _stop_spell_charge_particles() + + spell_charge_particles = Node2D.new() + spell_charge_particles.name = "SpellChargeParticles" + add_child(spell_charge_particles) + spell_charge_particle_timer = 0.0 + +func _update_spell_charge_particles(charge_progress: float): + # Update particle system based on charge progress + if not spell_charge_particles or not is_instance_valid(spell_charge_particles): + return + + var star_texture = load("res://assets/gfx/fx/magic/red_star.png") + if not star_texture: + return + + # Spawn particles periodically (more frequent as charge increases) + var spawn_interval = 0.15 - (charge_progress * 0.1) # 0.15s to 0.05s interval + if spell_charge_particle_timer >= spawn_interval: + spell_charge_particle_timer = 0.0 + + # Spawn a new particle + var particle = Sprite2D.new() + particle.texture = star_texture + + # Random position at player's feet + var feet_y = 8.0 # Player's feet position + var random_x = randf_range(-4.0, 4.0) + particle.position = Vector2(random_x, feet_y) + + # Scale from 0.2 to 1 based on charge progress + var base_scale = 0.2 + (charge_progress * 0.8) + particle.scale = Vector2.ONE * base_scale + + # Store initial properties for animation + particle.set_meta("initial_scale", base_scale) + particle.set_meta("initial_y", feet_y) + particle.set_meta("lifetime", 0.0) + particle.set_meta("max_lifetime", 0.5 + (charge_progress * 0.5)) # 0.5 to 1.0 seconds + + spell_charge_particles.add_child(particle) + + # Animate existing particles + var particles_to_remove = [] + for child in spell_charge_particles.get_children(): + if child is Sprite2D: + var lifetime = child.get_meta("lifetime", 0.0) + var max_lifetime = child.get_meta("max_lifetime", 1.0) + var initial_scale = child.get_meta("initial_scale", 1.0) + var initial_y = child.get_meta("initial_y", 8.0) + + lifetime += get_process_delta_time() + child.set_meta("lifetime", lifetime) + + # Move upward + var progress = lifetime / max_lifetime + child.position.y = initial_y - (progress * 30.0) # Move up 30 pixels + + # Scale down as it lives + var scale_factor = 1.0 - progress + child.scale = Vector2.ONE * (initial_scale * scale_factor) + + # Fade out + child.modulate.a = 1.0 - progress + + # Remove if expired + if lifetime >= max_lifetime: + particles_to_remove.append(child) + + # Remove expired particles + for particle in particles_to_remove: + particle.queue_free() + +func _stop_spell_charge_particles(): + # Remove particle system + if spell_charge_particles and is_instance_valid(spell_charge_particles): + spell_charge_particles.queue_free() + spell_charge_particles = null + +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: + return + + var sprites = [ + {"sprite": sprite_body, "name": "body"}, + {"sprite": sprite_boots, "name": "boots"}, + {"sprite": sprite_armour, "name": "armour"}, + {"sprite": sprite_facial_hair, "name": "facial_hair"}, + {"sprite": sprite_hair, "name": "hair"}, + {"sprite": sprite_eyes, "name": "eyes"}, + {"sprite": sprite_eyelashes, "name": "eyelashes"}, + {"sprite": sprite_addons, "name": "addons"}, + {"sprite": sprite_headgear, "name": "headgear"} + ] + + # Calculate pulse value (0.0 to 1.0) using sine wave + var pulse_value = (sin(spell_charge_tint_pulse_time) + 1.0) / 2.0 # 0.0 to 1.0 + + for sprite_data in sprites: + var sprite = sprite_data.sprite + var sprite_name = sprite_data.name + + # Double-check sprite belongs to this player instance + if not sprite or not is_instance_valid(sprite): + continue + + # Verify sprite is a child of this player node + if sprite.get_parent() != self and not is_ancestor_of(sprite): + continue + + if sprite.material and sprite.material is ShaderMaterial: + var shader_material = sprite.material as ShaderMaterial + + # Store original tint if not already stored (use unique key per player) + var tint_key = str(get_instance_id()) + "_" + sprite_name + if not tint_key in original_sprite_tints: + # Try to get the current tint parameter value + var original_tint_param = shader_material.get_shader_parameter("tint") + if original_tint_param is Vector4: + # Convert Vector4 to Color + original_sprite_tints[tint_key] = Color(original_tint_param.x, original_tint_param.y, original_tint_param.z, original_tint_param.w) + elif original_tint_param is Color: + # Already a Color + original_sprite_tints[tint_key] = original_tint_param + else: + # Default to white if no tint parameter or invalid + original_sprite_tints[tint_key] = Color.WHITE + + # Get original tint + var original_tint = original_sprite_tints.get(tint_key, Color.WHITE) + + # Calculate fully charged tint (original * spell_charge_tint) + var full_charged_tint = Color( + original_tint.r * spell_charge_tint.r, + original_tint.g * spell_charge_tint.g, + original_tint.b * spell_charge_tint.b, + original_tint.a * spell_charge_tint.a + ) + + # Interpolate between original and fully charged tint based on pulse + var current_tint = original_tint.lerp(full_charged_tint, pulse_value) + + # Apply the pulsing tint + shader_material.set_shader_parameter("tint", Vector4(current_tint.r, current_tint.g, current_tint.b, current_tint.a)) + +func _clear_spell_charge_tint(): + # Restore original tint values for all sprite layers + # IMPORTANT: Only restore THIS player's sprites (not other players) + var sprites = [ + {"sprite": sprite_body, "name": "body"}, + {"sprite": sprite_boots, "name": "boots"}, + {"sprite": sprite_armour, "name": "armour"}, + {"sprite": sprite_facial_hair, "name": "facial_hair"}, + {"sprite": sprite_hair, "name": "hair"}, + {"sprite": sprite_eyes, "name": "eyes"}, + {"sprite": sprite_eyelashes, "name": "eyelashes"}, + {"sprite": sprite_addons, "name": "addons"}, + {"sprite": sprite_headgear, "name": "headgear"} + ] + + var instance_id_str = str(get_instance_id()) + var keys_to_remove = [] + + for sprite_data in sprites: + var sprite = sprite_data.sprite + var sprite_name = sprite_data.name + + # Double-check sprite belongs to this player instance + if not sprite or not is_instance_valid(sprite): + continue + + # Verify sprite is a child of this player node + if sprite.get_parent() != self and not is_ancestor_of(sprite): + continue + + if sprite.material and sprite.material is ShaderMaterial: + var shader_material = sprite.material as ShaderMaterial + + # Use unique key per player + var tint_key = instance_id_str + "_" + sprite_name + + # Restore original tint if we stored it + if tint_key in original_sprite_tints: + var original_tint = original_sprite_tints[tint_key] + shader_material.set_shader_parameter("tint", Vector4(original_tint.r, original_tint.g, original_tint.b, original_tint.a)) + keys_to_remove.append(tint_key) + + # Clear stored tints for this player only + for key in keys_to_remove: + original_sprite_tints.erase(key) + +@rpc("any_peer", "reliable") +func _sync_spell_charge_start(): + # Sync spell charge start to other clients + if not is_multiplayer_authority(): + is_charging_spell = true + spell_charge_start_time = Time.get_ticks_msec() / 1000.0 + _start_spell_charge_particles() + print(name, " (synced) started charging spell") + +@rpc("any_peer", "reliable") +func _sync_spell_charge_end(): + # Sync spell charge end to other clients + if not is_multiplayer_authority(): + is_charging_spell = false + _stop_spell_charge_particles() + _clear_spell_charge_tint() + print(name, " (synced) ended charging spell") + +func _apply_burn_debuff(): + # Apply burn debuff to player + var was_already_burning = burn_debuff_timer > 0.0 + + if was_already_burning: + # Already burning - refresh duration + burn_debuff_timer = burn_debuff_duration + burn_damage_timer = 0.0 # Reset damage timer + print(name, " burn debuff refreshed") + else: + # Start burn debuff + burn_debuff_timer = burn_debuff_duration + burn_damage_timer = 0.0 + print(name, " applied burn debuff (", burn_debuff_duration, " seconds)") + + # Create visual indicator + _create_burn_debuff_visual() + + # Sync burn debuff to other clients (always sync, even on refresh) + if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree(): + _sync_burn_debuff.rpc(true) # true = apply/refresh burn debuff + +func _create_burn_debuff_visual(): + # Remove existing visual if any + if burn_debuff_visual and is_instance_valid(burn_debuff_visual): + burn_debuff_visual.queue_free() + + # Load burn debuff scene + var burn_debuff_scene = load("res://scenes/debuff_burn.tscn") + if burn_debuff_scene: + burn_debuff_visual = burn_debuff_scene.instantiate() + add_child(burn_debuff_visual) + # Position on player (centered) + burn_debuff_visual.position = Vector2(0, 0) + burn_debuff_visual.z_index = 5 # Above player sprites + burn_debuff_visual.visible = true + print(name, " created burn debuff visual (scene), visible: ", burn_debuff_visual.visible, ", z_index: ", burn_debuff_visual.z_index) + else: + # Fallback: create simple sprite if scene doesn't exist + var burn_texture = load("res://assets/gfx/fx/burn.png") + if burn_texture: + var sprite = Sprite2D.new() + sprite.name = "BurnDebuffSprite" + sprite.texture = burn_texture + sprite.hframes = 4 + sprite.vframes = 4 + sprite.frame = 0 + sprite.position = Vector2(0, 0) + sprite.z_index = 5 # Above player sprites + sprite.set_meta("burn_animation_frame", 0) + sprite.set_meta("burn_animation_timer", 0.0) + add_child(sprite) + burn_debuff_visual = sprite + + +func _remove_burn_debuff(): + # Remove burn debuff visual + if burn_debuff_visual and is_instance_valid(burn_debuff_visual): + burn_debuff_visual.queue_free() + burn_debuff_visual = null + print(name, " burn debuff removed") + + # Sync burn debuff removal to other clients + if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree(): + _sync_burn_debuff.rpc(false) # false = remove burn debuff + +@rpc("any_peer", "reliable") +func _sync_burn_debuff(apply: bool): + # Sync burn debuff visual to other clients + # Note: Only the authority deals damage, clients just show the visual + if not is_multiplayer_authority(): + # Client receives burn debuff sync + if apply: + if burn_debuff_timer <= 0.0: + # Only create visual if not already burning + _create_burn_debuff_visual() + print(name, " (client) created burn debuff visual from sync") + # Set timer for visual duration (clients don't deal damage, just show visual) + burn_debuff_timer = burn_debuff_duration + burn_damage_timer = 0.0 + else: + # Remove visual + if burn_debuff_visual and is_instance_valid(burn_debuff_visual): + burn_debuff_visual.queue_free() + burn_debuff_visual = null + burn_debuff_timer = 0.0 + burn_damage_timer = 0.0 + func _update_lifted_object(): if held_object and is_instance_valid(held_object): # Check if object is still being held (prevent updates after release) @@ -3779,12 +4541,12 @@ func set_being_held(held: bool): # RPC function called by attacker to deal damage to this player @rpc("any_peer", "reliable") -func rpc_take_damage(amount: float, attacker_position: Vector2): +func rpc_take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool = false, apply_burn_debuff: bool = false): # Only apply damage on the victim's own client (where they're authority) if is_multiplayer_authority(): - take_damage(amount, attacker_position) + take_damage(amount, attacker_position, is_burn_damage, apply_burn_debuff) -func take_damage(amount: float, attacker_position: Vector2): +func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool = false, apply_burn_debuff: bool = false): # Don't take damage if already dead if is_dead: return @@ -3850,20 +4612,27 @@ func take_damage(amount: float, attacker_position: Vector2): # Play damage animation _set_animation("DAMAGE") - # Calculate direction FROM attacker TO victim - var direction_from_attacker = (global_position - attacker_position).normalized() + # Only apply knockback if not burn damage + if not is_burn_damage: + # Calculate direction FROM attacker TO victim + var direction_from_attacker = (global_position - attacker_position).normalized() + + # Knockback - push player away from attacker + velocity = direction_from_attacker * 250.0 # Scaled down for 1x scale + + # Face the attacker (opposite of knockback direction) + var face_direction = -direction_from_attacker + current_direction = _get_direction_from_vector(face_direction) as Direction + facing_direction_vector = face_direction.normalized() + + # Enable knockback state (prevents player control for a short time) + is_knocked_back = true + knockback_time = 0.0 - # Knockback - push player away from attacker - velocity = direction_from_attacker * 250.0 # Scaled down for 1x scale - - # Face the attacker (opposite of knockback direction) - var face_direction = -direction_from_attacker - current_direction = _get_direction_from_vector(face_direction) as Direction - facing_direction_vector = face_direction.normalized() - - # Enable knockback state (prevents player control for a short time) - is_knocked_back = true - knockback_time = 0.0 + # Apply burn debuff if requested + if apply_burn_debuff: + print(name, " applying burn debuff from take_damage") + _apply_burn_debuff() # Flash red on body sprite if sprite_body: @@ -4283,6 +5052,29 @@ func _sync_race_and_stats(race: String, base_stats: Dictionary): character_stats.equipment["offhand"] = starting_arrows _apply_appearance_to_sprites() print("Elf player ", name, " (remote) received short bow and 3 arrows via race sync") + + "Human": + character_stats.setEars(0) + # Give Human starting spellbook (Tome of Flames) to remote players + # (Authority players get this in _setup_player_appearance) + # Check if equipment is missing - give it regardless of whether race changed + if not is_multiplayer_authority(): + var needs_equipment = false + if character_stats.equipment["offhand"] == null: + needs_equipment = true + else: + # Check if offhand is not Tome of Flames + var offhand = character_stats.equipment["offhand"] + if not offhand or offhand.item_name != "Tome of Flames": + needs_equipment = true + + if needs_equipment: + var starting_tome = ItemDatabase.create_item("tome_of_flames") + if starting_tome: + character_stats.equipment["offhand"] = starting_tome + _apply_appearance_to_sprites() + print("Human player ", name, " (remote) received Tome of Flames via race sync") + _: character_stats.setEars(0) diff --git a/src/scripts/trap.gd b/src/scripts/trap.gd index c27fbf0..718421a 100644 --- a/src/scripts/trap.gd +++ b/src/scripts/trap.gd @@ -220,6 +220,8 @@ func _update_disarm_ui() -> void: disarm_label.text = "DISARM (" + str(progress_percent) + "%)" func _cancel_disarm() -> void: + if disarming_player and disarming_player.has_method("set") and "is_disarming" in disarming_player: + disarming_player.is_disarming = false disarming_player = null disarm_progress = 0.0 # Stop disarming sound @@ -231,6 +233,8 @@ func _cancel_disarm() -> void: func _complete_disarm() -> void: # Trap successfully disarmed! is_disarmed = true + if disarming_player and disarming_player.has_method("set") and "is_disarming" in disarming_player: + disarming_player.is_disarming = false disarming_player = null disarm_progress = 0.0