diff --git a/src/assets/audio/sfx/shield/activate_shield.wav b/src/assets/audio/sfx/shield/activate_shield.wav new file mode 100644 index 0000000..1f0f8fb Binary files /dev/null and b/src/assets/audio/sfx/shield/activate_shield.wav differ diff --git a/src/assets/audio/sfx/shield/activate_shield.wav.import b/src/assets/audio/sfx/shield/activate_shield.wav.import new file mode 100644 index 0000000..7492cbc --- /dev/null +++ b/src/assets/audio/sfx/shield/activate_shield.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://c10ju1f6d4ed3" +path="res://.godot/imported/activate_shield.wav-4315e73a1b91a2001e5cfc56e2fd9c74.sample" + +[deps] + +source_file="res://assets/audio/sfx/shield/activate_shield.wav" +dest_files=["res://.godot/imported/activate_shield.wav-4315e73a1b91a2001e5cfc56e2fd9c74.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/src/assets/audio/sfx/shield/denied_activate_Shield.wav b/src/assets/audio/sfx/shield/denied_activate_Shield.wav new file mode 100644 index 0000000..e6a6b41 Binary files /dev/null and b/src/assets/audio/sfx/shield/denied_activate_Shield.wav differ diff --git a/src/assets/audio/sfx/shield/denied_activate_Shield.wav.import b/src/assets/audio/sfx/shield/denied_activate_Shield.wav.import new file mode 100644 index 0000000..922aac0 --- /dev/null +++ b/src/assets/audio/sfx/shield/denied_activate_Shield.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://nni5cgx3obpk" +path="res://.godot/imported/denied_activate_Shield.wav-0a9618c73f1763b87655b2b1207a185f.sample" + +[deps] + +source_file="res://assets/audio/sfx/shield/denied_activate_Shield.wav" +dest_files=["res://.godot/imported/denied_activate_Shield.wav-0a9618c73f1763b87655b2b1207a185f.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/src/assets/audio/sfx/shield/denied_activate_Shield2.wav b/src/assets/audio/sfx/shield/denied_activate_Shield2.wav new file mode 100644 index 0000000..e87721d Binary files /dev/null and b/src/assets/audio/sfx/shield/denied_activate_Shield2.wav differ diff --git a/src/assets/audio/sfx/shield/denied_activate_Shield2.wav.import b/src/assets/audio/sfx/shield/denied_activate_Shield2.wav.import new file mode 100644 index 0000000..c82f57b --- /dev/null +++ b/src/assets/audio/sfx/shield/denied_activate_Shield2.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dvq72502qa46f" +path="res://.godot/imported/denied_activate_Shield2.wav-c8d26214a783643d1704131f67c77cb5.sample" + +[deps] + +source_file="res://assets/audio/sfx/shield/denied_activate_Shield2.wav" +dest_files=["res://.godot/imported/denied_activate_Shield2.wav-c8d26214a783643d1704131f67c77cb5.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/src/assets/audio/sfx/shield/domedagsljud.wav b/src/assets/audio/sfx/shield/domedagsljud.wav new file mode 100644 index 0000000..75cea79 Binary files /dev/null and b/src/assets/audio/sfx/shield/domedagsljud.wav differ diff --git a/src/assets/audio/sfx/shield/domedagsljud.wav.import b/src/assets/audio/sfx/shield/domedagsljud.wav.import new file mode 100644 index 0000000..c5461b3 --- /dev/null +++ b/src/assets/audio/sfx/shield/domedagsljud.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dbdem0g2ivn7k" +path="res://.godot/imported/domedagsljud.wav-b4f6d0c4c012380dcda8492ee0223f5e.sample" + +[deps] + +source_file="res://assets/audio/sfx/shield/domedagsljud.wav" +dest_files=["res://.godot/imported/domedagsljud.wav-b4f6d0c4c012380dcda8492ee0223f5e.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/src/assets/audio/sfx/shield/pickup_weapon.wav b/src/assets/audio/sfx/shield/pickup_weapon.wav new file mode 100644 index 0000000..7c433c5 Binary files /dev/null and b/src/assets/audio/sfx/shield/pickup_weapon.wav differ diff --git a/src/assets/audio/sfx/shield/pickup_weapon.wav.import b/src/assets/audio/sfx/shield/pickup_weapon.wav.import new file mode 100644 index 0000000..8620dd9 --- /dev/null +++ b/src/assets/audio/sfx/shield/pickup_weapon.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://vijnmoeqpspl" +path="res://.godot/imported/pickup_weapon.wav-5daceda79276fbce185b7eae0942eb9f.sample" + +[deps] + +source_file="res://assets/audio/sfx/shield/pickup_weapon.wav" +dest_files=["res://.godot/imported/pickup_weapon.wav-5daceda79276fbce185b7eae0942eb9f.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/src/assets/audio/sfx/shield/shield.wav b/src/assets/audio/sfx/shield/shield.wav new file mode 100644 index 0000000..6a60e45 Binary files /dev/null and b/src/assets/audio/sfx/shield/shield.wav differ diff --git a/src/assets/audio/sfx/shield/shield.wav.import b/src/assets/audio/sfx/shield/shield.wav.import new file mode 100644 index 0000000..d443cf6 --- /dev/null +++ b/src/assets/audio/sfx/shield/shield.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://ly1euk0v3jxy" +path="res://.godot/imported/shield.wav-9f6d52c26ab431d241317a8914dbd400.sample" + +[deps] + +source_file="res://assets/audio/sfx/shield/shield.wav" +dest_files=["res://.godot/imported/shield.wav-9f6d52c26ab431d241317a8914dbd400.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/src/assets/audio/sfx/shield/shield1.wav b/src/assets/audio/sfx/shield/shield1.wav new file mode 100644 index 0000000..eafa071 Binary files /dev/null and b/src/assets/audio/sfx/shield/shield1.wav differ diff --git a/src/assets/audio/sfx/shield/shield1.wav.import b/src/assets/audio/sfx/shield/shield1.wav.import new file mode 100644 index 0000000..1f398b3 --- /dev/null +++ b/src/assets/audio/sfx/shield/shield1.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://c4lh535yj010h" +path="res://.godot/imported/shield1.wav-612ea507d8afe53393d2a3524371577b.sample" + +[deps] + +source_file="res://assets/audio/sfx/shield/shield1.wav" +dest_files=["res://.godot/imported/shield1.wav-612ea507d8afe53393d2a3524371577b.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/src/assets/audio/sfx/shield/shield2.wav b/src/assets/audio/sfx/shield/shield2.wav new file mode 100644 index 0000000..67d606c Binary files /dev/null and b/src/assets/audio/sfx/shield/shield2.wav differ diff --git a/src/assets/audio/sfx/shield/shield2.wav.import b/src/assets/audio/sfx/shield/shield2.wav.import new file mode 100644 index 0000000..ff67ea2 --- /dev/null +++ b/src/assets/audio/sfx/shield/shield2.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://ch3p57i7fvd1v" +path="res://.godot/imported/shield2.wav-2b1db8dc996b44c797a97c653e118567.sample" + +[deps] + +source_file="res://assets/audio/sfx/shield/shield2.wav" +dest_files=["res://.godot/imported/shield2.wav-2b1db8dc996b44c797a97c653e118567.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/src/assets/audio/sfx/shield/shield3.wav b/src/assets/audio/sfx/shield/shield3.wav new file mode 100644 index 0000000..6ef0142 Binary files /dev/null and b/src/assets/audio/sfx/shield/shield3.wav differ diff --git a/src/assets/audio/sfx/shield/shield3.wav.import b/src/assets/audio/sfx/shield/shield3.wav.import new file mode 100644 index 0000000..51194eb --- /dev/null +++ b/src/assets/audio/sfx/shield/shield3.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://t0sg2rxlfech" +path="res://.godot/imported/shield3.wav-bb74330e5d55af4d6399c4c2b21af2d8.sample" + +[deps] + +source_file="res://assets/audio/sfx/shield/shield3.wav" +dest_files=["res://.godot/imported/shield3.wav-bb74330e5d55af4d6399c4c2b21af2d8.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/src/assets/audio/sfx/shield/stuff.mp3 b/src/assets/audio/sfx/shield/stuff.mp3 new file mode 100644 index 0000000..9667888 Binary files /dev/null and b/src/assets/audio/sfx/shield/stuff.mp3 differ diff --git a/src/assets/audio/sfx/shield/stuff.mp3.import b/src/assets/audio/sfx/shield/stuff.mp3.import new file mode 100644 index 0000000..38bc97b --- /dev/null +++ b/src/assets/audio/sfx/shield/stuff.mp3.import @@ -0,0 +1,19 @@ +[remap] + +importer="mp3" +type="AudioStreamMP3" +uid="uid://b7kciskx5mn6y" +path="res://.godot/imported/stuff.mp3-7d25ebd8850ef3ff7524465aa1874914.mp3str" + +[deps] + +source_file="res://assets/audio/sfx/shield/stuff.mp3" +dest_files=["res://.godot/imported/stuff.mp3-7d25ebd8850ef3ff7524465aa1874914.mp3str"] + +[params] + +loop=false +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/StalkerHoodBlack.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/StalkerHoodBlack.png deleted file mode 100644 index b688b6d..0000000 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/StalkerHoodBlack.png and /dev/null differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/ThiefBandanaGreen.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/ThiefBandanaGreen.png deleted file mode 100644 index 8273f4c..0000000 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/ThiefBandanaGreen.png and /dev/null differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/AssasinBandanaBlack.png.import b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/AssasinBandanaBlack.png.import similarity index 100% rename from src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/AssasinBandanaBlack.png.import rename to src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/AssasinBandanaBlack.png.import diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/AssasinBandanaBlack.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/AssassinBandanaBlack.png similarity index 100% rename from src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/AssasinBandanaBlack.png rename to src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/AssassinBandanaBlack.png diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierGoldHelmBlue.png.import b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/AssassinBandanaBlack.png.import similarity index 74% rename from src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierGoldHelmBlue.png.import rename to src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/AssassinBandanaBlack.png.import index c074d51..207a7bc 100644 --- a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierGoldHelmBlue.png.import +++ b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/AssassinBandanaBlack.png.import @@ -2,16 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://dyq0jwt648fl2" -path="res://.godot/imported/SoldierGoldHelmBlue.png-2bb02261a7badbc0cd21e2f4e137c1ff.ctex" +uid="uid://do5jdbxjiwen" +path="res://.godot/imported/AssassinBandanaBlack.png-f7bcc2f961d450d22f42560829f068d0.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierGoldHelmBlue.png" -dest_files=["res://.godot/imported/SoldierGoldHelmBlue.png-2bb02261a7badbc0cd21e2f4e137c1ff.ctex"] +source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/AssassinBandanaBlack.png" +dest_files=["res://.godot/imported/AssassinBandanaBlack.png-f7bcc2f961d450d22f42560829f068d0.ctex"] [params] diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/StalkerHoodBlack.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/StalkerHoodBlack.png new file mode 100644 index 0000000..2cf2183 Binary files /dev/null and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/StalkerHoodBlack.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/StalkerHoodBlack.png.import b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/StalkerHoodBlack.png.import similarity index 78% rename from src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/StalkerHoodBlack.png.import rename to src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/StalkerHoodBlack.png.import index f3e97e5..0297e85 100644 --- a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/StalkerHoodBlack.png.import +++ b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/StalkerHoodBlack.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://dswnht88lm3ks" -path="res://.godot/imported/StalkerHoodBlack.png-bc5c1af5d15f82e95db19b23af7b861f.ctex" +path="res://.godot/imported/StalkerHoodBlack.png-db060deca6decafc4dc183500fb85065.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/StalkerHoodBlack.png" -dest_files=["res://.godot/imported/StalkerHoodBlack.png-bc5c1af5d15f82e95db19b23af7b861f.ctex"] +source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/StalkerHoodBlack.png" +dest_files=["res://.godot/imported/StalkerHoodBlack.png-db060deca6decafc4dc183500fb85065.ctex"] [params] diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/ThiefBandanaGreen.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/ThiefBandanaGreen.png new file mode 100644 index 0000000..17b42c9 Binary files /dev/null and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/ThiefBandanaGreen.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/ThiefBandanaGreen.png.import b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/ThiefBandanaGreen.png.import similarity index 77% rename from src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/ThiefBandanaGreen.png.import rename to src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/ThiefBandanaGreen.png.import index 6a635d5..bd8cdf3 100644 --- a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/ThiefBandanaGreen.png.import +++ b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/ThiefBandanaGreen.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://dt0otw6d11pa7" -path="res://.godot/imported/ThiefBandanaGreen.png-244270b26eabdab0125f17bea0cb15e5.ctex" +path="res://.godot/imported/ThiefBandanaGreen.png-4d39120b40e65fba58f9d71c1dde48e1.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assasin/ThiefBandanaGreen.png" -dest_files=["res://.godot/imported/ThiefBandanaGreen.png-244270b26eabdab0125f17bea0cb15e5.ctex"] +source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/ThiefBandanaGreen.png" +dest_files=["res://.godot/imported/ThiefBandanaGreen.png-4d39120b40e65fba58f9d71c1dde48e1.ctex"] [params] diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/EsperHatBlue.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/EsperHatBlue.png index 115b95b..053f5a5 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/EsperHatBlue.png and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/EsperHatBlue.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/HighMageHatCyan.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/HighMageHatCyan.png index cc88db3..19d1276 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/HighMageHatCyan.png and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/HighMageHatCyan.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/MageHatRed.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/MageHatRed.png index d9c6d62..a378c87 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/MageHatRed.png and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/MageHatRed.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/SorcererHoodCyan.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/SorcererHoodCyan.png index e7dbe78..fbdb018 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/SorcererHoodCyan.png and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/SorcererHoodCyan.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/DarkKnightHelm.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/DarkKnightHelm.png index fb9bc8e..702f39e 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/DarkKnightHelm.png and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/DarkKnightHelm.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierBronzeHelmRed.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierBronzeHelmRed.png deleted file mode 100644 index 1837e78..0000000 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierBronzeHelmRed.png and /dev/null differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierGoldHelmBlue.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierGoldHelmBlue.png deleted file mode 100644 index a776a5f..0000000 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierGoldHelmBlue.png and /dev/null differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/ArcherHatCyan.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/ArcherHatCyan.png index af7a912..05d6842 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/ArcherHatCyan.png and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/ArcherHatCyan.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/ArcherHatCyan.png.import b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/ArcherHatCyan.png.import index 3bba0cb..e6d3e3e 100644 --- a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/ArcherHatCyan.png.import +++ b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/ArcherHatCyan.png.import @@ -2,7 +2,7 @@ importer="texture" type="CompressedTexture2D" -uid="uid://bkaam8riwwft4" +uid="uid://yumebu3noyau" path="res://.godot/imported/ArcherHatCyan.png-359731c5c2a1a0c4d2a4e5623e2151c2.ctex" metadata={ "vram_texture": false diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/HunterHatRed.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/HunterHatRed.png index 8fe3348..80b358b 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/HunterHatRed.png and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/HunterHatRed.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RangerHatGreen.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RangerHatGreen.png deleted file mode 100644 index ec4007d..0000000 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RangerHatGreen.png and /dev/null differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RogueHatGreen.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RogueHatGreen.png index dad1f1d..dfb244c 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RogueHatGreen.png and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RogueHatGreen.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/French/MusketeerHatPurple.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/French/MusketeerHatPurple.png index 75b6fa5..f9aa578 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/French/MusketeerHatPurple.png and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/French/MusketeerHatPurple.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/DaimyoHelm.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/DaimyoHelm.png index 429c2c2..528d4b8 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/DaimyoHelm.png and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/DaimyoHelm.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/NinjaBandanaBlue.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/NinjaBandanaBlue.png index 87c5c7f..3d277e4 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/NinjaBandanaBlue.png and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/NinjaBandanaBlue.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/RoninStrawhatBlue.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/RoninStrawhatBlue.png index c08c1c9..caba88e 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/RoninStrawhatBlue.png and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/RoninStrawhatBlue.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/SamuraiHelm.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/SamuraiHelm.png index 7f7cc38..12a1bc9 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/SamuraiHelm.png and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/SamuraiHelm.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/ShogunHelmPurple.png b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/ShogunHelmPurple.png index 873bd15..b311a7f 100644 Binary files a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/ShogunHelmPurple.png and b/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/ShogunHelmPurple.png differ diff --git a/src/assets/gfx/Puny-Characters/ShieldOverlayer.png b/src/assets/gfx/Puny-Characters/ShieldOverlayer.png new file mode 100644 index 0000000..4727afe Binary files /dev/null and b/src/assets/gfx/Puny-Characters/ShieldOverlayer.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RangerHatGreen.png.import b/src/assets/gfx/Puny-Characters/ShieldOverlayer.png.import similarity index 69% rename from src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RangerHatGreen.png.import rename to src/assets/gfx/Puny-Characters/ShieldOverlayer.png.import index f54a3e5..538c3b6 100644 --- a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RangerHatGreen.png.import +++ b/src/assets/gfx/Puny-Characters/ShieldOverlayer.png.import @@ -2,16 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://dqwwsajau10n7" -path="res://.godot/imported/RangerHatGreen.png-b7941d36e5dec52b1b4b8ba30452afa7.ctex" +uid="uid://bkca7nmt4du5e" +path="res://.godot/imported/ShieldOverlayer.png-801252b933f8a048302e86bf4a907ac9.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RangerHatGreen.png" -dest_files=["res://.godot/imported/RangerHatGreen.png-b7941d36e5dec52b1b4b8ba30452afa7.ctex"] +source_file="res://assets/gfx/Puny-Characters/ShieldOverlayer.png" +dest_files=["res://.godot/imported/ShieldOverlayer.png-801252b933f8a048302e86bf4a907ac9.ctex"] [params] diff --git a/src/assets/gfx/Puny-Characters/ShieldOverlayerHolding.png b/src/assets/gfx/Puny-Characters/ShieldOverlayerHolding.png new file mode 100644 index 0000000..9326a58 Binary files /dev/null and b/src/assets/gfx/Puny-Characters/ShieldOverlayerHolding.png differ diff --git a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierBronzeHelmRed.png.import b/src/assets/gfx/Puny-Characters/ShieldOverlayerHolding.png.import similarity index 68% rename from src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierBronzeHelmRed.png.import rename to src/assets/gfx/Puny-Characters/ShieldOverlayerHolding.png.import index 2663cf4..53f4fae 100644 --- a/src/assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierBronzeHelmRed.png.import +++ b/src/assets/gfx/Puny-Characters/ShieldOverlayerHolding.png.import @@ -2,16 +2,16 @@ importer="texture" type="CompressedTexture2D" -uid="uid://bxgu54fmyteul" -path="res://.godot/imported/SoldierBronzeHelmRed.png-c2f0c80930493f16114c541823eaecff.ctex" +uid="uid://bpxxpdpow5qyl" +path="res://.godot/imported/ShieldOverlayerHolding.png-973cd2b6c2eba0416ade721b4b48b9ac.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierBronzeHelmRed.png" -dest_files=["res://.godot/imported/SoldierBronzeHelmRed.png-c2f0c80930493f16114c541823eaecff.ctex"] +source_file="res://assets/gfx/Puny-Characters/ShieldOverlayerHolding.png" +dest_files=["res://.godot/imported/ShieldOverlayerHolding.png-973cd2b6c2eba0416ade721b4b48b9ac.ctex"] [params] diff --git a/src/assets/gfx/Puny-Characters/shield.wav b/src/assets/gfx/Puny-Characters/shield.wav new file mode 100644 index 0000000..6a60e45 Binary files /dev/null and b/src/assets/gfx/Puny-Characters/shield.wav differ diff --git a/src/assets/gfx/Puny-Characters/shield.wav.import b/src/assets/gfx/Puny-Characters/shield.wav.import new file mode 100644 index 0000000..900e807 --- /dev/null +++ b/src/assets/gfx/Puny-Characters/shield.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dm0wpd6qub4i1" +path="res://.godot/imported/shield.wav-6f115adeaf28ccc3ea4a9f30145b8c34.sample" + +[deps] + +source_file="res://assets/gfx/Puny-Characters/shield.wav" +dest_files=["res://.godot/imported/shield.wav-6f115adeaf28ccc3ea4a9f30145b8c34.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/src/assets/gfx/Puny-Characters/shield_init.png b/src/assets/gfx/Puny-Characters/shield_init.png new file mode 100644 index 0000000..912ff30 Binary files /dev/null and b/src/assets/gfx/Puny-Characters/shield_init.png differ diff --git a/src/assets/gfx/Puny-Characters/shield_init.png.import b/src/assets/gfx/Puny-Characters/shield_init.png.import new file mode 100644 index 0000000..e054bf8 --- /dev/null +++ b/src/assets/gfx/Puny-Characters/shield_init.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cdqym40of00i" +path="res://.godot/imported/shield_init.png-c8f99f8a12f1cd02f5f0f86f9e835167.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/gfx/Puny-Characters/shield_init.png" +dest_files=["res://.godot/imported/shield_init.png-c8f99f8a12f1cd02f5f0f86f9e835167.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/assets/gfx/RPG DUNGEON VOL 3.tres b/src/assets/gfx/RPG DUNGEON VOL 3.tres index 129890b..d0c914e 100644 --- a/src/assets/gfx/RPG DUNGEON VOL 3.tres +++ b/src/assets/gfx/RPG DUNGEON VOL 3.tres @@ -205,6 +205,7 @@ separation = Vector2i(1, 1) 10:3/0 = 0 11:3/0 = 0 12:3/0 = 0 +12:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 0:4/0 = 0 0:4/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_hu0mk") 0:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) @@ -232,6 +233,7 @@ separation = Vector2i(1, 1) 11:4/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_okmkx") 11:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 12:4/0 = 0 +12:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 0:5/0 = 0 1:5/0 = 0 1:5/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) @@ -249,6 +251,7 @@ separation = Vector2i(1, 1) 10:5/0 = 0 11:5/0 = 0 12:5/0 = 0 +12:5/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 0:6/0 = 0 0:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 1:6/0 = 0 @@ -493,6 +496,27 @@ separation = Vector2i(1, 1) 11:15/0 = 0 12:15/0 = 0 13:15/0 = 0 +13:3/0 = 0 +13:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +14:3/0 = 0 +14:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +13:4/0 = 0 +13:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +13:5/0 = 0 +13:5/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +14:5/0 = 0 +14:5/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +15:5/0 = 0 +16:5/0 = 0 +16:5/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +17:5/0 = 0 +17:5/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +15:4/0 = 0 +14:4/0 = 0 +14:2/0 = 0 +14:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) +14:1/0 = 0 +14:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) [resource] occlusion_layer_0/light_mask = 1 diff --git a/src/assets/gfx/pickups/items_n_shit.png b/src/assets/gfx/pickups/items_n_shit.png index f368042..3cfe7f3 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/assets/gfx/yukon-salve.png b/src/assets/gfx/yukon-salve.png new file mode 100644 index 0000000..f268e19 Binary files /dev/null and b/src/assets/gfx/yukon-salve.png differ diff --git a/src/assets/gfx/yukon-salve.png.import b/src/assets/gfx/yukon-salve.png.import new file mode 100644 index 0000000..19ac7bc --- /dev/null +++ b/src/assets/gfx/yukon-salve.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dty1vobwvjhkw" +path="res://.godot/imported/yukon-salve.png-4bc768fa22c129f9ab6b79c2df853b34.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/gfx/yukon-salve.png" +dest_files=["res://.godot/imported/yukon-salve.png-4bc768fa22c129f9ab6b79c2df853b34.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/src/scenes/attack_spell_frostspike.tscn b/src/scenes/attack_spell_frostspike.tscn new file mode 100644 index 0000000..77158cf --- /dev/null +++ b/src/scenes/attack_spell_frostspike.tscn @@ -0,0 +1,30 @@ +[gd_scene format=3 uid="uid://d1u8p5rop4vye"] + +[ext_resource type="Script" path="res://scripts/attack_spell_frostspike.gd" id="1_script"] +[ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="2_tex"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_frost"] +size = Vector2(16, 16) + +[node name="FrostspikeSpell" type="Node2D"] +z_index = 4 +script = ExtResource("1_script") + +[node name="Sprite2D" type="Sprite2D" parent="."] +texture = ExtResource("2_tex") +hframes = 105 +vframes = 79 +frame = 4413 + +[node name="SpikeLight" type="PointLight2D" parent="."] +color = Color(0.35, 0.6, 1, 1) +energy = 1.0 +texture_scale = 0.5 +enabled = true + +[node name="Area2D" type="Area2D" parent="."] +collision_layer = 4 +collision_mask = 3 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"] +shape = SubResource("RectangleShape2D_frost") diff --git a/src/scenes/boss_room_test.tscn b/src/scenes/boss_room_test.tscn index ded98b1..f1ebf23 100644 --- a/src/scenes/boss_room_test.tscn +++ b/src/scenes/boss_room_test.tscn @@ -31,6 +31,9 @@ z_index = -2 tile_map_data = PackedByteArray("AAD//wAAAAAAAAAAAAAAAAAAAAAEAAQAAAAAAAEAAAAAAAEAAAAAAAIAAAAAAAIAAAAAAAMAAAAAAAIAAAAAAAQAAAAAAAIAAAABAAAAAAABAAAAAAABAAEAAAABAAEAAAABAAIAAAABAAIAAAABAAMAAAABAAIAAAABAAQAAAABAAIAAAACAAAAAAACAAAAAAACAAEAAAACAAEAAAACAAIAAAAOAAgAAAACAAMAAAAOAAgAAAACAAQAAAAOAAgAAAADAAAAAAACAAAAAAADAAEAAAACAAEAAAADAAIAAAAOAAgAAAADAAMAAAAOAAgAAAADAAQAAAAOAAgAAAAEAAAAAAACAAAAAAAEAAEAAAACAAEAAAAEAAIAAAAOAAgAAAAEAAMAAAAOAAgAAAAEAAQAAAAOAAgAAAAFAAAAAAACAAAAAAAFAAEAAAACAAEAAAAGAAAAAAACAAAAAAAGAAEAAAACAAEAAAAHAAAAAAACAAAAAAAHAAEAAAACAAEAAAAIAAAAAAACAAAAAAAIAAEAAAACAAEAAAAJAAAAAAACAAAAAAAJAAEAAAACAAEAAAAKAAAAAAACAAAAAAAKAAEAAAACAAEAAAALAAAAAAACAAAAAAALAAEAAAACAAEAAAAMAAAAAAACAAAAAAAMAAEAAAACAAEAAAANAAAAAAACAAAAAAANAAEAAAACAAEAAAAOAAAAAAACAAAAAAAOAAEAAAACAAEAAAAPAAAAAAACAAAAAAAPAAEAAAACAAEAAAAQAAAAAAACAAAAAAAQAAEAAAACAAEAAAARAAAAAAACAAAAAAARAAEAAAACAAEAAAASAAAAAAACAAAAAAASAAEAAAACAAEAAAATAAAAAAACAAAAAAATAAEAAAACAAEAAAAUAAAAAAACAAAAAAAUAAEAAAACAAEAAAAVAAAAAAACAAAAAAAVAAEAAAACAAEAAAAWAAAAAAACAAAAAAAWAAEAAAACAAEAAAAXAAAAAAACAAAAAAAXAAEAAAACAAEAAAAYAAAAAAACAAAAAAAYAAEAAAACAAEAAAAZAAAAAAACAAAAAAAZAAEAAAACAAEAAAAaAAAAAAACAAAAAAAaAAEAAAACAAEAAAAbAAAAAAACAAAAAAAbAAEAAAACAAEAAAAcAAAAAAACAAAAAAAcAAEAAAACAAEAAAAdAAAAAAACAAAAAAAdAAEAAAACAAEAAAAeAAAAAAACAAAAAAAeAAEAAAACAAEAAAAfAAAAAAACAAAAAAAfAAEAAAACAAEAAAAgAAAAAAACAAAAAAAgAAEAAAACAAEAAAAhAAAAAAACAAAAAAAhAAEAAAACAAEAAAAAAAUAAAAAAAIAAAABAAUAAAABAAIAAAAAAAYAAAAAAAIAAAABAAYAAAABAAIAAAAAAAcAAAAAAAIAAAABAAcAAAABAAIAAAAAAAgAAAAAAAIAAAABAAgAAAABAAIAAAAAAAkAAAAAAAIAAAABAAkAAAABAAIAAAAAAAoAAAAAAAIAAAABAAoAAAABAAIAAAAAAAsAAAAAAAIAAAABAAsAAAABAAIAAAAAAAwAAAAAAAIAAAABAAwAAAABAAIAAAAAAA0AAAAAAAIAAAABAA0AAAABAAIAAAAAAA4AAAAAAAIAAAABAA4AAAABAAIAAAAAAA8AAAAAAAIAAAABAA8AAAABAAIAAAAAABAAAAAAAAIAAAABABAAAAABAAIAAAAAABEAAAAAAAIAAAABABEAAAABAAIAAAAAABUAAAAAAAMAAAAAABYAAAAAAAQAAAABABUAAAABAAMAAAABABYAAAABAAQAAAAAABIAAAAAAAIAAAABABIAAAABAAIAAAAAABMAAAAAAAIAAAABABMAAAABAAIAAAAAABQAAAAAAAIAAAABABQAAAABAAIAAAACABUAAAACAAMAAAACABYAAAACAAQAAAADABUAAAACAAMAAAADABYAAAACAAQAAAAEABUAAAACAAMAAAAEABYAAAACAAQAAAAFABUAAAACAAMAAAAFABYAAAACAAQAAAAGABUAAAACAAMAAAAGABYAAAACAAQAAAAHABUAAAACAAMAAAAHABYAAAACAAQAAAAIABUAAAACAAMAAAAIABYAAAACAAQAAAAJABUAAAACAAMAAAAJABYAAAACAAQAAAAKABUAAAACAAMAAAAKABYAAAACAAQAAAALABUAAAACAAMAAAALABYAAAACAAQAAAAMABUAAAACAAMAAAAMABYAAAACAAQAAAANABUAAAACAAMAAAANABYAAAACAAQAAAAOABUAAAACAAMAAAAOABYAAAACAAQAAAAPABUAAAACAAMAAAAPABYAAAACAAQAAAAQABUAAAACAAMAAAAQABYAAAACAAQAAAARABUAAAACAAMAAAARABYAAAACAAQAAAASABUAAAACAAMAAAASABYAAAACAAQAAAATABUAAAACAAMAAAATABYAAAACAAQAAAAUABUAAAACAAMAAAAUABYAAAACAAQAAAAVABUAAAAHAAUAAAAVABYAAAAHAAYAAAAWABUAAAAJAAgAAAAWABYAAAAIAAYAAAAXABUAAAAJAAUAAAAXABYAAAAJAAYAAAAYABUAAAACAAMAAAAYABYAAAACAAQAAAAZABUAAAACAAMAAAAZABYAAAACAAQAAAAaABUAAAACAAMAAAAaABYAAAACAAQAAAAbABUAAAACAAMAAAAbABYAAAACAAQAAAAcABUAAAACAAMAAAAcABYAAAACAAQAAAAdABUAAAACAAMAAAAdABYAAAACAAQAAAAeABUAAAACAAMAAAAeABYAAAACAAQAAAAfABUAAAACAAMAAAAfABYAAAACAAQAAAAgABUAAAACAAMAAAAgABYAAAACAAQAAAAhABUAAAACAAMAAAAhABYAAAACAAQAAAAiABUAAAACAAMAAAAiABYAAAACAAQAAAAjABUAAAACAAMAAAAjABYAAAACAAQAAAAkABUAAAACAAMAAAAkABYAAAACAAQAAAAlABUAAAACAAMAAAAlABYAAAACAAQAAAAmABUAAAACAAMAAAAmABYAAAACAAQAAAAnABUAAAACAAMAAAAnABYAAAACAAQAAAAoABUAAAACAAMAAAAoABYAAAACAAQAAAApABUAAAACAAMAAAApABYAAAACAAQAAAAqABUAAAADAAMAAAAqABYAAAADAAQAAAArABUAAAAEAAMAAAArABYAAAAEAAQAAAAqABMAAAADAAIAAAArABMAAAAEAAIAAAAqABQAAAADAAIAAAArABQAAAAEAAIAAAAqABIAAAADAAIAAAArABIAAAAEAAIAAAAqABEAAAADAAIAAAArABEAAAAEAAIAAAAqABAAAAADAAIAAAArABAAAAAEAAIAAAAqAA8AAAADAAIAAAArAA8AAAAEAAIAAAAqAA4AAAADAAIAAAArAA4AAAAEAAIAAAAqAA0AAAADAAIAAAArAA0AAAAEAAIAAAAqAAwAAAADAAIAAAArAAwAAAAEAAIAAAAqAAsAAAADAAIAAAArAAsAAAAEAAIAAAAqAAoAAAADAAIAAAArAAoAAAAEAAIAAAAqAAkAAAADAAIAAAArAAkAAAAEAAIAAAAqAAgAAAADAAIAAAArAAgAAAAEAAIAAAAqAAcAAAADAAIAAAArAAcAAAAEAAIAAAAqAAYAAAADAAIAAAArAAYAAAAEAAIAAAAqAAUAAAADAAIAAAArAAUAAAAEAAIAAAAqAAQAAAADAAIAAAArAAQAAAAEAAIAAAAqAAMAAAADAAIAAAArAAMAAAAEAAIAAAAqAAIAAAADAAIAAAArAAIAAAAEAAIAAAAqAAAAAAADAAAAAAAqAAEAAAADAAEAAAArAAAAAAAEAAAAAAArAAEAAAAEAAEAAAApAAAAAAACAAAAAAApAAEAAAACAAEAAAAoAAAAAAACAAAAAAAoAAEAAAACAAEAAAAnAAAAAAACAAAAAAAnAAEAAAACAAEAAAAmAAAAAAACAAAAAAAmAAEAAAACAAEAAAAlAAAAAAACAAAAAAAlAAEAAAACAAEAAAAkAAAAAAACAAAAAAAkAAEAAAACAAEAAAAjAAAAAAACAAAAAAAjAAEAAAACAAEAAAAiAAAAAAACAAAAAAAiAAEAAAACAAEAAAAFAAMAAAAOAAgAAAAFAAQAAAAOAAgAAAAGAAMAAAAOAAgAAAAHAAMAAAAOAAgAAAAFAAIAAAAOAAgAAAAGAAIAAAAOAAgAAAAHAAIAAAAOAAgAAAAIAAIAAAAOAAgAAAAJAAMAAAAOAAgAAAAKAAMAAAAOAAgAAAALAAMAAAAOAAgAAAAIAAMAAAAOAAgAAAAMAAMAAAAOAAgAAAALAAIAAAAOAAgAAAAKAAIAAAAOAAgAAAAJAAIAAAAOAAgAAAAHAAQAAAAOAAgAAAAHAAUAAAAOAAgAAAAIAAYAAAAGAAsAAAAJAAYAAAAGAAsAAAAKAAYAAAAGAAsAAAALAAYAAAAOAAgAAAALAAUAAAAJAAgAAAAMAAUAAAAOAAgAAAANAAUAAAAOAAgAAAAOAAUAAAAOAAgAAAAOAAQAAAAOAAgAAAAPAAQAAAAOAAgAAAANAAQAAAAOAAgAAAAMAAQAAAAOAAgAAAALAAQAAAAOAAgAAAAKAAQAAAAOAAgAAAAJAAQAAAAOAAgAAAAIAAQAAAAOAAgAAAAGAAQAAAAOAAgAAAAQAAQAAAAOAAgAAAARAAQAAAAOAAgAAAARAAUAAAAGAAsAAAAQAAUAAAAOAAgAAAAPAAUAAAAOAAgAAAANAAMAAAAOAAgAAAAOAAMAAAAOAAgAAAAPAAMAAAAOAAgAAAAQAAMAAAAOAAgAAAARAAMAAAAOAAgAAAASAAMAAAAOAAgAAAATAAMAAAAOAAgAAAASAAQAAAAOAAgAAAAIAAUAAAAOAAgAAAAJAAUAAAAOAAgAAAATAAQAAAAGAAsAAAAMAAIAAAAOAAgAAAAUAAMAAAAOAAgAAAAUAAIAAAAOAAgAAAATAAIAAAAOAAgAAAASAAIAAAAOAAgAAAARAAIAAAAOAAgAAAAQAAIAAAAOAAgAAAAPAAIAAAAOAAgAAAAOAAIAAAAOAAgAAAANAAIAAAAOAAgAAAAKAAUAAAAOAAgAAAAGAAUAAAAOAAgAAAAFAAUAAAAOAAgAAAAEAAUAAAAOAAgAAAADAAUAAAAOAAgAAAACAAUAAAAOAAgAAAACAAYAAAAOAAgAAAADAAYAAAAOAAgAAAAEAAYAAAAOAAgAAAAFAAYAAAAOAAgAAAAGAAYAAAAOAAgAAAAHAAYAAAAOAAgAAAAVAAgAAAAOAAgAAAAVAAcAAAAJAAgAAAAVAAYAAAAOAAgAAAAVAAUAAAAGAAsAAAAVAAQAAAAOAAgAAAAVAAMAAAAOAAgAAAAVAAIAAAAOAAgAAAAWAAIAAAAOAAgAAAAWAAMAAAAOAAgAAAAWAAQAAAAOAAgAAAAWAAUAAAAGAAsAAAAWAAYAAAAOAAgAAAAWAAcAAAAJAAgAAAAWAAgAAAAJAAgAAAAWAAkAAAAJAAgAAAAVAAkAAAAJAAgAAAAUAAkAAAAJAAgAAAAUAAgAAAAJAAgAAAAUAAcAAAAJAAgAAAAUAAYAAAAOAAgAAAAUAAUAAAAGAAsAAAAUAAQAAAAGAAsAAAATAAUAAAAGAAsAAAASAAUAAAAGAAsAAAASAAYAAAAGAAsAAAARAAYAAAAGAAsAAAAQAAYAAAAGAAsAAAAPAAYAAAAGAAsAAAAOAAYAAAAOAAgAAAANAAYAAAAOAAgAAAAMAAYAAAAOAAgAAAAMAAcAAAAGAAsAAAALAAcAAAAGAAsAAAAKAAcAAAAGAAsAAAAJAAcAAAAGAAsAAAAIAAcAAAAGAAsAAAAHAAcAAAAGAAsAAAAGAAcAAAAGAAsAAAAFAAcAAAAOAAgAAAAEAAcAAAAOAAgAAAADAAcAAAAOAAgAAAACAAcAAAAOAAgAAAACAAgAAAAOAAgAAAACAAkAAAAOAAgAAAACAAoAAAAOAAgAAAACAAsAAAAOAAgAAAACAAwAAAAOAAgAAAACAA0AAAAOAAgAAAACAA4AAAAOAAgAAAACAA8AAAAOAAgAAAACABAAAAAOAAgAAAACABEAAAAOAAgAAAACABIAAAAOAAgAAAACABMAAAAOAAgAAAACABQAAAAOAAgAAAADABQAAAAOAAgAAAADABMAAAAOAAgAAAADABIAAAAOAAgAAAADABEAAAAOAAgAAAADABAAAAAOAAgAAAADAA8AAAAOAAgAAAADAA4AAAAOAAgAAAADAA0AAAAOAAgAAAADAAwAAAAOAAgAAAADAAsAAAAOAAgAAAADAAoAAAAOAAgAAAADAAkAAAAOAAgAAAADAAgAAAAOAAgAAAAEAAgAAAAOAAgAAAAEAAkAAAAOAAgAAAAEAAoAAAAOAAgAAAAEAAsAAAAOAAgAAAAEAAwAAAAOAAgAAAAEAA0AAAAOAAgAAAAEAA4AAAAOAAgAAAAEAA8AAAAOAAgAAAAEABAAAAAOAAgAAAAEABEAAAAOAAgAAAAEABIAAAAOAAgAAAAEABMAAAAOAAgAAAAEABQAAAAOAAgAAAAFABQAAAAOAAgAAAAFABMAAAAOAAgAAAAFABIAAAAOAAgAAAAFABEAAAAOAAgAAAAFABAAAAAGAAsAAAAFAA8AAAAGAAsAAAAFAA4AAAAGAAsAAAAFAA0AAAAGAAsAAAAFAAwAAAAGAAsAAAAFAAsAAAAGAAsAAAAFAAoAAAAGAAsAAAAFAAkAAAAGAAsAAAAFAAgAAAAOAAgAAAAGAAgAAAAGAAsAAAAGAAkAAAAGAAsAAAAGAAoAAAAGAAsAAAAGAAsAAAAOAAgAAAAGAAwAAAAOAAgAAAAGAA0AAAAOAAgAAAAGAA4AAAAOAAgAAAAGAA8AAAAGAAsAAAAGABAAAAAGAAsAAAAGABEAAAAOAAgAAAAGABIAAAAOAAgAAAAGABMAAAAOAAgAAAAGABQAAAAOAAgAAAAHABQAAAAOAAgAAAAHABMAAAAOAAgAAAAHABIAAAAGAAsAAAAHABEAAAAGAAsAAAAHABAAAAAGAAsAAAAHAA8AAAAGAAsAAAAHAA4AAAAGAAsAAAAHAA0AAAAGAAsAAAAHAAwAAAAGAAsAAAAHAAsAAAAGAAsAAAAHAAoAAAAGAAsAAAAHAAkAAAAOAAgAAAAHAAgAAAAOAAgAAAAIAAgAAAAOAAgAAAAIAAkAAAAOAAgAAAAIAAoAAAAGAAsAAAAIAAsAAAAGAAsAAAAIAAwAAAAOAAgAAAAIAA0AAAAOAAgAAAAIAA4AAAAJAAgAAAAIAA8AAAAJAAgAAAAIABAAAAAOAAgAAAAIABEAAAAOAAgAAAAIABIAAAAGAAsAAAAIABMAAAAOAAgAAAAIABQAAAAOAAgAAAAJABQAAAAOAAgAAAAJABMAAAAOAAgAAAAJABIAAAAGAAsAAAAJABEAAAAOAAgAAAAJABAAAAAOAAgAAAAJAA8AAAAJAAgAAAAJAA4AAAAOAAgAAAAJAA0AAAAJAAgAAAAJAAwAAAAGAAsAAAAJAAsAAAAGAAsAAAAJAAoAAAAOAAgAAAAJAAkAAAAGAAsAAAAJAAgAAAAGAAsAAAAKAAgAAAAGAAsAAAAKAAkAAAAGAAsAAAAKAAoAAAAJAAgAAAAKAAsAAAAJAAgAAAAKAAwAAAAGAAsAAAAKAA0AAAAGAAsAAAAKAA4AAAAOAAgAAAAKAA8AAAAOAAgAAAAKABAAAAAJAAgAAAAKABEAAAAOAAgAAAAKABIAAAAGAAsAAAAKABMAAAAOAAgAAAAKABQAAAAOAAgAAAALABQAAAAOAAgAAAALABMAAAAOAAgAAAALABIAAAAGAAsAAAALABEAAAAGAAsAAAALABAAAAAJAAgAAAALAA8AAAAOAAgAAAALAA4AAAAOAAgAAAALAA0AAAAGAAsAAAALAAwAAAAGAAsAAAALAAsAAAAJAAgAAAALAAoAAAAOAAgAAAALAAkAAAAGAAsAAAALAAgAAAAGAAsAAAAMAAgAAAAGAAsAAAAMAAkAAAAJAAgAAAAMAAoAAAAOAAgAAAAMAAsAAAAJAAgAAAAMAAwAAAAGAAsAAAAMAA0AAAAOAAgAAAAMAA4AAAAGAAsAAAAMAA8AAAAOAAgAAAAMABAAAAAOAAgAAAAMABEAAAAGAAsAAAAMABIAAAAOAAgAAAAMABMAAAAOAAgAAAAMABQAAAAOAAgAAAANABQAAAAOAAgAAAANABMAAAAOAAgAAAANABIAAAAOAAgAAAANABEAAAAGAAsAAAANABAAAAAGAAsAAAANAA8AAAAJAAgAAAANAA4AAAAGAAsAAAANAA0AAAAOAAgAAAANAAwAAAAGAAsAAAANAAsAAAAGAAsAAAANAAoAAAAGAAsAAAANAAkAAAAJAAgAAAANAAgAAAAGAAsAAAANAAcAAAAGAAsAAAAOAAcAAAAGAAsAAAAOAAgAAAAGAAsAAAAOAAkAAAAGAAsAAAAOAAoAAAAJAAgAAAAOAAsAAAAJAAgAAAAOAAwAAAAJAAgAAAAOAA0AAAAGAAsAAAAOAA4AAAAJAAgAAAAOAA8AAAAGAAsAAAAOABAAAAAGAAsAAAAOABEAAAAGAAsAAAAOABIAAAAOAAgAAAAOABMAAAAOAAgAAAAOABQAAAAOAAgAAAAPABQAAAAOAAgAAAAPABMAAAAOAAgAAAAPABIAAAAOAAgAAAAPABEAAAAGAAsAAAAPABAAAAAGAAsAAAAPAA8AAAAGAAsAAAAPAA4AAAAGAAsAAAAPAA0AAAAGAAsAAAAPAAwAAAAJAAgAAAAPAAsAAAAJAAgAAAAPAAoAAAAJAAgAAAAPAAkAAAAJAAgAAAAPAAgAAAAGAAsAAAAPAAcAAAAGAAsAAAAQAAcAAAAGAAsAAAAQAAgAAAAJAAgAAAAQAAkAAAAJAAgAAAAQAAoAAAAJAAgAAAAQAAsAAAAOAAgAAAAQAAwAAAAJAAgAAAAQAA0AAAAGAAsAAAAQAA4AAAAOAAgAAAAQAA8AAAAGAAsAAAAQABAAAAAGAAsAAAAQABEAAAAGAAsAAAAQABIAAAAOAAgAAAAQABMAAAAOAAgAAAAQABQAAAAOAAgAAAARABQAAAAOAAgAAAARABMAAAAOAAgAAAARABIAAAAOAAgAAAARABEAAAAGAAsAAAARABAAAAAGAAsAAAARAA8AAAAGAAsAAAARAA4AAAAJAAgAAAARAA0AAAAGAAsAAAARAAwAAAAJAAgAAAARAAsAAAAOAAgAAAARAAoAAAAOAAgAAAARAAkAAAAJAAgAAAARAAgAAAAJAAgAAAARAAcAAAAGAAsAAAASAAcAAAAJAAgAAAASAAgAAAAJAAgAAAASAAkAAAAOAAgAAAASAAoAAAAOAAgAAAASAAsAAAAJAAgAAAASAAwAAAAGAAsAAAASAA0AAAAGAAsAAAASAA4AAAAGAAsAAAASAA8AAAAGAAsAAAASABAAAAAGAAsAAAASABEAAAAGAAsAAAASABIAAAAOAAgAAAASABMAAAAOAAgAAAASABQAAAAOAAgAAAATABQAAAAOAAgAAAATABMAAAAOAAgAAAATABIAAAAOAAgAAAATABEAAAAGAAsAAAATABAAAAAGAAsAAAATAA8AAAAJAAgAAAATAA4AAAAGAAsAAAATAA0AAAAGAAsAAAATAAwAAAAJAAgAAAATAAsAAAAGAAsAAAATAAoAAAAJAAgAAAATAAkAAAAJAAgAAAATAAgAAAAJAAgAAAATAAcAAAAJAAgAAAATAAYAAAAGAAsAAAAUAAoAAAAJAAgAAAAUAAsAAAAGAAsAAAAUAAwAAAAGAAsAAAAUAA0AAAAGAAsAAAAUAA4AAAAGAAsAAAAUAA8AAAAJAAgAAAAUABAAAAAGAAsAAAAUABEAAAAGAAsAAAAUABIAAAAOAAgAAAAUABMAAAAOAAgAAAAUABQAAAAOAAgAAAAVABQAAAAOAAgAAAAVABMAAAAOAAgAAAAVABIAAAAOAAgAAAAVABEAAAAGAAsAAAAVABAAAAAGAAsAAAAVAA8AAAAJAAgAAAAVAA4AAAAGAAsAAAAVAA0AAAAGAAsAAAAVAAwAAAAGAAsAAAAVAAsAAAAOAAgAAAAVAAoAAAAGAAsAAAAWAAoAAAAGAAsAAAAWAAsAAAAGAAsAAAAWAAwAAAAGAAsAAAAWAA0AAAAGAAsAAAAWAA4AAAAOAAgAAAAWAA8AAAAJAAgAAAAWABAAAAAGAAsAAAAWABEAAAAGAAsAAAAWABIAAAAOAAgAAAAWABMAAAAOAAgAAAAWABQAAAAOAAgAAAAXABQAAAAOAAgAAAAXABMAAAAOAAgAAAAXABIAAAAOAAgAAAAXABEAAAAGAAsAAAAXABAAAAAGAAsAAAAXAA8AAAAJAAgAAAAXAA4AAAAOAAgAAAAXAA0AAAAGAAsAAAAXAAwAAAAGAAsAAAAXAAsAAAAGAAsAAAAXAAoAAAAOAAgAAAAXAAkAAAAGAAsAAAAXAAgAAAAOAAgAAAAXAAcAAAAJAAgAAAAXAAYAAAAOAAgAAAAXAAUAAAAGAAsAAAAXAAQAAAAOAAgAAAAXAAMAAAAOAAgAAAAXAAIAAAAOAAgAAAAYAAIAAAAOAAgAAAAYAAMAAAAOAAgAAAAYAAQAAAAOAAgAAAAYAAUAAAAGAAsAAAAYAAYAAAAGAAsAAAAYAAcAAAAJAAgAAAAYAAgAAAAOAAgAAAAYAAkAAAAGAAsAAAAYAAoAAAAOAAgAAAAYAAsAAAAGAAsAAAAYAAwAAAAGAAsAAAAYAA0AAAAGAAsAAAAYAA4AAAAOAAgAAAAYAA8AAAAJAAgAAAAYABAAAAAGAAsAAAAYABEAAAAGAAsAAAAYABIAAAAOAAgAAAAYABMAAAAOAAgAAAAYABQAAAAOAAgAAAAZABQAAAAOAAgAAAAZABMAAAAOAAgAAAAZABIAAAAOAAgAAAAZABEAAAAGAAsAAAAZABAAAAAGAAsAAAAZAA8AAAAJAAgAAAAZAA4AAAAJAAgAAAAZAA0AAAAGAAsAAAAZAAwAAAAGAAsAAAAZAAsAAAAOAAgAAAAZAAoAAAAGAAsAAAAZAAkAAAAGAAsAAAAZAAgAAAAGAAsAAAAZAAcAAAAJAAgAAAAZAAYAAAAOAAgAAAAZAAUAAAAGAAsAAAAZAAQAAAAOAAgAAAAZAAMAAAAOAAgAAAAZAAIAAAAOAAgAAAAaAAIAAAAOAAgAAAAaAAMAAAAOAAgAAAAaAAQAAAAOAAgAAAAaAAUAAAAGAAsAAAAaAAYAAAAOAAgAAAAaAAcAAAAJAAgAAAAaAAgAAAAGAAsAAAAaAAkAAAAGAAsAAAAaAAoAAAAGAAsAAAAaAAsAAAAOAAgAAAAaAAwAAAAJAAgAAAAaAA0AAAAGAAsAAAAaAA4AAAAJAAgAAAAaAA8AAAAGAAsAAAAaABAAAAAGAAsAAAAaABEAAAAGAAsAAAAaABIAAAAOAAgAAAAaABMAAAAOAAgAAAAaABQAAAAOAAgAAAAbABQAAAAOAAgAAAAbABMAAAAOAAgAAAAbABIAAAAOAAgAAAAbABEAAAAGAAsAAAAbABAAAAAGAAsAAAAbAA8AAAAGAAsAAAAbAA4AAAAJAAgAAAAbAA0AAAAGAAsAAAAbAAwAAAAJAAgAAAAbAAsAAAAOAAgAAAAbAAoAAAAGAAsAAAAbAAkAAAAJAAgAAAAbAAgAAAAJAAgAAAAbAAcAAAAJAAgAAAAbAAYAAAAGAAsAAAAbAAUAAAAOAAgAAAAbAAQAAAAOAAgAAAAbAAMAAAAOAAgAAAAbAAIAAAAOAAgAAAAcAAIAAAAOAAgAAAAcAAMAAAAOAAgAAAAcAAQAAAAOAAgAAAAcAAUAAAAOAAgAAAAcAAYAAAAGAAsAAAAcAAcAAAAJAAgAAAAcAAgAAAAJAAgAAAAcAAkAAAAJAAgAAAAcAAoAAAAGAAsAAAAcAAsAAAAGAAsAAAAcAAwAAAAGAAsAAAAcAA0AAAAJAAgAAAAcAA4AAAAJAAgAAAAcAA8AAAAGAAsAAAAcABAAAAAGAAsAAAAcABEAAAAGAAsAAAAcABIAAAAOAAgAAAAcABMAAAAOAAgAAAAcABQAAAAOAAgAAAAdABQAAAAOAAgAAAAdABMAAAAOAAgAAAAdABIAAAAOAAgAAAAdABEAAAAGAAsAAAAdABAAAAAGAAsAAAAdAA8AAAAGAAsAAAAdAA4AAAAOAAgAAAAdAA0AAAAOAAgAAAAdAAwAAAAGAAsAAAAdAAsAAAAGAAsAAAAdAAoAAAAGAAsAAAAdAAkAAAAOAAgAAAAdAAgAAAAOAAgAAAAdAAcAAAAOAAgAAAAdAAYAAAAGAAsAAAAdAAUAAAAOAAgAAAAdAAQAAAAOAAgAAAAdAAMAAAAOAAgAAAAdAAIAAAAOAAgAAAAeAAIAAAAOAAgAAAAeAAMAAAAOAAgAAAAeAAQAAAAOAAgAAAAeAAUAAAAOAAgAAAAeAAYAAAAGAAsAAAAeAAcAAAAOAAgAAAAeAAgAAAAOAAgAAAAeAAkAAAAOAAgAAAAeAAoAAAAGAAsAAAAeAAsAAAAGAAsAAAAeAAwAAAAGAAsAAAAeAA0AAAAGAAsAAAAeAA4AAAAGAAsAAAAeAA8AAAAGAAsAAAAeABAAAAAGAAsAAAAeABEAAAAOAAgAAAAeABIAAAAOAAgAAAAeABMAAAAOAAgAAAAeABQAAAAOAAgAAAAfABQAAAAOAAgAAAAfABMAAAAOAAgAAAAfABIAAAAOAAgAAAAfABEAAAAOAAgAAAAfABAAAAAGAAsAAAAfAA8AAAAOAAgAAAAfAA4AAAAGAAsAAAAfAA0AAAAOAAgAAAAfAAwAAAAOAAgAAAAfAAsAAAAGAAsAAAAfAAoAAAAGAAsAAAAfAAkAAAAOAAgAAAAfAAgAAAAOAAgAAAAfAAcAAAAOAAgAAAAfAAYAAAAGAAsAAAAfAAUAAAAOAAgAAAAfAAQAAAAOAAgAAAAfAAMAAAAOAAgAAAAfAAIAAAAOAAgAAAAgAAIAAAAOAAgAAAAgAAMAAAAOAAgAAAAgAAQAAAAOAAgAAAAgAAUAAAAOAAgAAAAgAAYAAAAJAAgAAAAgAAcAAAAGAAsAAAAgAAgAAAAOAAgAAAAgAAkAAAAOAAgAAAAgAAoAAAAGAAsAAAAgAAsAAAAJAAgAAAAgAAwAAAAOAAgAAAAgAA0AAAAOAAgAAAAgAA4AAAAGAAsAAAAgAA8AAAAGAAsAAAAgABAAAAAOAAgAAAAgABEAAAAOAAgAAAAgABIAAAAOAAgAAAAgABMAAAAOAAgAAAAgABQAAAAOAAgAAAAhABQAAAAOAAgAAAAhABMAAAAOAAgAAAAhABIAAAAGAAsAAAAhABEAAAAOAAgAAAAhABAAAAAOAAgAAAAhAA8AAAAGAAsAAAAhAA4AAAAOAAgAAAAhAA0AAAAGAAsAAAAhAAwAAAAOAAgAAAAhAAsAAAAJAAgAAAAhAAoAAAAOAAgAAAAhAAkAAAAOAAgAAAAhAAgAAAAOAAgAAAAhAAcAAAAGAAsAAAAhAAYAAAAOAAgAAAAhAAUAAAAOAAgAAAAhAAQAAAAOAAgAAAAhAAMAAAAOAAgAAAAhAAIAAAAOAAgAAAAiAAIAAAAOAAgAAAAiAAMAAAAOAAgAAAAiAAQAAAAGAAsAAAAiAAUAAAAGAAsAAAAiAAYAAAAGAAsAAAAiAAcAAAAGAAsAAAAiAAgAAAAJAAgAAAAiAAkAAAAOAAgAAAAiAAoAAAAOAAgAAAAiAAsAAAAOAAgAAAAiAAwAAAAOAAgAAAAiAA0AAAAGAAsAAAAiAA4AAAAGAAsAAAAiAA8AAAAOAAgAAAAiABAAAAAOAAgAAAAiABEAAAAGAAsAAAAiABIAAAAGAAsAAAAiABMAAAAOAAgAAAAiABQAAAAOAAgAAAAjABQAAAAOAAgAAAAjABMAAAAOAAgAAAAjABIAAAAOAAgAAAAjABEAAAAGAAsAAAAjABAAAAAOAAgAAAAjAA8AAAAOAAgAAAAjAA4AAAAGAAsAAAAjAA0AAAAOAAgAAAAjAAwAAAAGAAsAAAAjAAsAAAAOAAgAAAAjAAoAAAAOAAgAAAAjAAkAAAAGAAsAAAAjAAgAAAAGAAsAAAAjAAcAAAAOAAgAAAAjAAYAAAAOAAgAAAAjAAUAAAAGAAsAAAAjAAQAAAAGAAsAAAAjAAMAAAAOAAgAAAAjAAIAAAAOAAgAAAAkAAIAAAAOAAgAAAAkAAMAAAAOAAgAAAAkAAQAAAAOAAgAAAAkAAUAAAAOAAgAAAAkAAYAAAAGAAsAAAAkAAcAAAAOAAgAAAAkAAgAAAAGAAsAAAAkAAkAAAAGAAsAAAAkAAoAAAAGAAsAAAAkAAsAAAAGAAsAAAAkAAwAAAAOAAgAAAAkAA0AAAAGAAsAAAAkAA4AAAAOAAgAAAAkAA8AAAAOAAgAAAAkABAAAAAGAAsAAAAkABEAAAAGAAsAAAAkABIAAAAOAAgAAAAkABMAAAAOAAgAAAAkABQAAAAOAAgAAAAlABQAAAAOAAgAAAAlABMAAAAOAAgAAAAlABIAAAAOAAgAAAAlABEAAAAOAAgAAAAlABAAAAAGAAsAAAAlAA8AAAAGAAsAAAAlAA4AAAAGAAsAAAAlAA0AAAAGAAsAAAAlAAwAAAAGAAsAAAAlAAsAAAAGAAsAAAAlAAoAAAAGAAsAAAAlAAkAAAAGAAsAAAAlAAgAAAAGAAsAAAAlAAcAAAAGAAsAAAAlAAYAAAAOAAgAAAAlAAUAAAAOAAgAAAAlAAQAAAAOAAgAAAAlAAMAAAAOAAgAAAAlAAIAAAAOAAgAAAAmAAIAAAAOAAgAAAAmAAMAAAAOAAgAAAAmAAQAAAAOAAgAAAAmAAUAAAAOAAgAAAAmAAYAAAAOAAgAAAAmAAcAAAAOAAgAAAAmAAgAAAAOAAgAAAAmAAkAAAAGAAsAAAAmAAoAAAAGAAsAAAAmAAsAAAAGAAsAAAAmAAwAAAAGAAsAAAAmAA0AAAAOAAgAAAAmAA4AAAAOAAgAAAAmAA8AAAAOAAgAAAAmABAAAAAOAAgAAAAmABEAAAAOAAgAAAAmABIAAAAOAAgAAAAmABMAAAAOAAgAAAAmABQAAAAOAAgAAAAnABQAAAAOAAgAAAAnABMAAAAOAAgAAAAnABIAAAAOAAgAAAAnABEAAAAOAAgAAAAnABAAAAAOAAgAAAAnAA8AAAAOAAgAAAAnAA4AAAAOAAgAAAAnAA0AAAAOAAgAAAAnAAwAAAAOAAgAAAAnAAsAAAAOAAgAAAAnAAoAAAAGAAsAAAAnAAkAAAAGAAsAAAAnAAgAAAAOAAgAAAAnAAcAAAAOAAgAAAAnAAYAAAAOAAgAAAAnAAUAAAAOAAgAAAAnAAQAAAAOAAgAAAAnAAMAAAAOAAgAAAAnAAIAAAAOAAgAAAAoAAIAAAAOAAgAAAAoAAMAAAAOAAgAAAAoAAQAAAAOAAgAAAAoAAUAAAAOAAgAAAAoAAYAAAAOAAgAAAAoAAcAAAAOAAgAAAAoAAgAAAAOAAgAAAAoAAkAAAAOAAgAAAAoAAoAAAAOAAgAAAAoAAsAAAAOAAgAAAAoAAwAAAAOAAgAAAAoAA0AAAAOAAgAAAAoAA4AAAAOAAgAAAAoAA8AAAAOAAgAAAAoABAAAAAOAAgAAAAoABEAAAAOAAgAAAAoABIAAAAOAAgAAAAoABMAAAAOAAgAAAAoABQAAAAOAAgAAAApABQAAAAOAAgAAAApABMAAAAOAAgAAAApABIAAAAOAAgAAAApABEAAAAOAAgAAAApABAAAAAOAAgAAAApAA8AAAAOAAgAAAApAA4AAAAOAAgAAAApAA0AAAAOAAgAAAApAAwAAAAOAAgAAAApAAsAAAAOAAgAAAApAAoAAAAOAAgAAAApAAkAAAAOAAgAAAApAAgAAAAOAAgAAAApAAcAAAAOAAgAAAApAAYAAAAOAAgAAAApAAUAAAAOAAgAAAApAAQAAAAOAAgAAAApAAMAAAAOAAgAAAApAAIAAAAOAAgAAAAWABcAAAAJAAgAAAAWABgAAAAOAAgAAAAWABkAAAAOAAgAAAAWABoAAAAOAAgAAAAWAB4AAAAOAAgAAAAWAB0AAAAOAAgAAAAWABwAAAAOAAgAAAAWABsAAAAJAAgAAAAWACAAAAAOAAgAAAAWAB8AAAAOAAgAAAA=") tile_set = ExtResource("1_cvtbc") +[node name="TileMapLayer" type="TileMapLayer" parent="Environment" unique_id=349941462] +tile_set = ExtResource("1_cvtbc") + [node name="TileMapLayerAbove" type="TileMapLayer" parent="Environment" unique_id=2036754042] modulate = Color(1, 1, 1, 0.46666667) z_index = 1 diff --git a/src/scenes/healing_effect.tscn b/src/scenes/healing_effect.tscn new file mode 100644 index 0000000..ad83e97 --- /dev/null +++ b/src/scenes/healing_effect.tscn @@ -0,0 +1,21 @@ +[gd_scene format=3 uid="uid://d3heal8fx2kqm"] + +[ext_resource type="Script" path="res://scripts/healing_effect.gd" id="1_script"] +[ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="2_tex"] + +[node name="HealingEffect" type="Node2D"] +z_index = 5 +script = ExtResource("1_script") + +[node name="FxSprite" type="Sprite2D" parent="."] +offset = Vector2(0, -24) +texture = ExtResource("2_tex") +hframes = 105 +vframes = 79 +frame = 589 + +[node name="HealLight" type="PointLight2D" parent="."] +color = Color(0.3, 1, 0.35, 1) +energy = 0.8 +texture_scale = 0.5 +enabled = false diff --git a/src/scenes/loot.tscn b/src/scenes/loot.tscn index 8e8611d..d10aba8 100644 --- a/src/scenes/loot.tscn +++ b/src/scenes/loot.tscn @@ -3,6 +3,7 @@ [ext_resource type="Script" uid="uid://jk7o0itmiwp6" path="res://scripts/loot.gd" id="1"] [ext_resource type="Script" uid="uid://cpxabh3uq1kl4" path="res://scripts/create_shadow_sprite.gd" id="2"] [ext_resource type="AudioStream" uid="uid://b60bke4f5uw4v" path="res://assets/audio/sfx/pickups/coin_pickup.mp3" id="3_30m34"] +[ext_resource type="Shader" uid="uid://ldl7vaq5n13f" path="res://shaders/cloth.gdshader" id="3_37k03"] [ext_resource type="Texture2D" uid="uid://cimek2qjgoqa1" path="res://assets/gfx/pickups/gold_coin.png" id="3_531sv"] [ext_resource type="AudioStream" uid="uid://brl8ivwb1l5i7" path="res://assets/audio/sfx/pickups/coin_drop_01.wav.mp3" id="4_rtp8m"] [ext_resource type="AudioStream" uid="uid://umoxmryvbm01" path="res://assets/audio/sfx/cloth/leather_cloth_01.wav.mp3" id="5_vl55g"] @@ -22,6 +23,24 @@ fill = 1 fill_from = Vector2(0.51304346, 0.46086955) fill_to = Vector2(0, 0) +[sub_resource type="ShaderMaterial" id="ShaderMaterial_8ty1w"] +shader = ExtResource("3_37k03") +shader_parameter/original_0 = Color(0, 0, 0, 1) +shader_parameter/original_1 = Color(0, 0, 0, 1) +shader_parameter/original_2 = Color(0, 0, 0, 1) +shader_parameter/original_3 = Color(0, 0, 0, 1) +shader_parameter/original_4 = Color(0, 0, 0, 1) +shader_parameter/original_5 = Color(0, 0, 0, 1) +shader_parameter/original_6 = Color(0, 0, 0, 1) +shader_parameter/replace_0 = Color(0, 0, 0, 1) +shader_parameter/replace_1 = Color(0, 0, 0, 1) +shader_parameter/replace_2 = Color(0, 0, 0, 1) +shader_parameter/replace_3 = Color(0, 0, 0, 1) +shader_parameter/replace_4 = Color(0, 0, 0, 1) +shader_parameter/replace_5 = Color(0, 0, 0, 1) +shader_parameter/replace_6 = Color(0, 0, 0, 1) +shader_parameter/tint = Color(1, 1, 1, 1) + [sub_resource type="CircleShape2D" id="CircleShape2D_2"] radius = 3.0 @@ -49,6 +68,7 @@ script = ExtResource("2") [node name="Sprite2D" type="Sprite2D" parent="." unique_id=1501367665] y_sort_enabled = true +material = SubResource("ShaderMaterial_8ty1w") texture = ExtResource("3_531sv") hframes = 6 diff --git a/src/scenes/off_screen_indicators.tscn b/src/scenes/off_screen_indicators.tscn new file mode 100644 index 0000000..64b1c49 --- /dev/null +++ b/src/scenes/off_screen_indicators.tscn @@ -0,0 +1,16 @@ +[gd_scene format=3 uid="uid://c8k2xmwv4nqyp"] + +[ext_resource type="Script" path="res://scripts/off_screen_indicators.gd" id="1_script"] + +[node name="OffScreenIndicators" type="CanvasLayer"] +layer = 199 + +[node name="IndicatorOverlay" type="Control" parent="."] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +script = ExtResource("1_script") diff --git a/src/scenes/player.tscn b/src/scenes/player.tscn index d0e8034..0c79300 100644 --- a/src/scenes/player.tscn +++ b/src/scenes/player.tscn @@ -14,7 +14,9 @@ [ext_resource type="Texture2D" uid="uid://cu5fkio3ajr5i" path="res://assets/gfx/Puny-Characters/Layer 6 - Headgears/French/MusketeerHatPurple.png" id="12"] [ext_resource type="Texture2D" uid="uid://bloqx3mibftjn" path="res://assets/gfx/Puny-Characters/WeaponOverlayer.png" id="13"] [ext_resource type="AudioStream" uid="uid://cbio6f0ssxvd6" path="res://assets/audio/sfx/walk/stone/walk_stone_1.wav.mp3" id="13_fulsm"] +[ext_resource type="Texture2D" uid="uid://bkca7nmt4du5e" path="res://assets/gfx/Puny-Characters/ShieldOverlayer.png" id="13_t4otl"] [ext_resource type="AudioStream" uid="uid://dq1va2882v23v" path="res://assets/audio/sfx/walk/stone/walk_stone_2.wav.mp3" id="14_4r5pv"] +[ext_resource type="Texture2D" uid="uid://bpxxpdpow5qyl" path="res://assets/gfx/Puny-Characters/ShieldOverlayerHolding.png" id="14_j2b1d"] [ext_resource type="AudioStream" uid="uid://dsuf4oa710gi8" path="res://assets/audio/sfx/walk/stone/walk_stone_3.wav.mp3" id="15_60mlk"] [ext_resource type="AudioStream" uid="uid://fvhvmxtcq018" path="res://assets/audio/sfx/walk/stone/walk_stone_4.wav.mp3" id="16_i4ail"] [ext_resource type="AudioStream" uid="uid://cw74evef8fm0t" path="res://assets/audio/sfx/walk/stone/walk_stone_5.wav.mp3" id="17_a38lo"] @@ -37,6 +39,12 @@ [ext_resource type="AudioStream" uid="uid://bvi00vbftbgc5" path="res://assets/audio/sfx/player/ultra_run/shinespark_start.wav" id="35_bj30b"] [ext_resource type="AudioStream" uid="uid://0xm3gyh8051h" path="res://assets/audio/sfx/wizard/incantations/indignation.mp3" id="36_jc3p3"] [ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="37_hax0n"] +[ext_resource type="AudioStream" uid="uid://c10ju1f6d4ed3" path="res://assets/audio/sfx/shield/activate_shield.wav" id="40_hhpqf"] +[ext_resource type="AudioStream" uid="uid://ly1euk0v3jxy" path="res://assets/audio/sfx/shield/shield.wav" id="41_g5jhy"] +[ext_resource type="AudioStream" uid="uid://c4lh535yj010h" path="res://assets/audio/sfx/shield/shield1.wav" id="42_holxr"] +[ext_resource type="AudioStream" uid="uid://ch3p57i7fvd1v" path="res://assets/audio/sfx/shield/shield2.wav" id="43_mx1m4"] +[ext_resource type="AudioStream" uid="uid://t0sg2rxlfech" path="res://assets/audio/sfx/shield/shield3.wav" id="44_4gjji"] +[ext_resource type="AudioStream" uid="uid://dvq72502qa46f" path="res://assets/audio/sfx/shield/denied_activate_Shield2.wav" id="45_g5jhy"] [sub_resource type="Gradient" id="Gradient_wqfne"] colors = PackedColorArray(0, 0, 0, 1, 1, 0.13732082, 0.092538536, 1) @@ -234,6 +242,24 @@ shader_parameter/replace_5 = Color(0, 0, 0, 1) shader_parameter/replace_6 = Color(0, 0, 0, 1) shader_parameter/tint = Color(1, 1, 1, 1) +[sub_resource type="ShaderMaterial" id="ShaderMaterial_hhpqf"] +shader = ExtResource("3_wnwbv") +shader_parameter/original_0 = Color(0, 0, 0, 1) +shader_parameter/original_1 = Color(0, 0, 0, 1) +shader_parameter/original_2 = Color(0, 0, 0, 1) +shader_parameter/original_3 = Color(0, 0, 0, 1) +shader_parameter/original_4 = Color(0, 0, 0, 1) +shader_parameter/original_5 = Color(0, 0, 0, 1) +shader_parameter/original_6 = Color(0, 0, 0, 1) +shader_parameter/replace_0 = Color(0, 0, 0, 1) +shader_parameter/replace_1 = Color(0, 0, 0, 1) +shader_parameter/replace_2 = Color(0, 0, 0, 1) +shader_parameter/replace_3 = Color(0, 0, 0, 1) +shader_parameter/replace_4 = Color(0, 0, 0, 1) +shader_parameter/replace_5 = Color(0, 0, 0, 1) +shader_parameter/replace_6 = Color(0, 0, 0, 1) +shader_parameter/tint = Color(1, 1, 1, 1) + [sub_resource type="ShaderMaterial" id="ShaderMaterial_fdfoy"] shader = ExtResource("3_wnwbv") shader_parameter/original_0 = Color(0, 0, 0, 1) @@ -342,6 +368,73 @@ tracks/0/keys = { "values": [2037, 2038, 2039, 2040, 2041, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051, 2052, 2053] } +[sub_resource type="Animation" id="Animation_frost_ch"] +resource_name = "frost_charging" +length = 0.566 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("IncantationSprite:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.03333334, 0.06666667, 0.1, 0.13333334, 0.16666667, 0.2, 0.23333333, 0.26666668, 0.3, 0.33333334, 0.36666667, 0.4, 0.43333334, 0.46666667, 0.5, 0.53333336), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [3678, 3679, 3680, 3681, 3682, 3683, 3684, 3685, 3686, 3687, 3688, 3689, 3690, 3691, 3692, 3693, 3694] +} + +[sub_resource type="Animation" id="Animation_frost_rdy"] +resource_name = "frost_ready" +length = 0.566 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("IncantationSprite:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.03333334, 0.06666667, 0.1, 0.13333334, 0.16666667, 0.2, 0.23333333, 0.26666668, 0.3, 0.33333334, 0.36666667, 0.4, 0.43333334, 0.46666667, 0.5, 0.53333336), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [3678, 3679, 3680, 3681, 3682, 3683, 3684, 3685, 3686, 3687, 3688, 3689, 3690, 3691, 3692, 3693, 3694] +} + +[sub_resource type="Animation" id="Animation_heal_ch"] +resource_name = "healing_charging" +length = 0.5 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("IncantationSprite:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [589, 590, 591, 592, 593, 594, 595, 596, 597, 598] +} + +[sub_resource type="Animation" id="Animation_heal_rdy"] +resource_name = "healing_ready" +length = 0.5 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("IncantationSprite:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [589, 590, 591, 592, 593, 594, 595, 596, 597, 598] +} + [sub_resource type="Animation" id="Animation_hax0n"] resource_name = "idle" length = 0.1 @@ -363,9 +456,111 @@ _data = { &"RESET": SubResource("Animation_t4otl"), &"fire_charging": SubResource("Animation_j2b1d"), &"fire_ready": SubResource("Animation_cs1tg"), +&"frost_charging": SubResource("Animation_frost_ch"), +&"frost_ready": SubResource("Animation_frost_rdy"), +&"healing_charging": SubResource("Animation_heal_ch"), +&"healing_ready": SubResource("Animation_heal_rdy"), &"idle": SubResource("Animation_hax0n") } +[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_u2ulf"] +streams_count = 4 +stream_0/stream = ExtResource("41_g5jhy") +stream_1/stream = ExtResource("42_holxr") +stream_2/stream = ExtResource("43_mx1m4") +stream_3/stream = ExtResource("44_4gjji") + +[sub_resource type="Animation" id="Animation_g5jhy"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [711] +} + +[sub_resource type="Animation" id="Animation_holxr"] +resource_name = "concussion" +length = 1.0666667 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.06666667, 0.13333333, 0.2, 0.26666668, 0.33333334, 0.4, 0.46666667, 0.53333336, 0.6, 0.6666667, 0.73333335, 0.8, 0.8666667, 0.93333334, 1), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726] +} + +[sub_resource type="Animation" id="Animation_u2ulf"] +resource_name = "idle" +length = 0.46666667 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 1, +"values": [0] +} + +[sub_resource type="Animation" id="Animation_4gjji"] +resource_name = "poison" +length = 0.6 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.06666667, 0.13333334, 0.2, 0.26666668, 0.33333334, 0.4, 0.46666667, 0.53333336), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1), +"update": 1, +"values": [1893, 1894, 1895, 1896, 1897, 1898, 1899, 1900, 1901] +} + +[sub_resource type="Animation" id="Animation_mx1m4"] +resource_name = "sleep" +length = 0.6 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:frame") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.1, 0.2, 0.3, 0.4, 0.5), +"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1), +"update": 1, +"values": [291, 292, 293, 294, 295, 296] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_mx1m4"] +_data = { +&"RESET": SubResource("Animation_g5jhy"), +&"concussion": SubResource("Animation_holxr"), +&"idle": SubResource("Animation_u2ulf"), +&"poison": SubResource("Animation_4gjji"), +&"sleep": SubResource("Animation_mx1m4") +} + [node name="Player" type="CharacterBody2D" unique_id=937429705] collision_mask = 67 motion_mode = 1 @@ -462,6 +657,18 @@ texture = ExtResource("12") hframes = 35 vframes = 8 +[node name="Sprite2DShield" type="Sprite2D" parent="." unique_id=738217548] +material = SubResource("ShaderMaterial_hhpqf") +texture = ExtResource("13_t4otl") +hframes = 35 +vframes = 8 + +[node name="Sprite2DShieldHolding" type="Sprite2D" parent="." unique_id=1000811066] +material = SubResource("ShaderMaterial_hhpqf") +texture = ExtResource("14_j2b1d") +hframes = 35 +vframes = 8 + [node name="Sprite2DWeapon" type="Sprite2D" parent="." unique_id=1889932388] z_index = 1 y_sort_enabled = true @@ -562,7 +769,7 @@ stream = ExtResource("35_bj30b") [node name="SfxSpellIncantation" type="AudioStreamPlayer2D" parent="." unique_id=300820616] stream = ExtResource("36_jc3p3") -volume_db = 5.729 +volume_db = -46.271 attenuation = 7.727487 panning_strength = 1.04 bus = &"Sfx" @@ -576,3 +783,31 @@ frame = 2037 [node name="AnimationIncantation" type="AnimationPlayer" parent="." unique_id=17658820] libraries/ = SubResource("AnimationLibrary_2dvfe") autoplay = &"idle" + +[node name="SfxActivateShield" type="AudioStreamPlayer2D" parent="." unique_id=1247414383] +stream = ExtResource("40_hhpqf") +volume_db = 9.695 +attenuation = 1.3660401 +panning_strength = 1.78 + +[node name="SfxBlockWithShield" type="AudioStreamPlayer2D" parent="." unique_id=1010944313] +stream = SubResource("AudioStreamRandomizer_u2ulf") +volume_db = 7.254 +attenuation = 1.3195078 +panning_strength = 1.06 +bus = &"Sfx" + +[node name="SfxDenyActivateShield" type="AudioStreamPlayer2D" parent="." unique_id=1239738371] +stream = ExtResource("45_g5jhy") +volume_db = 9.458 + +[node name="Sprite2DStatus" type="Sprite2D" parent="." unique_id=1335748461] +position = Vector2(0, -10) +texture = ExtResource("37_hax0n") +hframes = 105 +vframes = 79 +frame = 711 + +[node name="AnimationPlayerStatus" type="AnimationPlayer" parent="Sprite2DStatus" unique_id=721795152] +libraries/ = SubResource("AnimationLibrary_mx1m4") +autoplay = &"idle" diff --git a/src/scripts/attack_bomb.gd b/src/scripts/attack_bomb.gd index ca462a9..ad13fc1 100644 --- a/src/scripts/attack_bomb.gd +++ b/src/scripts/attack_bomb.gd @@ -73,6 +73,9 @@ func _ready(): shadow.modulate = Color(0, 0, 0, 0.5) shadow.z_index = -1 + # Group for sync lookup when collected (multiplayer) + add_to_group("attack_bomb") + # Defer area/shape setup and fuse start – may run during physics (e.g. trap damage → throw) call_deferred("_deferred_ready") @@ -338,12 +341,12 @@ func _deal_explosion_damage(): if body.is_in_group("player") and body.has_method("rpc_take_damage"): var attacker_pos = player_owner.global_position if player_owner else global_position var player_peer_id = body.get_multiplayer_authority() - - if player_peer_id != 0: - if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id(): - body.rpc_take_damage(final_damage, attacker_pos) - else: - body.rpc_take_damage.rpc_id(player_peer_id, final_damage, attacker_pos) + # Avoid "RPC on yourself": call take_damage directly when victim is local peer + if player_peer_id != 0 and player_peer_id == multiplayer.get_unique_id(): + if body.has_method("take_damage"): + body.take_damage(final_damage, attacker_pos) + elif player_peer_id != 0: + body.rpc_take_damage.rpc_id(player_peer_id, final_damage, attacker_pos) else: body.rpc_take_damage.rpc(final_damage, attacker_pos) @@ -353,12 +356,12 @@ func _deal_explosion_damage(): elif body.is_in_group("enemy") and body.has_method("rpc_take_damage"): var attacker_pos = player_owner.global_position if player_owner else global_position 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, false) - else: - body.rpc_take_damage.rpc_id(enemy_peer_id, final_damage, attacker_pos, false, false, false) + # Avoid "RPC on yourself": call take_damage directly when enemy authority is local peer + if enemy_peer_id != 0 and enemy_peer_id == multiplayer.get_unique_id(): + if body.has_method("take_damage"): + body.take_damage(final_damage, attacker_pos, false, false, false) + elif enemy_peer_id != 0: + body.rpc_take_damage.rpc_id(enemy_peer_id, final_damage, attacker_pos, false, false, false) else: body.rpc_take_damage.rpc(final_damage, attacker_pos, false, false, false) @@ -530,6 +533,11 @@ func on_grabbed(by_player): print(by_player.name, " collected bomb!") + # Sync removal to other clients so bomb doesn't keep exploding on their sessions + if multiplayer.has_multiplayer_peer() and by_player and is_instance_valid(by_player): + if by_player.has_method("_rpc_to_ready_peers") and by_player.is_inside_tree(): + by_player._rpc_to_ready_peers("_sync_bomb_collected", [name]) + # Remove bomb immediately queue_free() diff --git a/src/scripts/attack_spell_frostspike.gd b/src/scripts/attack_spell_frostspike.gd new file mode 100644 index 0000000..b2601e6 --- /dev/null +++ b/src/scripts/attack_spell_frostspike.gd @@ -0,0 +1,102 @@ +extends Node2D + +# Frostspike spell — instant damage, no debuff. Frames 4413–4416, blue PointLight2D. +# If is_center: spawn center spike, then 0.5s later spawn 4 adjacent spikes. + +var player_owner: Node = null +var damage: float = 15.0 +var is_center: bool = false +var damage_dealt: bool = false +var elapsed: float = 0.0 +var _frames: Array = [4413, 4414, 4415, 4416] + +@onready var sprite: Sprite2D = $Sprite2D +@onready var spike_light: PointLight2D = $SpikeLight +@onready var hit_area: Area2D = $Area2D + +func _ready() -> void: + if spike_light: + spike_light.color = Color(0.35, 0.6, 1.0) + spike_light.energy = 1.0 + spike_light.enabled = true + if sprite: + sprite.frame = _frames[0] + sprite.modulate = Color(0.6, 0.8, 1.0) + if is_center: + _spawn_adjacent_after_delay() + +func setup(target_pos: Vector2, owner_player: Node, damage_value: float, center: bool) -> void: + global_position = target_pos + player_owner = owner_player + damage = damage_value + is_center = center + +func _spawn_adjacent_after_delay() -> void: + await get_tree().create_timer(0.5).timeout + if not is_instance_valid(self): + return + var gw = get_tree().get_first_node_in_group("game_world") + if not gw or not gw.has_method("_get_adjacent_valid_spell_tile_centers"): + _finish_center_spike() + return + var player_pos = player_owner.global_position if player_owner else global_position + var adjacent = gw._get_adjacent_valid_spell_tile_centers(global_position, player_pos) + var scene = load("res://scenes/attack_spell_frostspike.tscn") as PackedScene + if not scene: + _finish_center_spike() + return + var par = get_parent() + for pos in adjacent: + var sp = scene.instantiate() + par.add_child(sp) + sp.setup(pos, player_owner, damage, false) + _finish_center_spike() + +func _finish_center_spike() -> void: + if hit_area: + hit_area.set_deferred("monitoring", false) + queue_free() + +func _process(delta: float) -> void: + elapsed += delta + if not damage_dealt and player_owner and player_owner.is_multiplayer_authority() and elapsed >= 0.05: + _deal_damage_once() + damage_dealt = true + if sprite and _frames.size() > 0: + var idx = min(int(elapsed / 0.05), _frames.size() - 1) + sprite.frame = _frames[idx] + # Non-center spikes: short lifetime. Center spike freed after spawning adjacent (in coroutine). + if not is_center and elapsed >= 0.25: + if hit_area: + hit_area.set_deferred("monitoring", false) + queue_free() + +func _deal_damage_once() -> void: + if not hit_area: + return + for body in hit_area.get_overlapping_bodies(): + if body == player_owner: + continue + var final_damage = damage + if player_owner and player_owner.character_stats: + var int_stat = player_owner.character_stats.baseStats.int + player_owner.character_stats.get_pass("int") + final_damage += int_stat * 0.5 + var attacker_pos = player_owner.global_position if player_owner else global_position + if body.is_in_group("player") and body.has_method("rpc_take_damage"): + var pid = body.get_multiplayer_authority() + if pid != 0: + if multiplayer.get_unique_id() == pid: + body.take_damage(final_damage, attacker_pos, false, false) + else: + body.rpc_take_damage.rpc_id(pid, final_damage, attacker_pos, false, false) + else: + body.rpc_take_damage.rpc(final_damage, attacker_pos, false, false) + elif body.is_in_group("enemy") and body.has_method("rpc_take_damage"): + var eid = body.get_multiplayer_authority() + if eid != 0: + if multiplayer.get_unique_id() == eid: + body.take_damage(final_damage, attacker_pos, false, false, false) + else: + body.rpc_take_damage.rpc_id(eid, final_damage, attacker_pos, false, false, false) + else: + body.rpc_take_damage.rpc(final_damage, attacker_pos, false, false, false) diff --git a/src/scripts/attack_spell_frostspike.gd.uid b/src/scripts/attack_spell_frostspike.gd.uid new file mode 100644 index 0000000..8eb4f72 --- /dev/null +++ b/src/scripts/attack_spell_frostspike.gd.uid @@ -0,0 +1 @@ +uid://cy3rt80axidqg diff --git a/src/scripts/game_ui.gd b/src/scripts/game_ui.gd index ca6a999..58865e7 100644 --- a/src/scripts/game_ui.gd +++ b/src/scripts/game_ui.gd @@ -26,6 +26,7 @@ var is_hosting: bool = false # Track if we're hosting (don't fetch rooms when h var room_list_container: VBoxContainer = null # Container for displaying available rooms var refresh_button: Button = null # Refresh button for manually reloading rooms var refresh_cooldown_timer: Timer = null # Timer for refresh button cooldown +var active_room_join_button: Button = null # Join button we're currently using (reset on fail) func _ready(): # Wait for nodes to be ready @@ -513,19 +514,47 @@ func _add_room_item(room_code: String, players: int, level: int): # Join button var room_join_button = Button.new() + room_join_button.name = "JoinButton" room_join_button.text = "Join" room_join_button.custom_minimum_size = Vector2(80, 0) - # Connect button to join function - room_join_button.pressed.connect(func(): _join_room(room_code)) + room_join_button.pressed.connect(func(): _join_room(room_code, room_join_button)) room_row.add_child(room_join_button) room_list_container.add_child(room_row) -func _join_room(room_code: String): +func _disable_all_room_join_buttons(): + """Disable all room Join buttons to prevent multiple clicks""" + if not room_list_container: + return + for row in room_list_container.get_children(): + if row.name.begins_with("RoomRow_"): + var btn = row.get_node_or_null("JoinButton") + if btn and is_instance_valid(btn): + btn.disabled = true + +func _reset_room_join_buttons(): + """Re-enable all room Join buttons and restore 'Join' text""" + if not room_list_container: + return + for row in room_list_container.get_children(): + if row.name.begins_with("RoomRow_"): + var btn = row.get_node_or_null("JoinButton") + if btn and is_instance_valid(btn): + btn.disabled = false + btn.text = "Join" + active_room_join_button = null + +func _join_room(room_code: String, room_join_button: Button = null): """Join a room by setting the address and clicking join""" if room_code.is_empty(): return + # Prevent multiple clicks: disable all join buttons and show loader state + _disable_all_room_join_buttons() + if room_join_button and is_instance_valid(room_join_button): + room_join_button.text = "Joining..." + active_room_join_button = room_join_button + # Set the address input if address_input: address_input.text = room_code @@ -534,9 +563,15 @@ func _join_room(room_code: String): var local_count = int(local_players_spinbox.value) network_manager.set_local_player_count(local_count) + is_joining_attempt = true + last_join_address = room_code + # Join the game - if network_manager.join_game(room_code): - LogManager.log("Joining room: " + room_code, LogManager.CATEGORY_UI) + if not network_manager.join_game(room_code): + _reset_room_join_buttons() + is_joining_attempt = false + return + LogManager.log("Joining room: " + room_code, LogManager.CATEGORY_UI) func _on_network_mode_changed(index: int): # On web builds, index 0 = WebRTC, index 1 = WebSocket @@ -648,6 +683,9 @@ func _on_connection_succeeded(): func _on_connection_failed(): LogManager.log("Connection failed", LogManager.CATEGORY_UI) + # Always reset room Join buttons on failure (they may be stuck on "Joining...") + if is_joining_attempt: + _reset_room_join_buttons() if connection_error_shown: # Already shown, don't spam return diff --git a/src/scripts/game_world.gd b/src/scripts/game_world.gd index 98d7d00..e3a0bb3 100644 --- a/src/scripts/game_world.gd +++ b/src/scripts/game_world.gd @@ -1127,6 +1127,31 @@ func _show_loot_floating_text(player: Node, text: String, color: Color, item_tex floating_text.global_position = Vector2(player.global_position.x, player.global_position.y - 20) floating_text.setup(text, color, 0.5, 0.5, item_texture, sprite_hframes, sprite_vframes, sprite_frame) +func _apply_heal_spell_sync(target_name: String, amount: float): + var target: Node = null + for p in get_tree().get_nodes_in_group("player"): + if p.name == target_name and is_instance_valid(p): + target = p + break + if not target: + return + var me = multiplayer.get_unique_id() + var tid = target.get_multiplayer_authority() + if me == tid and target.has_method("heal"): + target.heal(amount) + var entities = get_node_or_null("Entities") + var parent = entities if entities else target.get_parent() + if not parent: + return + var eff_scene = load("res://scenes/healing_effect.tscn") as PackedScene + if eff_scene: + var eff = eff_scene.instantiate() + parent.add_child(eff) + eff.global_position = target.global_position + if eff.has_method("setup"): + eff.setup(target) + _show_loot_floating_text(target, "+" + str(int(amount)) + " HP", Color.GREEN, null, 1, 1, 0) + @rpc("authority", "unreliable") func _sync_enemy_position(enemy_name: String, enemy_index: int, pos: Vector2, vel: Vector2, z_pos: float, dir: int, frame: int, anim: String, frame_num: int, state_value: int): # Clients receive enemy position updates from server @@ -1844,15 +1869,15 @@ func _update_mouse_cursor(delta: float): 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 + var spell_element = "fire" 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 + if "current_spell_element" in player: + spell_element = player.current_spell_element # Update pulse time for grid cursor color animation cursor_pulse_time += delta * CURSOR_PULSE_SPEED @@ -1895,11 +1920,16 @@ func _update_mouse_cursor(delta: float): # 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) + var spell_target_pos = Vector2.ZERO + if spell_element == "healing" and local_players.size() > 0: + var cp = local_players[0] + if cp and is_instance_valid(cp) and cp.has_method("_get_heal_target"): + var ht = cp._get_heal_target() + if ht and is_instance_valid(ht): + spell_target_pos = ht.global_position + else: + spell_target_pos = _get_valid_spell_target_position(world_pos) if spell_target_pos != Vector2.ZERO: spell_cursor_sprite.visible = true # Convert world position to screen position @@ -1912,6 +1942,10 @@ func _update_mouse_cursor(delta: float): match spell_element: "fire": spell_cursor_sprite.modulate = Color(1.0, 0.3, 0.3, 0.5) # Red + "healing": + spell_cursor_sprite.modulate = Color(0.3, 1.0, 0.35, 0.5) # Green + "frost": + spell_cursor_sprite.modulate = Color(0.3, 0.6, 1.0, 0.5) # Blue "water", "ice": spell_cursor_sprite.modulate = Color(0.3, 0.5, 1.0, 0.5) # Blue "electric": @@ -2068,6 +2102,19 @@ func _is_valid_spell_target(target_pos: Vector2, player_pos: Vector2) -> bool: return true +func _get_adjacent_valid_spell_tile_centers(center_world_pos: Vector2, player_pos: Vector2) -> Array: + var out: Array = [] + if not dungeon_tilemap_layer or dungeon_data.is_empty() or not dungeon_data.has("grid"): + return out + var center_tile = dungeon_tilemap_layer.local_to_map(center_world_pos - dungeon_tilemap_layer.global_position) + var offsets = [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)] + for off in offsets: + var t = center_tile + off + var tile_center = dungeon_tilemap_layer.map_to_local(t) + dungeon_tilemap_layer.global_position + if _is_valid_spell_target(tile_center, player_pos): + out.append(tile_center) + return out + func _init_fog_of_war(): if dungeon_data.is_empty() or not dungeon_data.has("map_size"): return @@ -2165,87 +2212,85 @@ func _update_fog_of_war(delta: float) -> void: if local_player_list.size() > 0 and local_player_list[0]: var p_tile = Vector2i(int(local_player_list[0].global_position.x / FOG_TILE_SIZE), int(local_player_list[0].global_position.y / FOG_TILE_SIZE)) current_room = _find_room_at_tile(p_tile) - if not current_room.is_empty(): - _mark_room_explored(current_room) - _mark_room_visible(current_room) - for y in range(map_size.y): - for x in range(map_size.x): - if not _is_tile_in_room_or_walls(Vector2i(x, y), current_room): - var idx = x + y * map_size.x - if idx >= 0 and idx < combined_seen.size(): - combined_seen[idx] = 0 - else: - # In corridors (no room), only show tiles connected to the corridor component - # AND explicitly clear combined_seen for all tiles in rooms that aren't connected - var player_tile = Vector2i(int(local_player_list[0].global_position.x / FOG_TILE_SIZE), int(local_player_list[0].global_position.y / FOG_TILE_SIZE)) - - # Cache corridor data - only rebuild if player moved more than 1 tile - var should_rebuild_corridor = cached_corridor_mask.is_empty() or cached_corridor_player_tile.distance_to(player_tile) > 1 - - var corridor_mask: PackedInt32Array - var corridor_rooms: Array - var allowed_room_ids: Dictionary - - if should_rebuild_corridor: - # Rebuild corridor mask and rooms (expensive operation) - cached_corridor_mask = _build_corridor_mask(player_tile) - cached_corridor_rooms = _get_rooms_connected_to_corridor(cached_corridor_mask, player_tile) - cached_corridor_player_tile = player_tile - - # Build a set of allowed room IDs for fast lookup - cached_corridor_allowed_room_ids = {} - for room in cached_corridor_rooms: - var room_id = str(room.x) + "," + str(room.y) + "," + str(room.w) + "," + str(room.h) - cached_corridor_allowed_room_ids[room_id] = true - - # Use the rebuilt data - corridor_mask = cached_corridor_mask - corridor_rooms = cached_corridor_rooms - allowed_room_ids = cached_corridor_allowed_room_ids - else: - # Use cached data (much faster!) - corridor_mask = cached_corridor_mask - corridor_rooms = cached_corridor_rooms - allowed_room_ids = cached_corridor_allowed_room_ids - - # Check explored rooms and mark them visible - for room in corridor_rooms: - # If this room was previously explored, mark the entire room (including outer walls) as visible - var was_explored = false - for x in range(room.x - 2, room.x + room.w + 2): - for y in range(room.y - 2, room.y + room.h + 2): - if x < 0 or y < 0 or x >= map_size.x or y >= map_size.y: - continue - var idx = x + y * map_size.x - if idx >= 0 and idx < explored_map.size() and explored_map[idx] == 1: - was_explored = true - break - if was_explored: - break - if was_explored: - _mark_room_visible(room) - # Explicitly clear combined_seen for ANY tile not in corridor mask or allowed rooms - # OPTIMIZATION: Only do expensive per-tile checks when corridor state changed or player moved significantly - var needs_tile_clear = corridor_state_changed or should_rebuild_corridor - if needs_tile_clear: + if not current_room.is_empty(): + _mark_room_explored(current_room) + _mark_room_visible(current_room) for y in range(map_size.y): for x in range(map_size.x): - var idx = x + y * map_size.x - if idx < 0 or idx >= combined_seen.size(): - continue - var tile_in_corridor = idx < corridor_mask.size() and corridor_mask[idx] == 1 - # Check if this tile is in a room, and if so, is it an allowed room? - var tile_room = _find_room_at_tile(Vector2i(x, y)) - var in_allowed_room = false - if not tile_room.is_empty(): - var room_id = str(tile_room.x) + "," + str(tile_room.y) + "," + str(tile_room.w) + "," + str(tile_room.h) - in_allowed_room = allowed_room_ids.has(room_id) - # Clear combined_seen for any tile not in corridor or allowed rooms - if not tile_in_corridor and not in_allowed_room: - combined_seen[idx] = 0 - - # Update last corridor fog update time - last_corridor_fog_update = Time.get_ticks_msec() / 1000.0 + if not _is_tile_in_room_or_walls(Vector2i(x, y), current_room): + var idx = x + y * map_size.x + if idx >= 0 and idx < combined_seen.size(): + combined_seen[idx] = 0 + else: + # In corridors (no room), only show tiles connected to the corridor component + # AND explicitly clear combined_seen for all tiles in rooms that aren't connected + var player_tile = Vector2i(int(local_player_list[0].global_position.x / FOG_TILE_SIZE), int(local_player_list[0].global_position.y / FOG_TILE_SIZE)) + + # Cache corridor data - only rebuild if player moved more than 1 tile + var should_rebuild_corridor = cached_corridor_mask.is_empty() or cached_corridor_player_tile.distance_to(player_tile) > 1 + + var corridor_mask: PackedInt32Array + var corridor_rooms: Array + var allowed_room_ids: Dictionary + + if should_rebuild_corridor: + # Rebuild corridor mask and rooms (expensive operation) + cached_corridor_mask = _build_corridor_mask(player_tile) + cached_corridor_rooms = _get_rooms_connected_to_corridor(cached_corridor_mask, player_tile) + cached_corridor_player_tile = player_tile + # Build a set of allowed room IDs for fast lookup + cached_corridor_allowed_room_ids = {} + for room in cached_corridor_rooms: + var room_id = str(room.x) + "," + str(room.y) + "," + str(room.w) + "," + str(room.h) + cached_corridor_allowed_room_ids[room_id] = true + # Use the rebuilt data + corridor_mask = cached_corridor_mask + corridor_rooms = cached_corridor_rooms + allowed_room_ids = cached_corridor_allowed_room_ids + else: + # Use cached data (much faster!) + corridor_mask = cached_corridor_mask + corridor_rooms = cached_corridor_rooms + allowed_room_ids = cached_corridor_allowed_room_ids + + # Check explored rooms and mark them visible + for room in corridor_rooms: + # If this room was previously explored, mark the entire room (including outer walls) as visible + var was_explored = false + for x in range(room.x - 2, room.x + room.w + 2): + for y in range(room.y - 2, room.y + room.h + 2): + if x < 0 or y < 0 or x >= map_size.x or y >= map_size.y: + continue + var idx = x + y * map_size.x + if idx >= 0 and idx < explored_map.size() and explored_map[idx] == 1: + was_explored = true + break + if was_explored: + break + if was_explored: + _mark_room_visible(room) + # Explicitly clear combined_seen for ANY tile not in corridor mask or allowed rooms + # OPTIMIZATION: Only do expensive per-tile checks when corridor state changed or player moved significantly + var needs_tile_clear = corridor_state_changed or should_rebuild_corridor + if needs_tile_clear: + for y in range(map_size.y): + for x in range(map_size.x): + var idx = x + y * map_size.x + if idx < 0 or idx >= combined_seen.size(): + continue + var tile_in_corridor = idx < corridor_mask.size() and corridor_mask[idx] == 1 + # Check if this tile is in a room, and if so, is it an allowed room? + var tile_room = _find_room_at_tile(Vector2i(x, y)) + var in_allowed_room = false + if not tile_room.is_empty(): + var room_id = str(tile_room.x) + "," + str(tile_room.y) + "," + str(tile_room.w) + "," + str(tile_room.h) + in_allowed_room = allowed_room_ids.has(room_id) + # Clear combined_seen for any tile not in corridor or allowed rooms + if not tile_in_corridor and not in_allowed_room: + combined_seen[idx] = 0 + + # Update last corridor fog update time + last_corridor_fog_update = Time.get_ticks_msec() / 1000.0 if fog_node.has_method("set_maps"): fog_node.set_maps(explored_map, combined_seen) @@ -6491,6 +6536,19 @@ func _load_hud(): print("GameWorld: HUD loaded successfully and added to scene tree") print("GameWorld: HUD visible: ", hud.visible, ", layer: ", hud.layer) print("GameWorld: HUD is_inside_tree: ", hud.is_inside_tree()) + + _load_off_screen_indicators() + +func _load_off_screen_indicators(): + var existing = get_node_or_null("OffScreenIndicators") + if existing and is_instance_valid(existing): + return + var scene = load("res://scenes/off_screen_indicators.tscn") as PackedScene + if not scene: + return + var layer = scene.instantiate() + layer.name = "OffScreenIndicators" + add_child(layer) func _initialize_hud(): # Find or get the HUD and reset its level timer diff --git a/src/scripts/healing_effect.gd b/src/scripts/healing_effect.gd new file mode 100644 index 0000000..3f05fbd --- /dev/null +++ b/src/scripts/healing_effect.gd @@ -0,0 +1,97 @@ +extends Node2D + +# Visual effect when a player is healed by Tome of Healing. +# Plays frames 589-598 from shade_spell_effects, green PointLight2D flicker, +# and pulses target player sprites green for a short duration. + +const DURATION: float = 1.2 +var _frames: Array = [589, 590, 591, 592, 593, 594, 595, 596, 597, 598] +const HEAL_PULSE_TINT: Color = Color(0.35, 1.5, 0.4, 1.0) + +var target_player: Node = null +var elapsed: float = 0.0 +var _original_tints: Dictionary = {} + +@onready var fx_sprite: Sprite2D = $FxSprite +@onready var heal_light: PointLight2D = $HealLight + +func _ready() -> void: + pass + +func setup(p_target: Node) -> void: + target_player = p_target + if not target_player or not is_instance_valid(target_player): + queue_free() + return + global_position = target_player.global_position + elapsed = 0.0 + if fx_sprite and _frames.size() > 0: + fx_sprite.frame = _frames[0] + fx_sprite.modulate = Color(0.4, 1.0, 0.5) + if heal_light: + heal_light.color = Color(0.3, 1.0, 0.35) + heal_light.energy = 0.8 + heal_light.enabled = true + +func _process(delta: float) -> void: + if not target_player or not is_instance_valid(target_player): + queue_free() + return + elapsed += delta + global_position = target_player.global_position + # Animate frames 589-598 + if fx_sprite and _frames.size() > 0: + var idx = int((elapsed / DURATION) * float(_frames.size())) % _frames.size() + fx_sprite.frame = _frames[idx] + # Flicker light + if heal_light: + var flicker = 0.7 + 0.4 * sin(elapsed * 18.0) * cos(elapsed * 7.0) + heal_light.energy = clamp(flicker, 0.3, 1.2) + # Pulse green on target sprites + _apply_heal_pulse() + if elapsed >= DURATION: + _clear_heal_pulse() + queue_free() + +func _sprites_for_target() -> Array: + if not target_player: + return [] + var names = ["Sprite2DBody", "Sprite2DBoots", "Sprite2DArmour", "Sprite2DFacialHair", "Sprite2DHair", "Sprite2DEyes", "Sprite2DEyeLashes", "Sprite2DAddons", "Sprite2DHeadgear", "Sprite2DShield", "Sprite2DShieldHolding", "Sprite2DWeapon"] + var out: Array = [] + for n in names: + var node = target_player.get_node_or_null(n) + if node and node is Sprite2D: + out.append({"sprite": node, "name": n}) + return out + +func _apply_heal_pulse() -> void: + var pulse = (sin(elapsed * 12.0) + 1.0) * 0.5 # 0..1 oscillating + for s in _sprites_for_target(): + var sprite: Sprite2D = s.sprite + var key: String = s.name + if not sprite or not is_instance_valid(sprite) or not sprite.material or not (sprite.material is ShaderMaterial): + continue + var sm: ShaderMaterial = sprite.material as ShaderMaterial + if not _original_tints.has(key): + var tp = sm.get_shader_parameter("tint") + if tp is Vector4: + _original_tints[key] = Color(tp.x, tp.y, tp.z, tp.w) + elif tp is Color: + _original_tints[key] = tp + else: + _original_tints[key] = Color.WHITE + var orig: Color = _original_tints[key] + var green_tint = Color(orig.r * HEAL_PULSE_TINT.r, orig.g * HEAL_PULSE_TINT.g, orig.b * HEAL_PULSE_TINT.b, orig.a * HEAL_PULSE_TINT.a) + var cur = orig.lerp(green_tint, pulse * 0.6) + sm.set_shader_parameter("tint", Vector4(cur.r, cur.g, cur.b, cur.a)) + +func _clear_heal_pulse() -> void: + for s in _sprites_for_target(): + var sprite: Sprite2D = s.sprite + var key: String = s.name + if not sprite or not is_instance_valid(sprite) or not sprite.material or not (sprite.material is ShaderMaterial): + continue + if _original_tints.has(key): + var c: Color = _original_tints[key] + (sprite.material as ShaderMaterial).set_shader_parameter("tint", Vector4(c.r, c.g, c.b, c.a)) + _original_tints.clear() diff --git a/src/scripts/healing_effect.gd.uid b/src/scripts/healing_effect.gd.uid new file mode 100644 index 0000000..18ca5dd --- /dev/null +++ b/src/scripts/healing_effect.gd.uid @@ -0,0 +1 @@ +uid://27wuloudfkme diff --git a/src/scripts/interactable_object.gd b/src/scripts/interactable_object.gd index 01b1c1b..832a7ba 100644 --- a/src/scripts/interactable_object.gd +++ b/src/scripts/interactable_object.gd @@ -533,6 +533,7 @@ func _convert_to_bomb_projectile(by_player, force: Vector2): # Spawn bomb projectile at current position var bomb = attack_bomb_scene.instantiate() + bomb.name = "ThrownBomb_" + name get_parent().add_child(bomb) bomb.global_position = current_pos # Use current position, not target @@ -857,6 +858,19 @@ func _open_chest(by_player: Node = null): # Sync chest open visual with item_data so clients can show the floating text var item_data = chest_item.save() if chest_item else {} game_world._rpc_to_ready_peers("_sync_chest_open_by_name", [chest_name, "item", player_peer_id, item_data]) + # Sync inventory+equipment to joiner (server added item; joiner's client must apply) + if multiplayer.is_server(): + var owner_id = by_player.get_multiplayer_authority() + if owner_id != 1 and owner_id != multiplayer.get_unique_id(): + var inv_data: Array = [] + for inv_item in by_player.character_stats.inventory: + inv_data.append(inv_item.save() if inv_item else null) + var equip_data: Dictionary = {} + for slot_name in by_player.character_stats.equipment.keys(): + var eq = by_player.character_stats.equipment[slot_name] + equip_data[slot_name] = eq.save() if eq else null + if by_player.has_method("_apply_inventory_and_equipment_from_server"): + by_player._apply_inventory_and_equipment_from_server.rpc_id(owner_id, inv_data, equip_data) else: push_error("Chest: ERROR - No valid player to give item to!") diff --git a/src/scripts/inventory_ui.gd b/src/scripts/inventory_ui.gd index ef7106b..7c42529 100644 --- a/src/scripts/inventory_ui.gd +++ b/src/scripts/inventory_ui.gd @@ -512,6 +512,7 @@ func _update_ui(): sprite.centered = false # Like inspiration system sprite.position = Vector2(4, 4) # Like inspiration system sprite.scale = Vector2(2.0, 2.0) # 2x size as requested + ItemDatabase.apply_item_colors_to_sprite(sprite, equipped_item) button.add_child(sprite) # Add quantity label if item can have multiple (like arrows, bombs) @@ -594,6 +595,7 @@ func _update_ui(): sprite.centered = false # Like inspiration system sprite.position = Vector2(4, 4) # Like inspiration system sprite.scale = Vector2(2.0, 2.0) # 2x size as requested + ItemDatabase.apply_item_colors_to_sprite(sprite, item) button.add_child(sprite) # Add quantity label if item quantity > 1 (show for all stacked items) diff --git a/src/scripts/item_database.gd b/src/scripts/item_database.gd index 8543299..3696ea7 100644 --- a/src/scripts/item_database.gd +++ b/src/scripts/item_database.gd @@ -163,105 +163,623 @@ static func _load_all_items(): "rarity": ItemRarity.UNCOMMON }) - # HEADGEAR items (row 1) - _register_item("beanie", { - "item_name": "Beanie", - "description": "A warm beanie", + # HEADGEAR items + # MageHatRed (frame 9) variants + var _mhr_o = [Color(255/255.0,39/255.0,44/255.0), Color(182/255.0,0,0), Color(118/255.0,1/255.0,0), Color(72/255.0,0,12/255.0)] + var _mhr_brown = [Color(139/255.0,90/255.0,43/255.0), Color(101/255.0,67/255.0,33/255.0), Color(80/255.0,50/255.0,20/255.0), Color(50/255.0,30/255.0,10/255.0)] + var _mhr_blue = [Color(30/255.0,80/255.0,180/255.0), Color(20/255.0,50/255.0,120/255.0), Color(10/255.0,30/255.0,80/255.0), Color(5/255.0,15/255.0,50/255.0)] + var _mhr_lightblue = [Color(170/255.0,220/255.0,1.0), Color(130/255.0,190/255.0,1.0), Color(90/255.0,150/255.0,220/255.0), Color(50/255.0,100/255.0,180/255.0)] + var _mhr_white = [Color(250/255.0,250/255.0,250/255.0), Color(220/255.0,220/255.0,220/255.0), Color(190/255.0,190/255.0,190/255.0), Color(150/255.0,150/255.0,150/255.0)] + var _shc_o = [Color(0,180/255.0,157/255.0), Color(0,121/255.0,102/255.0), Color(0,79/255.0,67/255.0), Color(0,46/255.0,93/255.0)] + var _shc_red = [Color(180/255.0,40/255.0,40/255.0), Color(130/255.0,0,0), Color(90/255.0,0,0), Color(60/255.0,0,0)] + var _shc_green = [Color(40/255.0,160/255.0,40/255.0), Color(0,120/255.0,0), Color(0,80/255.0,0), Color(0,50/255.0,0)] + var _sb_o = [Color(248/255.0,219/255.0,108/255.0), Color(225/255.0,159/255.0,57/255.0), Color(199/255.0,115/255.0,29/255.0), Color(151/255.0,73/255.0,9/255.0), Color(108/255.0,43/255.0,0), Color(58/255.0,23/255.0,11/255.0), Color(0,76/255.0,218/255.0), Color(9/255.0,35/255.0,105/255.0), Color(0,14/255.0,62/255.0)] + var _sb_iron = [Color(190/255.0,187/255.0,181/255.0), Color(162/255.0,158/255.0,150/255.0), Color(125/255.0,123/255.0,118/255.0), Color(77/255.0,76/255.0,75/255.0), Color(54/255.0,54/255.0,54/255.0), Color(30/255.0,30/255.0,30/255.0), Color(0,76/255.0,218/255.0), Color(9/255.0,35/255.0,105/255.0), Color(0,14/255.0,62/255.0)] + var _sb_steel = [Color(227/255.0,227/255.0,227/255.0), Color(183/255.0,183/255.0,183/255.0), Color(116/255.0,116/255.0,116/255.0), Color(77/255.0,76/255.0,75/255.0), Color(54/255.0,54/255.0,54/255.0), Color(30/255.0,30/255.0,30/255.0), Color(0,76/255.0,218/255.0), Color(9/255.0,35/255.0,105/255.0), Color(0,14/255.0,62/255.0)] + + _register_item("hat", { + "item_name": "Hat", + "description": "A simple cloth hat", "item_type": Item.ItemType.Equippable, "equipment_type": Item.EquipmentType.HEADGEAR, "weapon_type": Item.WeaponType.NONE, - "spriteFrame": 1 * 20 + 10, # 10,1 - "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Headband.png", + "spriteFrame": 9, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/MageHatRed.png", + "modifiers": {"def": 1}, + "buy_cost": 20, + "sell_worth": 6, + "rarity": ItemRarity.COMMON, + "colorReplacements": [{"original": _mhr_o[0], "replace": _mhr_brown[0]}, {"original": _mhr_o[1], "replace": _mhr_brown[1]}, {"original": _mhr_o[2], "replace": _mhr_brown[2]}, {"original": _mhr_o[3], "replace": _mhr_brown[3]}] + }) + _register_item("red_hat", { + "item_name": "Red hat", + "description": "A red mage hat", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 9, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/MageHatRed.png", + "modifiers": {"def": 1}, + "buy_cost": 25, + "sell_worth": 8, + "rarity": ItemRarity.COMMON + }) + _register_item("blue_mages_hat", { + "item_name": "Blue mage's hat", + "description": "A blue mage hat", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 9, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/MageHatRed.png", + "modifiers": {"def": 1}, + "buy_cost": 28, + "sell_worth": 9, + "rarity": ItemRarity.COMMON, + "colorReplacements": [{"original": _mhr_o[0], "replace": _mhr_blue[0]}, {"original": _mhr_o[1], "replace": _mhr_blue[1]}, {"original": _mhr_o[2], "replace": _mhr_blue[2]}, {"original": _mhr_o[3], "replace": _mhr_blue[3]}] + }) + _register_item("wizards_hat", { + "item_name": "Wizard's hat", + "description": "A light blue wizard hat", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 9, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/MageHatRed.png", + "modifiers": {"def": 1, "wis": 1}, + "buy_cost": 55, + "sell_worth": 18, + "rarity": ItemRarity.UNCOMMON, + "colorReplacements": [{"original": _mhr_o[0], "replace": _mhr_lightblue[0]}, {"original": _mhr_o[1], "replace": _mhr_lightblue[1]}, {"original": _mhr_o[2], "replace": _mhr_lightblue[2]}, {"original": _mhr_o[3], "replace": _mhr_lightblue[3]}] + }) + _register_item("gandolfs_hat", { + "item_name": "Gandolf's Hat", + "description": "A white wizard hat", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 9, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/MageHatRed.png", + "modifiers": {"def": 1, "wis": 1}, + "buy_cost": 60, + "sell_worth": 20, + "rarity": ItemRarity.UNCOMMON, + "colorReplacements": [{"original": _mhr_o[0], "replace": _mhr_white[0]}, {"original": _mhr_o[1], "replace": _mhr_white[1]}, {"original": _mhr_o[2], "replace": _mhr_white[2]}, {"original": _mhr_o[3], "replace": _mhr_white[3]}] + }) + + _register_item("sorcerors_hood", { + "item_name": "Sorceror's Hood", + "description": "A cyan sorceror hood", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 27, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/SorcererHoodCyan.png", + "modifiers": {"def": 2, "wis": 1}, + "buy_cost": 50, + "sell_worth": 16, + "rarity": ItemRarity.COMMON + }) + _register_item("red_hood", { + "item_name": "Red Hood", + "description": "A red hood", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 27, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/SorcererHoodCyan.png", + "modifiers": {"def": 2}, + "buy_cost": 45, + "sell_worth": 14, + "rarity": ItemRarity.COMMON, + "colorReplacements": [{"original": _shc_o[0], "replace": _shc_red[0]}, {"original": _shc_o[1], "replace": _shc_red[1]}, {"original": _shc_o[2], "replace": _shc_red[2]}, {"original": _shc_o[3], "replace": _shc_red[3]}] + }) + _register_item("green_hood", { + "item_name": "Green Hood", + "description": "A green hood", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 27, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/SorcererHoodCyan.png", + "modifiers": {"def": 2, "dex": 1}, + "buy_cost": 52, + "sell_worth": 17, + "rarity": ItemRarity.COMMON, + "colorReplacements": [{"original": _shc_o[0], "replace": _shc_green[0]}, {"original": _shc_o[1], "replace": _shc_green[1]}, {"original": _shc_o[2], "replace": _shc_green[2]}, {"original": _shc_o[3], "replace": _shc_green[3]}] + }) + + _register_item("high_mage_hat", { + "item_name": "High Mage Hat", + "description": "A high mage hat", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 5, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/HighMageHatCyan.png", + "modifiers": {"def": 2, "wis": 1}, + "buy_cost": 70, + "sell_worth": 22, + "rarity": ItemRarity.UNCOMMON + }) + _register_item("esper_hat", { + "item_name": "Esper Hat", + "description": "An esper hat", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 4, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/EsperHatBlue.png", + "modifiers": {"def": 2, "wis": 1}, + "buy_cost": 65, + "sell_worth": 20, + "rarity": ItemRarity.UNCOMMON + }) + + _register_item("jarl_helm", { + "item_name": "Jarl Helm", + "description": "A jarl's helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 26, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Viking/JarlHelm.png", + "modifiers": {"def": 4, "str": 1}, + "buy_cost": 150, + "sell_worth": 48, + "rarity": ItemRarity.RARE + }) + _register_item("karl_helm", { + "item_name": "Karl Helm", + "description": "A karl's helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 25, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Viking/KarlHelm.png", + "modifiers": {"def": 3}, + "buy_cost": 90, + "sell_worth": 28, + "rarity": ItemRarity.UNCOMMON + }) + _register_item("valkyrie_helm", { + "item_name": "Valkyrie Helm", + "description": "A valkyrie helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 24, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Viking/ValkyrieHelm.png", + "modifiers": {"def": 4}, + "buy_cost": 130, + "sell_worth": 40, + "rarity": ItemRarity.RARE + }) + _register_item("warrior_helm", { + "item_name": "Warrior Helm", + "description": "A warrior helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 23, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Viking/WarriorHelmRed.png", + "modifiers": {"def": 3, "str": 1}, + "buy_cost": 100, + "sell_worth": 32, + "rarity": ItemRarity.UNCOMMON + }) + _register_item("enforced_warrior_helm", { + "item_name": "Enforced Warrior Helm", + "description": "A reinforced warrior helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 22, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Viking/WarriorHelmYellow.png", + "modifiers": {"def": 4, "str": 1}, + "buy_cost": 140, + "sell_worth": 44, + "rarity": ItemRarity.RARE + }) + + _register_item("cherbi_helm", { + "item_name": "Cherbi Helm", + "description": "A cherbi helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 49, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Mongol/CherbiHelm.png", + "modifiers": {"def": 3}, + "buy_cost": 85, + "sell_worth": 26, + "rarity": ItemRarity.UNCOMMON + }) + _register_item("khaan_helm", { + "item_name": "Khaan Helm", + "description": "A khaan's helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 48, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Mongol/KhaanHelm.png", + "modifiers": {"def": 5, "str": 1}, + "buy_cost": 180, + "sell_worth": 56, + "rarity": ItemRarity.RARE + }) + _register_item("kheshig_helm", { + "item_name": "Kheshig Helm", + "description": "A kheshig helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 47, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Mongol/KheshigHelm.png", + "modifiers": {"def": 4}, + "buy_cost": 120, + "sell_worth": 38, + "rarity": ItemRarity.RARE + }) + _register_item("noyon_helm", { + "item_name": "Noyon Helm", + "description": "A noyon helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 46, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Mongol/NoyonHelm.png", + "modifiers": {"def": 4}, + "buy_cost": 115, + "sell_worth": 36, + "rarity": ItemRarity.UNCOMMON + }) + + _register_item("daimyo_helm", { + "item_name": "Daimyo Helm", + "description": "A daimyo's helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 69, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/DaimyoHelm.png", + "modifiers": {"def": 5, "str": 1}, + "buy_cost": 190, + "sell_worth": 60, + "rarity": ItemRarity.RARE + }) + _register_item("blue_ninja_bandana", { + "item_name": "Blue Ninja Bandana", + "description": "A blue ninja bandana", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 68, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/NinjaBandanaBlue.png", + "modifiers": {"def": 1, "dex": 1}, + "buy_cost": 40, + "sell_worth": 13, + "rarity": ItemRarity.COMMON + }) + _register_item("ronin_strawhat", { + "item_name": "Ronin Straw-hat", + "description": "A ronin's straw hat", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 67, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/RoninStrawhatBlue.png", + "modifiers": {"def": 1}, + "buy_cost": 22, + "sell_worth": 7, + "rarity": ItemRarity.COMMON + }) + _register_item("samurai_helm", { + "item_name": "Samurai Helm", + "description": "A samurai helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 66, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/SamuraiHelm.png", + "modifiers": {"def": 4, "str": 1}, + "buy_cost": 145, + "sell_worth": 46, + "rarity": ItemRarity.RARE + }) + _register_item("shogun_helm", { + "item_name": "Shogun Helm", + "description": "A shogun's helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 65, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/ShogunHelmPurple.png", + "modifiers": {"def": 5, "str": 1}, + "buy_cost": 200, + "sell_worth": 64, + "rarity": ItemRarity.RARE + }) + _register_item("straw_hat", { + "item_name": "Straw-hat", + "description": "A simple straw hat", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 64, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/TravellerStrawhat.png", + "modifiers": {"def": 1}, + "buy_cost": 18, + "sell_worth": 6, + "rarity": ItemRarity.COMMON + }) + _register_item("travelers_strawhat", { + "item_name": "Traveler's Straw-hat", + "description": "A traveler's straw hat", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 63, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/TravellerStrawhatCyan.png", "modifiers": {"def": 1}, "buy_cost": 20, "sell_worth": 6, "rarity": ItemRarity.COMMON }) - - _register_item("leather_helm", { - "item_name": "Leather Helm", - "description": "A nice leather helm", + _register_item("villagers_strawhat", { + "item_name": "Villager's Straw-hat", + "description": "A villager's straw hat", "item_type": Item.ItemType.Equippable, "equipment_type": Item.EquipmentType.HEADGEAR, "weapon_type": Item.WeaponType.NONE, - "spriteFrame": 1 * 20 + 11, # 11,1 - "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/NoviceHelm.png", - "modifiers": {"def": 2}, - "buy_cost": 50, - "sell_worth": 15, + "spriteFrame": 62, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/VillagerStrawhat.png", + "modifiers": {"def": 1}, + "buy_cost": 16, + "sell_worth": 5, "rarity": ItemRarity.COMMON }) - - _register_item("nomads_helm", { - "item_name": "Nomad's Helm", - "description": "A helm for travelers", + _register_item("yabusame_strawhat", { + "item_name": "Yabusame Straw-hat", + "description": "A yabusame straw hat", "item_type": Item.ItemType.Equippable, "equipment_type": Item.EquipmentType.HEADGEAR, "weapon_type": Item.WeaponType.NONE, - "spriteFrame": 1 * 20 + 12, # 12,1 - "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RangerHatGreen.png", + "spriteFrame": 61, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Japanese/YabusameStrawhatBlue.png", + "modifiers": {"def": 1}, + "buy_cost": 24, + "sell_worth": 8, + "rarity": ItemRarity.COMMON + }) + + _register_item("musketeers_hat", { + "item_name": "Musketeer's Hat", + "description": "A musketeer's hat", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 60, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/French/MusketeerHatPurple.png", + "modifiers": {"def": 2}, + "buy_cost": 55, + "sell_worth": 18, + "rarity": ItemRarity.UNCOMMON + }) + + _register_item("archers_hat", { + "item_name": "Archer's Hat", + "description": "An archer's hat", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 89, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/ArcherHatCyan.png", "modifiers": {"def": 2, "dex": 1}, - "buy_cost": 60, + "buy_cost": 58, "sell_worth": 18, "rarity": ItemRarity.COMMON }) - - _register_item("strong_helm", { - "item_name": "Strong Helm", - "description": "A reinforced helm", + _register_item("hunters_hat", { + "item_name": "Hunter's Hat", + "description": "A hunter's hat", "item_type": Item.ItemType.Equippable, "equipment_type": Item.EquipmentType.HEADGEAR, "weapon_type": Item.WeaponType.NONE, - "spriteFrame": 1 * 20 + 13, # 13,1 - "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/KnightHelm.png", - "modifiers": {"def": 3}, - "buy_cost": 80, - "sell_worth": 24, - "rarity": ItemRarity.UNCOMMON + "spriteFrame": 88, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/HunterHatRed.png", + "modifiers": {"def": 2, "dex": 1}, + "buy_cost": 60, + "sell_worth": 19, + "rarity": ItemRarity.COMMON }) - - _register_item("plate_helm", { - "item_name": "Plate Helm", - "description": "Heavy plate helm", + _register_item("rogues_hat", { + "item_name": "Rogue's Hat", + "description": "A rogue's hat", "item_type": Item.ItemType.Equippable, "equipment_type": Item.EquipmentType.HEADGEAR, "weapon_type": Item.WeaponType.NONE, - "spriteFrame": 1 * 20 + 14, # 14,1 - "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierSteelHelmBlue.png", - "modifiers": {"def": 4}, - "buy_cost": 120, - "sell_worth": 36, - "rarity": ItemRarity.UNCOMMON + "spriteFrame": 87, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Range/RogueHatGreen.png", + "modifiers": {"def": 2, "dex": 1}, + "buy_cost": 62, + "sell_worth": 20, + "rarity": ItemRarity.COMMON }) - - _register_item("warriors_helm", { - "item_name": "Warrior's Helm", - "description": "A helm for true warriors", + + _register_item("dark_knight_helm", { + "item_name": "Dark Knight Helm", + "description": "A dark knight helm", "item_type": Item.ItemType.Equippable, "equipment_type": Item.EquipmentType.HEADGEAR, "weapon_type": Item.WeaponType.NONE, - "spriteFrame": 1 * 20 + 15, # 15,1 - "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/DragonKnightHelm.png", + "spriteFrame": 29, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/DarkKnightHelm.png", "modifiers": {"def": 5, "str": 1}, - "buy_cost": 180, - "sell_worth": 54, + "buy_cost": 175, + "sell_worth": 55, "rarity": ItemRarity.RARE }) - + _register_item("dragon_knight_helm", { + "item_name": "Dragon Knight Helm", + "description": "A dragon knight helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 21, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/DragonKnightHelm.png", + "modifiers": {"def": 5, "str": 1}, + "buy_cost": 185, + "sell_worth": 58, + "rarity": ItemRarity.RARE + }) + _register_item("grunt_helm", { + "item_name": "Grunt Helm", + "description": "A grunt helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 28, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/GruntHelm.png", + "modifiers": {"def": 3}, + "buy_cost": 75, + "sell_worth": 24, + "rarity": ItemRarity.COMMON + }) + _register_item("knight_helm", { + "item_name": "Knight Helm", + "description": "A knight helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 20, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/KnightHelm.png", + "modifiers": {"def": 4}, + "buy_cost": 110, + "sell_worth": 34, + "rarity": ItemRarity.UNCOMMON + }) + _register_item("novice_helm", { + "item_name": "Novice Helm", + "description": "A novice helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 45, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/NoviceHelm.png", + "modifiers": {"def": 2}, + "buy_cost": 45, + "sell_worth": 14, + "rarity": ItemRarity.COMMON + }) _register_item("paladins_helm", { "item_name": "Paladin's Helm", "description": "A blessed paladin helm", "item_type": Item.ItemType.Equippable, "equipment_type": Item.EquipmentType.HEADGEAR, "weapon_type": Item.WeaponType.NONE, - "spriteFrame": 1 * 20 + 16, # 16,1 + "spriteFrame": 44, "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/PaladinHelmCyan.png", "modifiers": {"def": 5, "wis": 1}, "buy_cost": 200, "sell_worth": 60, "rarity": ItemRarity.RARE }) - + _register_item("scout_helm", { + "item_name": "Scout Helm", + "description": "A scout helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 43, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/ScoutHelmGreen.png", + "modifiers": {"def": 3, "dex": 1}, + "buy_cost": 95, + "sell_worth": 30, + "rarity": ItemRarity.UNCOMMON + }) + _register_item("soldier_bronze_helm", { + "item_name": "Soldier Bronze Helm", + "description": "A bronze soldier helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 42, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierBronzeHelmBlue.png", + "modifiers": {"def": 4}, + "buy_cost": 105, + "sell_worth": 32, + "rarity": ItemRarity.UNCOMMON + }) + _register_item("soldier_iron_helm", { + "item_name": "Soldier Iron Helm", + "description": "An iron soldier helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 42, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierBronzeHelmBlue.png", + "modifiers": {"def": 4}, + "buy_cost": 110, + "sell_worth": 34, + "rarity": ItemRarity.UNCOMMON, + "colorReplacements": [{"original": _sb_o[0], "replace": _sb_iron[0]}, {"original": _sb_o[1], "replace": _sb_iron[1]}, {"original": _sb_o[2], "replace": _sb_iron[2]}, {"original": _sb_o[3], "replace": _sb_iron[3]}, {"original": _sb_o[4], "replace": _sb_iron[4]}, {"original": _sb_o[5], "replace": _sb_iron[5]}, {"original": _sb_o[6], "replace": _sb_iron[6]}, {"original": _sb_o[7], "replace": _sb_iron[7]}, {"original": _sb_o[8], "replace": _sb_iron[8]}] + }) + _register_item("soldier_steel_helm", { + "item_name": "Soldier Steel Helm", + "description": "A steel soldier helm", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 42, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Melee/SoldierBronzeHelmBlue.png", + "modifiers": {"def": 4}, + "buy_cost": 125, + "sell_worth": 38, + "rarity": ItemRarity.UNCOMMON, + "colorReplacements": [{"original": _sb_o[0], "replace": _sb_steel[0]}, {"original": _sb_o[1], "replace": _sb_steel[1]}, {"original": _sb_o[2], "replace": _sb_steel[2]}, {"original": _sb_o[3], "replace": _sb_steel[3]}, {"original": _sb_o[4], "replace": _sb_steel[4]}, {"original": _sb_o[5], "replace": _sb_steel[5]}, {"original": _sb_o[6], "replace": _sb_steel[6]}, {"original": _sb_o[7], "replace": _sb_steel[7]}, {"original": _sb_o[8], "replace": _sb_steel[8]}] + }) + + _register_item("assassin_bandana", { + "item_name": "Assassin Bandana", + "description": "An assassin bandana", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 6, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/AssassinBandanaBlack.png", + "modifiers": {"def": 1, "dex": 1}, + "buy_cost": 48, + "sell_worth": 15, + "rarity": ItemRarity.COMMON + }) + _register_item("stalker_hood", { + "item_name": "Stalker Hood", + "description": "A stalker hood", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 7, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/StalkerHoodBlack.png", + "modifiers": {"def": 2, "dex": 1}, + "buy_cost": 72, + "sell_worth": 22, + "rarity": ItemRarity.UNCOMMON + }) + _register_item("thief_bandana", { + "item_name": "Thief Bandana", + "description": "A thief bandana", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.HEADGEAR, + "weapon_type": Item.WeaponType.NONE, + "spriteFrame": 8, + "equipmentPath": "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Assassin/ThiefBandanaGreen.png", + "modifiers": {"def": 1, "dex": 1}, + "buy_cost": 42, + "sell_worth": 13, + "rarity": ItemRarity.COMMON + }) + # ACCESSORY items (row 1) _register_item("amulet_of_strength", { "item_name": "Amulet of Strength", @@ -1219,7 +1737,21 @@ static func _load_all_items(): }) # SPELLBOOKS (row 11, columns 13-14) - # Sprite 233 = 11 * 20 + 13 + # Sprite 233 = 11 * 20 + 13 — same base as Tome of Healing, blue colorReplacements + var _tf_o = [ + Color(225.0/255.0, 130.0/255.0, 137.0/255.0), + Color(174.0/255.0, 108.0/255.0, 55.0/255.0), + Color(245.0/255.0, 183.0/255.0, 132.0/255.0), + Color(130.0/255.0, 60.0/255.0, 61.0/255.0), + Color(197.0/255.0, 151.0/255.0, 130.0/255.0) + ] + var _tf_blue = [ + Color(0.35, 0.6, 0.95), + Color(0.2, 0.4, 0.75), + Color(0.5, 0.75, 1.0), + Color(0.15, 0.35, 0.6), + Color(0.4, 0.6, 0.85) + ] _register_item("tome_of_frostspike", { "item_name": "Tome of Frostspike", "description": "A spellbook containing frost magic", @@ -1233,7 +1765,11 @@ static func _load_all_items(): "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 + {"original": _tf_o[0], "replace": _tf_blue[0]}, + {"original": _tf_o[1], "replace": _tf_blue[1]}, + {"original": _tf_o[2], "replace": _tf_blue[2]}, + {"original": _tf_o[3], "replace": _tf_blue[3]}, + {"original": _tf_o[4], "replace": _tf_blue[4]} ] }) @@ -1254,6 +1790,42 @@ static func _load_all_items(): {"old_color": Color(1.0, 1.0, 1.0), "new_color": Color(1.0, 0.8, 0.6)} # Warm orange tint for fire ] }) + + # Tome of Healing - frame 233 (11*20+13), green colorReplacements + var _th_o = [ + Color(225.0/255.0, 130.0/255.0, 137.0/255.0), + Color(174.0/255.0, 108.0/255.0, 55.0/255.0), + Color(245.0/255.0, 183.0/255.0, 132.0/255.0), + Color(130.0/255.0, 60.0/255.0, 61.0/255.0), + Color(197.0/255.0, 151.0/255.0, 130.0/255.0) + ] + var _th_green = [ + Color(0.35, 0.85, 0.4), + Color(0.2, 0.6, 0.25), + Color(0.5, 0.95, 0.55), + Color(0.15, 0.5, 0.2), + Color(0.4, 0.75, 0.45) + ] + _register_item("tome_of_healing", { + "item_name": "Tome of Healing", + "description": "A spellbook containing healing magic", + "item_type": Item.ItemType.Equippable, + "equipment_type": Item.EquipmentType.OFFHAND, + "weapon_type": Item.WeaponType.SPELLBOOK, + "spriteFrame": 233, + "modifiers": {}, + "buy_cost": 100, + "sell_worth": 30, + "weight": 1.5, + "rarity": ItemRarity.UNCOMMON, + "colorReplacements": [ + {"original": _th_o[0], "replace": _th_green[0]}, + {"original": _th_o[1], "replace": _th_green[1]}, + {"original": _th_o[2], "replace": _th_green[2]}, + {"original": _th_o[3], "replace": _th_green[3]}, + {"original": _th_o[4], "replace": _th_green[4]} + ] + }) # BOMB item (sprite index 199 = row 9, col 19) _register_item("bomb", { @@ -1443,3 +2015,35 @@ static func get_random_container_item() -> Item: rarity = ItemRarity.RARE return get_random_item_by_rarity(rarity) + +# Ensure sprite uses cloth shader and apply item colorReplacements/tint (same as loot + character sprites). +# Use for inventory UI, loot, or any item icon sprite. +static func apply_item_colors_to_sprite(sprite: Sprite2D, item: Item) -> void: + if not sprite: + return + var shader_res = load("res://shaders/cloth.gdshader") as Shader + if not shader_res: + return + var mat: ShaderMaterial + if sprite.material is ShaderMaterial: + mat = sprite.material as ShaderMaterial + if mat.shader != shader_res: + mat = ShaderMaterial.new() + mat.shader = shader_res + sprite.material = mat + else: + mat = ShaderMaterial.new() + mat.shader = shader_res + sprite.material = mat + for i in range(7): + mat.set_shader_parameter("original_" + str(i), Color(0, 0, 0, 0)) + mat.set_shader_parameter("replace_" + str(i), Color(0, 0, 0, 0)) + mat.set_shader_parameter("tint", Color(1, 1, 1, 1)) + if item and item.colorReplacements and item.colorReplacements.size() > 0: + for idx in range(item.colorReplacements.size()): + if idx >= 7: + break + var cr: Dictionary = item.colorReplacements[idx] + if cr.get("original") != null and cr.get("replace") != null: + mat.set_shader_parameter("original_" + str(idx), cr["original"] as Color) + mat.set_shader_parameter("replace_" + str(idx), cr["replace"] as Color) diff --git a/src/scripts/loot.gd b/src/scripts/loot.gd index f420ae2..f5f3736 100644 --- a/src/scripts/loot.gd +++ b/src/scripts/loot.gd @@ -8,7 +8,7 @@ enum LootType { BANANA, CHERRY, KEY, - ITEM # Item instance (equipment, consumables, etc.) + ITEM # Item instance (equipment, consumables, etc.) } @export var loot_type: LootType = LootType.COIN @@ -41,7 +41,7 @@ var correction_smoothing: float = 0.3 # Lerp factor for smooth correction (0-1, var coin_value: int = 1 var heal_amount: float = 20.0 var collected: bool = false -var item: Item = null # Item instance (for LootType.ITEM) +var item: Item = null # Item instance (for LootType.ITEM) @onready var sprite = $Sprite2D @onready var shadow = $Shadow @@ -152,6 +152,7 @@ func _setup_sprite(): sprite.hframes = item.spriteFrames.x if item.spriteFrames.x > 0 else 20 sprite.vframes = item.spriteFrames.y if item.spriteFrames.y > 0 else 14 sprite.frame = item.spriteFrame + ItemDatabase.apply_item_colors_to_sprite(sprite, item) print("Loot: Set up item sprite for ", item.item_name, " frame=", sprite.frame) # Add quantity badge if quantity > 1 @@ -194,8 +195,8 @@ func _create_quantity_badge(quantity: int): quantity_badge.add_theme_color_override("font_color", Color.WHITE) quantity_badge.add_theme_color_override("font_outline_color", Color.BLACK) quantity_badge.add_theme_constant_override("outline_size", 2) - quantity_badge.z_index = 100 # Above the sprite - quantity_badge.position = Vector2(6, -8) # Bottom right of sprite + quantity_badge.z_index = 100 # Above the sprite + quantity_badge.position = Vector2(6, -8) # Bottom right of sprite add_child(quantity_badge) func _physics_process(delta): @@ -240,7 +241,7 @@ func _physics_process(delta): bounce_timer = 0.08 # Matches old code timing # Simple bounce (matches old code) - velocity_z = -velocity_z * bounce_restitution + velocity_z = - velocity_z * bounce_restitution is_airborne = true # Still bouncing else: # Velocity too small or collected - stop bouncing @@ -267,9 +268,9 @@ func _physics_process(delta): if collider and not collider.is_in_group("player"): # Check if velocity is too small before bouncing (prevent infinite micro-bounces) var velocity_magnitude = velocity.length() - if velocity_magnitude < 15.0: # If velocity is very small, stop bouncing + if velocity_magnitude < 15.0: # If velocity is very small, stop bouncing velocity = Vector2.ZERO - continue # Skip bounce and sound + continue # Skip bounce and sound # Bounce off walls (matches old code - no aggressive velocity reduction) var normal = collision.get_normal() @@ -329,7 +330,7 @@ func _physics_process(delta): sfx_coin_bounce.volume_db = -1.0 + (-10.0 - (abs(velocity_z) * 0.1)) sfx_coin_bounce.play() bounce_timer = 0.08 - velocity_z = -velocity_z * bounce_restitution + velocity_z = - velocity_z * bounce_restitution is_airborne = true else: velocity_z = 0.0 @@ -405,7 +406,7 @@ func _on_pickup_area_body_entered(body): var dropped_by_peer_id = get_meta("dropped_by_peer_id") var drop_time = get_meta("drop_time") var current_time = Time.get_ticks_msec() - var time_since_drop = (current_time - drop_time) / 1000.0 # Convert to seconds + var time_since_drop = (current_time - drop_time) / 1000.0 # Convert to seconds # Check if this player dropped the item and cooldown hasn't expired if body.has_method("get_multiplayer_authority"): @@ -465,7 +466,6 @@ func _pickup(player: Node): func _process_pickup_on_server(player: Node): # Internal function to process pickup on server (called from _request_pickup RPC) # This skips the authority check since we've already validated the request - # Mark as collected immediately to prevent duplicate pickups # (Note: This may already be set by _request_pickup, but set it here too for safety) if not collected: @@ -624,16 +624,30 @@ func _process_pickup_on_server(player: Node): player.character_stats.add_item(item) print(name, " picked up item: ", item.item_name, " (added to inventory)") + # Sync inventory+equipment to joiner (server added to their copy; joiner's client must apply) + if multiplayer.has_multiplayer_peer() and multiplayer.is_server(): + var owner_id = player.get_multiplayer_authority() + if owner_id != 1 and owner_id != multiplayer.get_unique_id(): + var inv_data: Array = [] + for inv_item in player.character_stats.inventory: + inv_data.append(inv_item.save() if inv_item else null) + var equip_data: Dictionary = {} + for slot_name in player.character_stats.equipment.keys(): + var eq = player.character_stats.equipment[slot_name] + equip_data[slot_name] = eq.save() if eq else null + if player.has_method("_apply_inventory_and_equipment_from_server"): + player._apply_inventory_and_equipment_from_server.rpc_id(owner_id, inv_data, equip_data) + # Show floating text with item name (uppercase) var items_texture = load(item.spritePath) - var display_text = item.item_name.to_upper() # Always uppercase + var display_text = item.item_name.to_upper() # Always uppercase var text_color = Color.WHITE # Color code based on item type if item.item_type == Item.ItemType.Equippable: - text_color = Color.CYAN # Cyan for equipment + text_color = Color.CYAN # Cyan for equipment elif item.item_type == Item.ItemType.Restoration: - text_color = Color.GREEN # Green for consumables + text_color = Color.GREEN # Green for consumables _show_floating_text(player, display_text, text_color, 0.5, 0.5, items_texture, item.spriteFrames.x, item.spriteFrames.y, item.spriteFrame) @@ -650,7 +664,7 @@ func _process_pickup_on_server(player: Node): await sfx_loot_collect.finished queue_free() -var processing_pickup: bool = false # Mutex to prevent concurrent pickup processing +var processing_pickup: bool = false # Mutex to prevent concurrent pickup processing @rpc("any_peer", "reliable") func _request_pickup(player_peer_id: int): @@ -666,7 +680,7 @@ func _request_pickup(player_peer_id: int): var dropped_by_peer_id = get_meta("dropped_by_peer_id") var drop_time = get_meta("drop_time") var current_time = Time.get_ticks_msec() - var time_since_drop = (current_time - drop_time) / 1000.0 # Convert to seconds + var time_since_drop = (current_time - drop_time) / 1000.0 # Convert to seconds if player_peer_id == dropped_by_peer_id and time_since_drop < 5.0: # Player can't pick up their own dropped item for 5 seconds @@ -719,7 +733,7 @@ func _sync_remove(): # Clients remove loot when any player picks it up # Only process if we're not the authority (i.e., we're a client) if multiplayer.is_server(): - return # Server ignores its own updates + return # Server ignores its own updates print("Loot: Client received removal sync for loot at ", global_position) @@ -760,7 +774,7 @@ func _sync_remove(): func _sync_show_floating_text(loot_type_value: int, text: String, color_value: Color, _value: int, sprite_frame_value: int, player_peer_id: int): # Client receives floating text sync from server if multiplayer.is_server(): - return # Server ignores this (it's the sender) + return # Server ignores this (it's the sender) # Find player by peer ID var player = null @@ -771,7 +785,7 @@ func _sync_show_floating_text(loot_type_value: int, text: String, color_value: C break if not player or not is_instance_valid(player): - return # Can't find player + return # Can't find player # Determine texture and parameters based on loot type var item_texture: Texture2D = null diff --git a/src/scripts/off_screen_indicators.gd b/src/scripts/off_screen_indicators.gd new file mode 100644 index 0000000..3bab46e --- /dev/null +++ b/src/scripts/off_screen_indicators.gd @@ -0,0 +1,183 @@ +extends Control + +# Off-screen player indicators: show other players at the edge of the screen when outside camera. +# Uses the same colors as chat (player_colors by peer_id). + +# Match chat_ui.gd player colors exactly +var _player_colors: Array = [ + Color.RED, + Color.GREEN, + Color.BLUE, + Color.ORANGE, + Color(0.5, 0.0, 0.5) # Purple +] + +const INDICATOR_RADIUS: float = 8.0 +const EDGE_MARGIN: float = 24.0 # Inset from viewport edge so circles stay visible +const NAME_FONT_SIZE: int = 12 +const NAME_OFFSET: float = 14.0 # Gap between circle and name + +var _game_world: Node = null +var _camera: Camera2D = null +var _local_player_set: Dictionary = {} # player instance id -> true +var _indicators: Array = [] # { position: Vector2, color: Color, name: String, direction: Vector2 } +var _font: Font = null + +func _ready() -> void: + _update_size() + if get_viewport(): + get_viewport().size_changed.connect(_update_size) + _game_world = get_tree().get_first_node_in_group("game_world") + if _game_world and _game_world.has_node("Camera2D"): + _camera = _game_world.get_node("Camera2D") + _font = ThemeDB.fallback_font + mouse_filter = Control.MOUSE_FILTER_IGNORE + +func _update_size() -> void: + var vp = get_viewport() + if not vp: + return + var rect = vp.get_visible_rect() + position = rect.position + size = rect.size + custom_minimum_size = rect.size + +func _process(_delta: float) -> void: + _indicators.clear() + if not _game_world or not _camera or not multiplayer or not multiplayer.has_multiplayer_peer(): + queue_redraw() + return + + var vp = get_viewport() + if not vp: + return + var vp_rect = vp.get_visible_rect() + var vp_size = vp_rect.size + var center = vp_rect.get_center() + + # Local players (don't show indicators for them) + _local_player_set.clear() + var local_players: Array = [] + if _game_world.has_node("PlayerManager"): + var pm = _game_world.get_node("PlayerManager") + if pm.has_method("get_local_players"): + local_players = pm.get_local_players() + for p in local_players: + if is_instance_valid(p): + _local_player_set[p.get_instance_id()] = true + + var zoom_x: float = _camera.zoom.x if _camera.zoom.x > 0.0 else 1.0 + + for player in get_tree().get_nodes_in_group("player"): + if not is_instance_valid(player): + continue + if _local_player_set.has(player.get_instance_id()): + continue + if "is_dead" in player and player.is_dead: + continue + if not player.has_method("get_multiplayer_authority"): + continue + + var world_pos: Vector2 = player.global_position + var screen_pos: Vector2 = (world_pos - _camera.global_position) * zoom_x + center + + # Check if on screen (with small margin) + var m = EDGE_MARGIN + if screen_pos.x >= m and screen_pos.x <= vp_size.x - m and screen_pos.y >= m and screen_pos.y <= vp_size.y - m: + continue + + var edge_pos: Vector2 = _clamp_to_viewport_edge(center, screen_pos, vp_rect) + var peer_id: int = player.get_multiplayer_authority() + var col: Color = _player_colors[peer_id % _player_colors.size()] + var dir: Vector2 = (edge_pos - center).normalized() + var pname: String = player.name + if player.get("character_stats") and player.character_stats: + var cn: String = player.character_stats.character_name + if not cn.is_empty(): + pname = cn + _indicators.append({ "position": edge_pos, "color": col, "name": pname, "direction": dir }) + + queue_redraw() + +func _clamp_to_viewport_edge(center: Vector2, point: Vector2, vp_rect: Rect2) -> Vector2: + var sz = vp_rect.size + var mx = EDGE_MARGIN + var cx = center.x + var cy = center.y + var px = point.x + var py = point.y + var dx = px - cx + var dy = py - cy + var len_sq = dx * dx + dy * dy + if len_sq < 0.0001: + return center + var inv = 1.0 / sqrt(len_sq) + var ux = dx * inv + var uy = dy * inv + + # Intersect ray center + t * (ux, uy) with inset rect [mx, my] to [w-mx, h-mx] + var w = sz.x + var h = sz.y + var t_min = INF + var hit = center + + # Left edge x = mx + if abs(ux) > 0.0001: + var t = (mx - cx) / ux + if t > 0: + var y = cy + t * uy + if y >= mx and y <= h - mx: + if t < t_min: + t_min = t + hit = Vector2(mx, y) + # Right edge x = w - mx + if abs(ux) > 0.0001: + var t = (w - mx - cx) / ux + if t > 0: + var y = cy + t * uy + if y >= mx and y <= h - mx: + if t < t_min: + t_min = t + hit = Vector2(w - mx, y) + # Top edge y = mx + if abs(uy) > 0.0001: + var t = (mx - cy) / uy + if t > 0: + var x = cx + t * ux + if x >= mx and x <= w - mx: + if t < t_min: + t_min = t + hit = Vector2(x, mx) + # Bottom edge y = h - mx + if abs(uy) > 0.0001: + var t = (h - mx - cy) / uy + if t > 0: + var x = cx + t * ux + if x >= mx and x <= w - mx: + if t < t_min: + t_min = t + hit = Vector2(x, h - mx) + + return hit + +func _draw() -> void: + for d in _indicators: + var pos: Vector2 = d["position"] + var col: Color = d["color"] + var pname: String = d["name"] + var dir: Vector2 = d["direction"] + # Slight dark outline so visible on any background + draw_circle(pos, INDICATOR_RADIUS + 1.0, Color(0.15, 0.15, 0.15, 0.7)) + draw_circle(pos, INDICATOR_RADIUS, col) + # Player name inward from circle, same color as indicator + if _font and not pname.is_empty(): + var text_anchor: Vector2 = pos - dir * (INDICATOR_RADIUS + NAME_OFFSET) + var ts: Vector2 = _font.get_string_size(pname, HORIZONTAL_ALIGNMENT_LEFT, -1, NAME_FONT_SIZE) + var base_y: float = text_anchor.y + var text_pos: Vector2 + if dir.x >= 0: + text_pos = Vector2(text_anchor.x - ts.x, base_y) + else: + text_pos = Vector2(text_anchor.x, base_y) + draw_string_outline(_font, text_pos, pname, HORIZONTAL_ALIGNMENT_LEFT, -1, NAME_FONT_SIZE, 1, Color(0.1, 0.1, 0.1, 0.9)) + draw_string(_font, text_pos, pname, HORIZONTAL_ALIGNMENT_LEFT, -1, NAME_FONT_SIZE, col) diff --git a/src/scripts/off_screen_indicators.gd.uid b/src/scripts/off_screen_indicators.gd.uid new file mode 100644 index 0000000..d30e79a --- /dev/null +++ b/src/scripts/off_screen_indicators.gd.uid @@ -0,0 +1 @@ +uid://j468bxsdgknu diff --git a/src/scripts/player.gd b/src/scripts/player.gd index 4de9182..1087f3e 100644 --- a/src/scripts/player.gd +++ b/src/scripts/player.gd @@ -41,6 +41,7 @@ var just_grabbed_this_frame = false # Prevents immediate release bug - persists var grab_released_while_lifting = false # Track if grab was released while lifting (for drop-on-second-press logic) var grab_start_time = 0.0 # Time when we first grabbed (to detect quick tap) var grab_tap_threshold = 0.2 # Seconds to distinguish tap from hold +var is_shielding: bool = false # True when holding grab with shield in offhand and nothing to grab/lift var last_movement_direction = Vector2.DOWN # Track last direction for placing objects var push_axis = Vector2.ZERO # Locked axis for pushing/pulling var push_direction_locked: int = Direction.DOWN # Locked facing direction when pushing @@ -86,6 +87,7 @@ var bow_charge_tint_pulse_time: float = 0.0 # Timer for pulsing tint animation var bow_charge_tint_pulse_speed_charged: float = 20.0 # Speed of tint pulse animation when fully charged var original_sprite_tints: Dictionary = {} # Store original tint values for restoration var spell_incantation_played: bool = false # Track if incantation sound has been played +var current_spell_element: String = "fire" # "fire" or "healing" for cursor/tint var burn_debuff_timer: float = 0.0 # Timer for burn debuff var burn_debuff_duration: float = 5.0 # Burn lasts 5 seconds var burn_debuff_damage_per_second: float = 1.0 # 1 HP per second @@ -96,6 +98,10 @@ var direction_lock_timer: float = 0.0 # Lock facing direction when attacking var locked_facing_direction: Vector2 = Vector2.ZERO # Locked facing direction during attack var damage_direction_lock_timer: float = 0.0 # Lock facing when taking damage (enemies/players) var damage_direction_lock_duration: float = 0.3 # Duration to block direction change after damage +var shield_block_cooldown_timer: float = 0.0 # After blocking, cannot block again until this reaches 0 +var shield_block_cooldown_duration: float = 1.2 # Seconds before can block again +var shield_block_direction: Vector2 = Vector2.DOWN # Facing direction when we started blocking (locked) +var was_shielding_last_frame: bool = false # For detecting shield activate transition var empty_bow_shot_attempts: int = 0 var _arrow_spawn_seq: int = 0 # Per-player sequence for stable arrow names (multiplayer sync) var sword_slash_scene = preload("res://scenes/sword_slash.tscn") # Old rotating version (kept for reference) @@ -103,6 +109,8 @@ var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn") # New var staff_projectile_scene = preload("res://scenes/attack_staff.tscn") # Staff magic ball projectile var 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 frostspike_spell_scene = preload("res://scenes/attack_spell_frostspike.tscn") # Frostspike for Tome of Frostspike +var healing_effect_scene = preload("res://scenes/healing_effect.tscn") # Visual effect for Tome of Healing var attack_bomb_scene = preload("res://scenes/attack_bomb.tscn") # Bomb projectile var blood_scene = preload("res://scenes/blood_clot.tscn") @@ -135,6 +143,8 @@ var is_airborne: bool = false @onready var sprite_eyelashes = $Sprite2DEyeLashes @onready var sprite_addons = $Sprite2DAddons @onready var sprite_headgear = $Sprite2DHeadgear +@onready var sprite_shield = $Sprite2DShield +@onready var sprite_shield_holding = $Sprite2DShieldHolding @onready var sprite_weapon = $Sprite2DWeapon @onready var cone_light = $ConeLight @@ -478,6 +488,10 @@ func _duplicate_sprite_materials(): sprite_headgear.material = sprite_headgear.material.duplicate() if sprite_weapon and sprite_weapon.material: sprite_weapon.material = sprite_weapon.material.duplicate() + if sprite_shield and sprite_shield.material: + sprite_shield.material = sprite_shield.material.duplicate() + if sprite_shield_holding and sprite_shield_holding.material: + sprite_shield_holding.material = sprite_shield_holding.material.duplicate() func _initialize_character_stats(): # Create character_stats if it doesn't exist @@ -693,12 +707,15 @@ func _setup_player_appearance(): character_stats.equipment["offhand"] = starting_bomb print("Dwarf player ", name, " spawned with 5 bombs") - # Give Human race starting spellbook (Tome of Flames) + # Give Human race (Wizard) starting spellbook (Tome of Flames) and Hat if selected_race == "Human": 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") + var starting_hat = ItemDatabase.create_item("hat") + if starting_hat: + character_stats.equipment["headgear"] = starting_hat + print("Human player ", name, " spawned with Tome of Flames and Hat") # Randomize skin (human only for players) # Weighted random: Human1 has highest chance, Human7 has lowest chance @@ -1129,6 +1146,8 @@ func _apply_appearance_to_sprites(): _apply_weapon_color_replacements(sprite_weapon, equipped_weapon) else: _clear_weapon_color_replacements(sprite_weapon) + + _update_shield_visibility() # Appearance applied (verbose logging removed) @@ -1458,6 +1477,10 @@ func _update_animation(delta): sprite_addons.frame = frame_index if sprite_headgear: sprite_headgear.frame = frame_index + if sprite_shield: + sprite_shield.frame = frame_index + if sprite_shield_holding: + sprite_shield_holding.frame = frame_index # Update weapon sprite - use BOW_STRING animation if charging bow if sprite_weapon: @@ -1511,6 +1534,10 @@ func _update_facing_from_mouse(mouse_direction: Vector2): if is_pushing: return + # Don't update if shielding (locked block direction) + if is_shielding: + return + # Don't update if direction is locked (during attack) if direction_lock_timer > 0.0: return @@ -1699,7 +1726,7 @@ func _update_z_physics(delta): # Apply to all sprite layers for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair, sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons, - sprite_headgear, sprite_weapon]: + sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon]: if sprite_layer: sprite_layer.position.y = y_offset if position_z > 0: @@ -1729,6 +1756,28 @@ func _physics_process(delta): if is_airborne: _update_z_physics(delta) + # Spell charge visuals (incantation, tint pulse) - run for ALL players so others see the pulse + if is_charging_spell: + var charge_time = (Time.get_ticks_msec() / 1000.0) - spell_charge_start_time + var charge_progress = clamp(charge_time / spell_charge_duration, 0.0, 1.0) + spell_charge_particle_timer += delta + _update_spell_charge_particles(charge_progress) + _update_spell_charge_incantation(charge_progress) + if charge_progress >= 1.0: + spell_charge_tint_pulse_time += delta * spell_charge_tint_pulse_speed_charged + _apply_spell_charge_tint() + if not spell_incantation_played and has_node("SfxSpellIncantation"): + $SfxSpellIncantation.play() + spell_incantation_played = true + else: + spell_charge_tint_pulse_time = 0.0 + _clear_spell_charge_tint() + if has_node("SfxSpellIncantation"): + $SfxSpellIncantation.stop() + spell_incantation_played = false + else: + spell_charge_tint_pulse_time = 0.0 + if is_local_player and is_multiplayer_authority(): # Skip all input and logic if dead if is_dead: @@ -1759,37 +1808,10 @@ func _physics_process(delta): if damage_direction_lock_timer <= 0.0: damage_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_spell_charge_incantation(charge_progress) - - # Update tint pulse timer when fully charged - if charge_progress >= 1.0: - # Use much faster pulse speed when fully charged - spell_charge_tint_pulse_time += delta * spell_charge_tint_pulse_speed_charged - _apply_spell_charge_tint() - - # Play incantation sound when fully charged (only once) - if not spell_incantation_played: - if has_node("SfxSpellIncantation"): - $SfxSpellIncantation.play() - spell_incantation_played = true - else: - spell_charge_tint_pulse_time = 0.0 - _clear_spell_charge_tint() - # Stop incantation if not fully charged - if has_node("SfxSpellIncantation"): - $SfxSpellIncantation.stop() - spell_incantation_played = false - else: - # Reset pulse timer when not charging - spell_charge_tint_pulse_time = 0.0 + if shield_block_cooldown_timer > 0.0: + shield_block_cooldown_timer -= delta + if shield_block_cooldown_timer <= 0.0: + shield_block_cooldown_timer = 0.0 # Update bow charge tint (when fully charged) if is_charging_bow: @@ -1881,9 +1903,15 @@ func _physics_process(delta): break if being_held_by_someone: + is_shielding = false + was_shielding_last_frame = false + _update_shield_visibility() # Handle struggle mechanic _handle_struggle(delta) elif is_knocked_back: + is_shielding = false + was_shielding_last_frame = false + _update_shield_visibility() # During knockback, no input control - just let velocity carry the player # Apply friction to slow down knockback velocity = velocity.lerp(Vector2.ZERO, delta * 8.0) @@ -1895,6 +1923,9 @@ func _physics_process(delta): _handle_movement(delta) _handle_interactions() else: + is_shielding = false + was_shielding_last_frame = false + _update_shield_visibility() # Reset struggle when airborne struggle_time = 0.0 struggle_direction = Vector2.ZERO @@ -2100,8 +2131,10 @@ func _handle_input(): # Update full 360-degree facing direction for attacks (gamepad/keyboard input) # Only update if mouse control is not active (i.e., mouse is outside window or using gamepad) - # 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: + # Don't update if direction is locked (during attack) or shielding + if is_shielding: + facing_direction_vector = shield_block_direction + elif not is_pushing and (not mouse_control_active or input_device != -1) and direction_lock_timer <= 0.0: facing_direction_vector = input_vector.normalized() elif direction_lock_timer > 0.0 and locked_facing_direction.length() > 0.0: # Use locked direction during attack @@ -2109,8 +2142,13 @@ func _handle_input(): # Update facing direction for animations (except when pushing - locked direction) # Only update from movement input if mouse control is not active or using gamepad - # 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: + # Don't update if direction is locked (during attack) or shielding + if is_shielding: + var new_direction = _get_direction_from_vector(shield_block_direction) as Direction + if new_direction != current_direction: + current_direction = new_direction + _update_cone_light_rotation() + elif not is_pushing and (not mouse_control_active or input_device != -1) and direction_lock_timer <= 0.0: var new_direction = _get_direction_from_vector(input_vector) as Direction # Update direction and cone light rotation if changed @@ -2164,6 +2202,12 @@ func _handle_input(): if push_direction_locked != current_direction: current_direction = push_direction_locked as Direction _update_cone_light_rotation() + elif is_shielding: + # Keep locked block direction when shielding and idle + var new_direction = _get_direction_from_vector(shield_block_direction) as Direction + if new_direction != current_direction: + current_direction = new_direction + _update_cone_light_rotation() else: 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") @@ -2192,6 +2236,7 @@ func _handle_input(): # Reduce speed by half when pushing/pulling # Reduce speed by 50% when charging bow # Reduce speed by 80% when charging spell (20% speed) + # Reduce speed to 60% when shielding # Calculate speed with encumbrance penalty var speed_multiplier = 1.0 if is_pushing: @@ -2200,6 +2245,8 @@ func _handle_input(): speed_multiplier = 0.5 elif is_charging_spell: speed_multiplier = 0.2 # 20% speed (80% reduction) + elif is_shielding: + speed_multiplier = 0.6 # 60% speed when blocking with shield var base_speed = move_speed * speed_multiplier var current_speed = base_speed @@ -2262,6 +2309,25 @@ func _handle_interactions(): else: grab_just_released = false + # Update is_shielding: hold grab with shield in offhand and nothing to grab/lift + var would_shield = (not is_dead and grab_button_down and _has_shield_in_offhand() \ + and not held_object and not is_lifting and not is_pushing \ + and not _has_nearby_grabbable() and not is_disarming) + if would_shield and shield_block_cooldown_timer > 0.0: + is_shielding = false + if has_node("SfxDenyActivateShield"): + $SfxDenyActivateShield.play() + elif would_shield: + if not was_shielding_last_frame: + shield_block_direction = facing_direction_vector.normalized() if facing_direction_vector.length() > 0.1 else last_movement_direction.normalized() + if has_node("SfxActivateShield"): + $SfxActivateShield.play() + is_shielding = true + else: + is_shielding = false + was_shielding_last_frame = is_shielding + _update_shield_visibility() + # Cancel bow charging if grab is pressed if grab_just_pressed and is_charging_bow: is_charging_bow = false @@ -2275,17 +2341,23 @@ func _handle_interactions(): print(name, " cancelled bow charge") - # Check for spell casting (with Tome of Flames) - # Handle spell charging (Tome of Flames) + # Check for spell casting (Tome of Flames, Frostspike, or Healing) 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 is_fire = offhand_item.item_name == "Tome of Flames" + var is_frost = offhand_item.item_name == "Tome of Frostspike" + var is_heal = offhand_item.item_name == "Tome of Healing" + if is_fire or is_frost or is_heal: var game_world = get_tree().get_first_node_in_group("game_world") var target_pos = Vector2.ZERO - if game_world and game_world.has_method("get_grid_locked_cursor_position"): + var heal_target: Node = null + if (is_fire or is_frost) and game_world and game_world.has_method("get_grid_locked_cursor_position"): target_pos = game_world.get_grid_locked_cursor_position() + elif is_heal: + heal_target = _get_heal_target() + + var has_valid_target = ((is_fire or is_frost) and target_pos != Vector2.ZERO) or (is_heal and heal_target != null) # Check if there's a grabbable object nearby - prioritize grabbing over spell casting var nearby_grabbable = null @@ -2307,133 +2379,95 @@ func _handle_interactions(): 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: + if grab_just_pressed and not is_charging_spell and has_valid_target and not nearby_grabbable and not is_lifting and not held_object: is_charging_spell = true + current_spell_element = "healing" if is_heal else ("frost" if is_frost else "fire") spell_charge_start_time = Time.get_ticks_msec() / 1000.0 - spell_incantation_played = false # Reset flag when starting new charge + spell_incantation_played = false _start_spell_charge_particles() _start_spell_charge_incantation() - - # 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 + print(name, " started charging spell (", current_spell_element, ")") 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 + current_spell_element = "fire" spell_incantation_played = false _stop_spell_charge_particles() _stop_spell_charge_incantation() _clear_spell_charge_tint() - - # Return to IDLE animation _set_animation("IDLE") - - # 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)") + print(name, " cancelled spell (released too quickly)") 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"): + if (is_fire or is_frost) and game_world and game_world.has_method("get_grid_locked_cursor_position"): target_pos = game_world.get_grid_locked_cursor_position() + if is_heal: + heal_target = _get_heal_target() + has_valid_target = ((is_fire or is_frost) and target_pos != Vector2.ZERO) or (is_heal and heal_target != null) - # Cast spell if fully charged (will find valid position if target is blocked) - if 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 + if has_valid_target and is_fully_charged: + if is_fire: + _cast_flame_spell(target_pos) + elif is_frost: + _cast_frostspike_spell(target_pos) + else: + _cast_heal_spell(heal_target) _set_animation("FINISH_SPELL") - - # Stop charging and clear tint (but let incantation sound finish) is_charging_spell = false + current_spell_element = "fire" spell_incantation_played = false _stop_spell_charge_particles() _stop_spell_charge_incantation() - _clear_spell_charge_tint() # This will restore original tints - - # Stop spell charging sound, but let incantation play to completion + _clear_spell_charge_tint() if has_node("SfxSpellCharge"): $SfxSpellCharge.stop() - # Don't stop SfxSpellIncantation - let it finish playing - - # Sync spell charge end to other clients - if multiplayer.has_multiplayer_peer(): - _sync_spell_charge_end.rpc() else: - # 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 + print(name, " spell not cast (charge: ", charge_time, "s, fully: ", is_fully_charged, ", target ok: ", has_valid_target, ")") is_charging_spell = false + current_spell_element = "fire" spell_incantation_played = false _stop_spell_charge_particles() _stop_spell_charge_incantation() - _clear_spell_charge_tint() # This will restore original tints - - # Return to IDLE animation + _clear_spell_charge_tint() _set_animation("IDLE") - - # 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 + print(name, " released spell (", "healing" if is_heal else ("frost" if is_frost else "fire"), ", fully: ", is_fully_charged, ")") 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): + elif is_charging_spell and (not has_valid_target or is_lifting or held_object): is_charging_spell = false + current_spell_element = "fire" spell_incantation_played = false _stop_spell_charge_particles() _stop_spell_charge_incantation() _clear_spell_charge_tint() - - # Return to IDLE animation _set_animation("IDLE") - - # 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) @@ -2784,6 +2818,44 @@ func _get_nearby_disarmable_trap() -> Node: return null +func _has_shield_in_offhand() -> bool: + if not character_stats or not character_stats.equipment.has("offhand"): + return false + var off = character_stats.equipment["offhand"] + return off != null and "shield" in off.item_name.to_lower() + +func _has_nearby_grabbable() -> bool: + if not grab_area: + return false + var bodies = grab_area.get_overlapping_bodies() + for body in bodies: + if body == self: + continue + var is_grabbable = false + if body.has_method("can_be_grabbed"): + if body.can_be_grabbed(): + is_grabbable = true + elif body.is_in_group("player") or ("peer_id" in body and body is CharacterBody2D): + is_grabbable = true + if is_grabbable and position.distance_to(body.position) < grab_range: + return true + return false + +func _update_shield_visibility() -> void: + if not sprite_shield or not sprite_shield_holding: + return + var has_shield = _has_shield_in_offhand() + if not has_shield: + sprite_shield.visible = false + sprite_shield_holding.visible = false + return + if is_shielding: + sprite_shield.visible = false + sprite_shield_holding.visible = true + else: + sprite_shield.visible = true + sprite_shield_holding.visible = false + func _try_grab(): if not grab_area: return @@ -3026,6 +3098,9 @@ func reset_grab_state(): grab_start_time = 0.0 grab_released_while_lifting = false was_dragging_last_frame = false + is_shielding = false + was_shielding_last_frame = false + _update_shield_visibility() # Reset to idle animation if current_animation == "IDLE_HOLD" or current_animation == "RUN_HOLD" or current_animation == "LIFT" or current_animation == "IDLE_PUSH" or current_animation == "RUN_PUSH": @@ -3411,6 +3486,7 @@ func _place_down_object(): if not attack_bomb_scene: return var bomb = attack_bomb_scene.instantiate() + bomb.name = "PlacedBomb_" + bomb_name get_parent().add_child(bomb) bomb.global_position = place_pos bomb.setup(place_pos, self, Vector2.ZERO, false) # not thrown, fuse starts in _deferred_ready @@ -3813,8 +3889,10 @@ func _place_bomb(target_position: Vector2): print(name, " cannot place bomb - no valid target position") return - # Spawn bomb at target position + # Unique id for sync (collect/remove on other clients) + var bomb_id = "DirectBomb_%d_%d" % [Time.get_ticks_msec(), get_multiplayer_authority()] var bomb = attack_bomb_scene.instantiate() + bomb.name = bomb_id get_parent().add_child(bomb) bomb.global_position = valid_target_pos @@ -3827,7 +3905,7 @@ func _place_bomb(target_position: Vector2): # Sync bomb spawn to other clients if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): - _rpc_to_ready_peers("_sync_place_bomb", [valid_target_pos]) + _rpc_to_ready_peers("_sync_place_bomb", [bomb_id, valid_target_pos]) print(name, " placed bomb!") @@ -3889,6 +3967,128 @@ func _sync_flame_spell(target_position: Vector2, spell_damage: float): print(name, " (synced) spawned flame spell at ", target_position) +func _cast_frostspike_spell(target_position: Vector2): + if not frostspike_spell_scene: + return + if not is_multiplayer_authority(): + return + var game_world = get_tree().get_first_node_in_group("game_world") + var valid_target_pos = target_position + if game_world and game_world.has_method("_get_valid_spell_target_position"): + var found_pos = game_world._get_valid_spell_target_position(target_position) + if found_pos != Vector2.ZERO: + valid_target_pos = found_pos + else: + print(name, " cannot cast frostspike - no valid target position") + return + var spell_damage = 15.0 + if character_stats: + spell_damage = character_stats.damage * 0.75 + var frost = frostspike_spell_scene.instantiate() + frost.setup(valid_target_pos, self, spell_damage, true) + get_parent().add_child(frost) + if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree(): + _rpc_to_ready_peers("_sync_frostspike_spell", [valid_target_pos, spell_damage]) + print(name, " cast frostspike at ", valid_target_pos) + +@rpc("any_peer", "reliable") +func _sync_frostspike_spell(target_position: Vector2, spell_damage: float): + if is_multiplayer_authority(): + return + if not frostspike_spell_scene: + return + var frost = frostspike_spell_scene.instantiate() + frost.setup(target_position, self, spell_damage, true) + get_parent().add_child(frost) + print(name, " (synced) spawned frostspike at ", target_position) + +func _cast_heal_spell(target: Node): + if not target or not is_instance_valid(target): + return + if not character_stats: + return + var int_val = character_stats.baseStats.int + character_stats.get_pass("int") + var base_heal = 10.0 + var amount = base_heal + int_val * 0.5 + amount = max(1.0, floor(amount)) + var cap = 0.0 + if target.character_stats: + cap = target.character_stats.maxhp - target.character_stats.hp + amount = min(amount, max(0.0, cap)) + if amount <= 0: + return + var me = multiplayer.get_unique_id() + var tid = target.get_multiplayer_authority() + if me == tid: + target.heal(amount) + _spawn_heal_effect_and_text(target, amount) + if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree(): + var gw = get_tree().get_first_node_in_group("game_world") + if gw and gw.has_method("_sync_heal_spell"): + _rpc_to_ready_peers("_sync_heal_spell_via_gw", [target.name, amount]) + print(name, " cast heal on ", target.name, " for ", int(amount), " HP") + +func _spawn_heal_effect_and_text(target: Node, amount: float): + if not target or not is_instance_valid(target): + return + var game_world = get_tree().get_first_node_in_group("game_world") + var entities = game_world.get_node_or_null("Entities") if game_world else null + var parent = entities if entities else target.get_parent() + if not parent: + return + if healing_effect_scene: + var eff = healing_effect_scene.instantiate() + parent.add_child(eff) + eff.global_position = target.global_position + if eff.has_method("setup"): + eff.setup(target) + var floating_text_scene = preload("res://scenes/floating_text.tscn") + if floating_text_scene: + var ft = floating_text_scene.instantiate() + parent.add_child(ft) + ft.global_position = Vector2(target.global_position.x, target.global_position.y - 20) + ft.setup("+" + str(int(amount)) + " HP", Color.GREEN, 0.5, 0.5, null, 1, 1, 0) + +@rpc("any_peer", "reliable") +func _sync_heal_spell_via_gw(target_name: String, amount: float): + if is_multiplayer_authority(): + return + var gw = get_tree().get_first_node_in_group("game_world") + if gw and gw.has_method("_apply_heal_spell_sync"): + gw._apply_heal_spell_sync(target_name, amount) + +func _is_healing_spell() -> bool: + if not character_stats or not character_stats.equipment.has("offhand"): + return false + var off = character_stats.equipment["offhand"] + return off != null and off.item_name == "Tome of Healing" + +func _is_frost_spell() -> bool: + if not character_stats or not character_stats.equipment.has("offhand"): + return false + var off = character_stats.equipment["offhand"] + return off != null and off.item_name == "Tome of Frostspike" + +func _get_heal_target() -> Node: + var game_world = get_tree().get_first_node_in_group("game_world") + if not game_world or not game_world.has_node("Camera2D"): + return null + var cam = game_world.get_node("Camera2D") + var mouse_world = cam.get_global_mouse_position() + const HEAL_RANGE: float = 56.0 + var best: Node = null + var best_d: float = HEAL_RANGE + for p in get_tree().get_nodes_in_group("player"): + if not is_instance_valid(p): + continue + if "is_dead" in p and p.is_dead: + continue + var d = p.global_position.distance_to(mouse_world) + if d < best_d: + best_d = d + best = p + return best + func _can_cast_spell_at(target_position: Vector2) -> bool: # Check if spell can be cast at target position # Must be on floor tile and not blocked by walls @@ -4024,17 +4224,25 @@ func _stop_spell_charge_particles(): spell_charge_particles = null func _start_spell_charge_incantation(): - # Play fire_charging on AnimationIncantation when starting spell charge spell_incantation_fire_ready_shown = false if has_node("AnimationIncantation"): - $AnimationIncantation.play("fire_charging") + if _is_healing_spell(): + $AnimationIncantation.play("healing_charging") + elif _is_frost_spell(): + $AnimationIncantation.play("frost_charging") + else: + $AnimationIncantation.play("fire_charging") func _update_spell_charge_incantation(charge_progress: float): - # Switch to fire_ready when fully charged (fire_charging already playing from start) if not has_node("AnimationIncantation"): return if charge_progress >= 1.0 and not spell_incantation_fire_ready_shown: - $AnimationIncantation.play("fire_ready") + if _is_healing_spell(): + $AnimationIncantation.play("healing_ready") + elif _is_frost_spell(): + $AnimationIncantation.play("frost_ready") + else: + $AnimationIncantation.play("fire_ready") spell_incantation_fire_ready_shown = true func _stop_spell_charge_incantation(): @@ -4044,12 +4252,13 @@ func _stop_spell_charge_incantation(): $AnimationIncantation.play("idle") 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 tint = spell_charge_tint + if _is_healing_spell(): + tint = Color(0.25, 2.0, 0.3, 2.0) # Green pulse for healing + elif _is_frost_spell(): + tint = Color(0.25, 0.5, 2.0, 2.0) # Blue pulse for frost var sprites = [ {"sprite": sprite_body, "name": "body"}, {"sprite": sprite_boots, "name": "boots"}, @@ -4098,12 +4307,12 @@ func _apply_spell_charge_tint(): # Get original tint var original_tint = original_sprite_tints.get(tint_key, Color.WHITE) - # Calculate fully charged tint (original * spell_charge_tint) + # Calculate fully charged tint (original * tint) var full_charged_tint = Color( - 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 + original_tint.r * tint.r, + original_tint.g * tint.g, + original_tint.b * tint.b, + original_tint.a * tint.a ) # Interpolate between original and fully charged tint based on pulse @@ -4285,15 +4494,13 @@ func _sync_spell_charge_start(): @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 + current_spell_element = "fire" spell_incantation_played = false _stop_spell_charge_particles() _stop_spell_charge_incantation() _clear_spell_charge_tint() - - # Return to IDLE animation _set_animation("IDLE") # Stop spell charging sounds @@ -4741,6 +4948,7 @@ func _sync_bomb_dropped(bomb_name: String, place_pos: Vector2): if not attack_bomb_scene: return var bomb = attack_bomb_scene.instantiate() + bomb.name = "PlacedBomb_" + bomb_name get_parent().add_child(bomb) bomb.global_position = place_pos bomb.setup(place_pos, self, Vector2.ZERO, false) # not thrown, fuse lit @@ -4749,20 +4957,17 @@ func _sync_bomb_dropped(bomb_name: String, place_pos: Vector2): print(name, " (synced) dropped bomb at ", place_pos) @rpc("any_peer", "reliable") -func _sync_place_bomb(target_pos: Vector2): +func _sync_place_bomb(bomb_id: String, target_pos: Vector2): # Sync bomb placement to other clients (Human/Elf) if not is_multiplayer_authority(): if not attack_bomb_scene: return - # Spawn bomb at target position var bomb = attack_bomb_scene.instantiate() + bomb.name = bomb_id get_parent().add_child(bomb) bomb.global_position = target_pos - - # Setup bomb without throw (placed directly) bomb.setup(target_pos, self, Vector2.ZERO, false) # false = not thrown - print(name, " (synced) placed bomb at ", target_pos) @rpc("any_peer", "reliable") @@ -4778,6 +4983,7 @@ func _sync_throw_bomb(bomb_name: String, bomb_pos: Vector2, throw_force: Vector2 if not attack_bomb_scene: return var bomb = attack_bomb_scene.instantiate() + bomb.name = "ThrownBomb_" + bomb_name get_parent().add_child(bomb) bomb.global_position = bomb_pos bomb.setup(bomb_pos, self, throw_force, true) # true = is_thrown @@ -4785,6 +4991,16 @@ func _sync_throw_bomb(bomb_name: String, bomb_pos: Vector2, throw_force: Vector2 bomb.get_node("Sprite2D").visible = true print(name, " (synced) threw bomb from ", bomb_pos) +@rpc("any_peer", "reliable") +func _sync_bomb_collected(bomb_name: String): + # Another peer collected this bomb – remove our copy so it doesn't keep exploding + # Collector already removed and added to inventory locally; we just free our instance + var bombs = get_tree().get_nodes_in_group("attack_bomb") + for b in bombs: + if b.name == bomb_name and is_instance_valid(b): + b.queue_free() + return + @rpc("any_peer", "reliable") func _sync_throw(obj_name: String, throw_pos: Vector2, force: Vector2, thrower_name: String): # Sync throw to all clients (RPC sender already threw on their side) @@ -5358,6 +5574,28 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool _rpc_to_ready_peers("_sync_damage", [0.0, attacker_position, false, false, true]) # is_dodged = true return # No damage taken, exit early + # Check for shield block (would have hit; enemy attack from blocked direction; no burn) + if not is_burn_damage and is_shielding and shield_block_cooldown_timer <= 0.0: + var dir_to_attacker = (attacker_position - global_position).normalized() + if dir_to_attacker.length() < 0.01: + dir_to_attacker = Vector2.RIGHT + var block_dir = shield_block_direction.normalized() if shield_block_direction.length() > 0.01 else Vector2.DOWN + var dot = block_dir.dot(dir_to_attacker) + if dot > 0.5: # Lenient: attacker in front (~60° cone) + # Blocked: no damage, small knockback, BLOCKED notification, cooldown + shield_block_cooldown_timer = shield_block_cooldown_duration + var direction_from_attacker = (global_position - attacker_position).normalized() + velocity = direction_from_attacker * 90.0 # Small knockback + is_knocked_back = true + knockback_time = 0.0 + if has_node("SfxBlockWithShield"): + $SfxBlockWithShield.play() + _show_damage_number(0.0, attacker_position, false, false, false, true) # is_blocked = true + if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree(): + _rpc_to_ready_peers("_sync_damage", [0.0, attacker_position, false, false, false, true]) + print(name, " BLOCKED attack from direction ", dir_to_attacker) + return + # If taking damage while holding something, drop/throw immediately if held_object: if is_lifting: @@ -5457,6 +5695,9 @@ func _die(): velocity = Vector2.ZERO is_knocked_back = false damage_direction_lock_timer = 0.0 + is_shielding = false + was_shielding_last_frame = false + _update_shield_visibility() # CRITICAL: Release any held object/player BEFORE dying to restore their collision layers if held_object: @@ -5528,7 +5769,7 @@ func _die(): fade_tween.set_parallel(true) for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair, sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons, - sprite_headgear, sprite_weapon, shadow]: + sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]: if sprite_layer: fade_tween.tween_property(sprite_layer, "modulate:a", 0.0, 0.5) @@ -5601,7 +5842,7 @@ func _respawn(): # Restore visibility (fade all sprite layers back in) for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair, sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons, - sprite_headgear, sprite_weapon, shadow]: + sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]: if sprite_layer: sprite_layer.modulate.a = 1.0 @@ -5741,7 +5982,7 @@ func _sync_respawn(spawn_pos: Vector2): # Restore visibility for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair, sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons, - sprite_headgear, sprite_weapon, shadow]: + sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]: if sprite_layer: sprite_layer.modulate.a = 1.0 @@ -5867,7 +6108,7 @@ func _sync_race_and_stats(race: String, base_stats: Dictionary): "Human": character_stats.setEars(0) - # Give Human starting spellbook (Tome of Flames) to remote players + # Give Human (Wizard) starting spellbook (Tome of Flames) and Hat to remote players # (Authority players get this in _setup_player_appearance) # Check if equipment is missing - give it regardless of whether race changed if not is_multiplayer_authority(): @@ -5875,17 +6116,25 @@ func _sync_race_and_stats(race: String, base_stats: Dictionary): if character_stats.equipment["offhand"] == null: 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 character_stats.equipment["headgear"] == null: + needs_equipment = true + else: + var headgear = character_stats.equipment["headgear"] + if not headgear or headgear.item_name != "Hat": + needs_equipment = true if needs_equipment: 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") + var starting_hat = ItemDatabase.create_item("hat") + if starting_hat: + character_stats.equipment["headgear"] = starting_hat + _apply_appearance_to_sprites() + print("Human player ", name, " (remote) received Tome of Flames and Hat via race sync") _: character_stats.setEars(0) @@ -5990,6 +6239,25 @@ func _sync_inventory(inventory_data: Array): character_stats.character_changed.emit(character_stats) print(name, " inventory synced: ", character_stats.inventory.size(), " items") +@rpc("any_peer", "reliable") +func _apply_inventory_and_equipment_from_server(inventory_data: Array, equipment_data: Dictionary): + # Joiner receives inventory+equipment push from server after loot pickup (or other server-driven change). + # Always apply – no authority rejection. Used only when server adds items to a remote player. + if multiplayer.is_server(): + return + if not character_stats: + return + character_stats.inventory.clear() + for item_data in inventory_data: + if item_data != null: + character_stats.inventory.append(Item.new(item_data)) + for slot_name in character_stats.equipment.keys(): + var item_data = equipment_data.get(slot_name, null) + character_stats.equipment[slot_name] = Item.new(item_data) if item_data != null else null + _apply_appearance_to_sprites() + character_stats.character_changed.emit(character_stats) + print(name, " inventory+equipment applied from server: ", character_stats.inventory.size(), " items") + func heal(amount: float): if is_dead: return @@ -6030,9 +6298,9 @@ func _sync_keys(new_key_count: int): keys = new_key_count @rpc("authority", "reliable") -func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false): +func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false, is_blocked: bool = false): # Show damage number (red, using dmg_numbers.png font) above player - # Show even if amount is 0 for MISS/DODGED + # Show even if amount is 0 for MISS/DODGED/BLOCKED var damage_number_scene = preload("res://scenes/damage_number.tscn") if not damage_number_scene: @@ -6049,6 +6317,9 @@ func _show_damage_number(amount: float, from_position: Vector2, is_crit: bool = elif is_miss: damage_label.label = "MISS" damage_label.color = Color.GRAY + elif is_blocked: + damage_label.label = "BLOCKED" + damage_label.color = Color(0.4, 0.65, 1.0) # Light blue else: damage_label.label = str(int(amount)) damage_label.color = Color.ORANGE if is_crit else Color.RED @@ -6138,13 +6409,23 @@ func _on_level_up_stats(stats_increased: Array): base_y_offset -= y_spacing @rpc("any_peer", "reliable") -func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false): +func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = false, is_miss: bool = false, is_dodged: bool = false, is_blocked: bool = false): # This RPC only syncs visual effects, not damage application # (damage is already applied via rpc_take_damage) if not is_multiplayer_authority(): # If dodged, only show dodge text, no other effects if is_dodged: - _show_damage_number(0.0, attacker_position, false, false, true) + _show_damage_number(0.0, attacker_position, false, false, true, false) + return + # If blocked, show BLOCKED, small knockback, block sound; no damage effects + if is_blocked: + var block_knock_dir = (global_position - attacker_position).normalized() + velocity = block_knock_dir * 90.0 + is_knocked_back = true + knockback_time = 0.0 + if has_node("SfxBlockWithShield"): + $SfxBlockWithShield.play() + _show_damage_number(0.0, attacker_position, false, false, false, true) return # Play damage sound and effects (rate limited to prevent spam when tab becomes active) @@ -6181,7 +6462,7 @@ func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = fa tween.tween_property(sprite_body, "modulate", Color.WHITE, 0.1) # Show damage number - _show_damage_number(_amount, attacker_position, is_crit, is_miss, false) + _show_damage_number(_amount, attacker_position, is_crit, is_miss, false, false) func on_grabbed(by_player): print(name, " grabbed by ", by_player.name)