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