added so player can cast Flame spell

This commit is contained in:
2026-01-24 01:03:48 +01:00
parent 9b4135b175
commit 262ce5b351
70 changed files with 2352 additions and 65 deletions

Binary file not shown.

View File

@@ -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

Binary file not shown.

View File

@@ -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

Binary file not shown.

View File

@@ -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

Binary file not shown.

View File

@@ -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

Binary file not shown.

View File

@@ -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

Binary file not shown.

View File

@@ -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

Binary file not shown.

View File

@@ -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

Binary file not shown.

View File

@@ -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

Binary file not shown.

View File

@@ -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

Binary file not shown.

View File

@@ -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

Binary file not shown.

View File

@@ -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

View File

@@ -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

Binary file not shown.

View File

@@ -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

View File

@@ -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

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

BIN
src/assets/gfx/fx/burn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -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"

View File

@@ -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

View File

@@ -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"

View File

@@ -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()

View File

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

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -31,7 +31,8 @@ enum WeaponType {
DAGGER,
STAFF,
SPEAR,
MACE
MACE,
SPELLBOOK
}
var use_function = null

View File

@@ -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):

View File

@@ -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:

File diff suppressed because it is too large Load Diff

View File

@@ -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