fix spider bat boss alittle
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://d0tknj36n3vwo"
|
||||||
|
path="res://.godot/imported/spawn_spider.wav-8b3babbab3c4ba911667834ca652655e.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/enemies/boss/spider_bat_boss/spawn_spider.wav"
|
||||||
|
dest_files=["res://.godot/imported/spawn_spider.wav-8b3babbab3c4ba911667834ca652655e.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/enemies/boss/spider_bat_boss/webbed.wav
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://c72t6mx4oqep6"
|
||||||
|
path="res://.godot/imported/webbed.wav-7454b773fd83c25e717dd593b6e03e5d.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/enemies/boss/spider_bat_boss/webbed.wav"
|
||||||
|
dest_files=["res://.godot/imported/webbed.wav-7454b773fd83c25e717dd593b6e03e5d.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/z3/Dungeon Teleporting.wav
Normal file
24
src/assets/audio/sfx/z3/Dungeon Teleporting.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://dvvw4jhpj0ba"
|
||||||
|
path="res://.godot/imported/Dungeon Teleporting.wav-9c15e87c6625a75b2c5ae70765a0dfc5.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/Dungeon Teleporting.wav"
|
||||||
|
dest_files=["res://.godot/imported/Dungeon Teleporting.wav-9c15e87c6625a75b2c5ae70765a0dfc5.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/z3/Hidden Treasure.wav
Normal file
24
src/assets/audio/sfx/z3/Hidden Treasure.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://ya85wjb2c2jq"
|
||||||
|
path="res://.godot/imported/Hidden Treasure.wav-4a722a48442d87d1f342ba7690322fd0.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/Hidden Treasure.wav"
|
||||||
|
dest_files=["res://.godot/imported/Hidden Treasure.wav-4a722a48442d87d1f342ba7690322fd0.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/z3/LA_Dungeon_Teleport_Appear.wav
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://c62cldelcfne2"
|
||||||
|
path="res://.godot/imported/LA_Dungeon_Teleport_Appear.wav-54a258164e247f88194203626b8a7227.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Dungeon_Teleport_Appear.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Dungeon_Teleport_Appear.wav-54a258164e247f88194203626b8a7227.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/z3/LA_Enemy_Fall.wav
Normal file
24
src/assets/audio/sfx/z3/LA_Enemy_Fall.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://b5eb43738ubhr"
|
||||||
|
path="res://.godot/imported/LA_Enemy_Fall.wav-5c1f126f97d1f1e5329cf7967629c757.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Enemy_Fall.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Enemy_Fall.wav-5c1f126f97d1f1e5329cf7967629c757.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/z3/LA_Enemy_Jump.wav
Normal file
24
src/assets/audio/sfx/z3/LA_Enemy_Jump.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://csrrj2vrkbsqj"
|
||||||
|
path="res://.godot/imported/LA_Enemy_Jump.wav-14c628b999ac2f732bb16c252c8ae295.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Enemy_Jump.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Enemy_Jump.wav-14c628b999ac2f732bb16c252c8ae295.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/z3/LA_Fanfare_Item_Extended.wav
Normal file
24
src/assets/audio/sfx/z3/LA_Fanfare_Item_Extended.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://bqt1nwobb8ox7"
|
||||||
|
path="res://.godot/imported/LA_Fanfare_Item_Extended.wav-0b55b761c7da9e2f8b0bc63e17d54474.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Fanfare_Item_Extended.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Fanfare_Item_Extended.wav-0b55b761c7da9e2f8b0bc63e17d54474.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/z3/LA_Get_Item2.wav
Normal file
24
src/assets/audio/sfx/z3/LA_Get_Item2.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://vxjjcuiam777"
|
||||||
|
path="res://.godot/imported/LA_Get_Item2.wav-ab9a88eeed38824f4ca082f2325efa2c.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Get_Item2.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Get_Item2.wav-ab9a88eeed38824f4ca082f2325efa2c.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/z3/LA_Ground_Crumble.wav
Normal file
24
src/assets/audio/sfx/z3/LA_Ground_Crumble.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://cjvqt1udp5kxq"
|
||||||
|
path="res://.godot/imported/LA_Ground_Crumble.wav-272cd2e57105c587e86ebefa52d1f6be.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Ground_Crumble.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Ground_Crumble.wav-272cd2e57105c587e86ebefa52d1f6be.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/z3/LA_Link_Fall.wav
Normal file
24
src/assets/audio/sfx/z3/LA_Link_Fall.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://dfel4odf6r138"
|
||||||
|
path="res://.godot/imported/LA_Link_Fall.wav-65086e00cfbb51f054cd9689fb280393.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Link_Fall.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Link_Fall.wav-65086e00cfbb51f054cd9689fb280393.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/z3/LA_NightmareShadows_Move.wav
Normal file
24
src/assets/audio/sfx/z3/LA_NightmareShadows_Move.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://ckb3vyfwwmij"
|
||||||
|
path="res://.godot/imported/LA_NightmareShadows_Move.wav-2885ed8c855dee177e45a85e898c3e47.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_NightmareShadows_Move.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_NightmareShadows_Move.wav-2885ed8c855dee177e45a85e898c3e47.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/z3/LA_Sword_Charge.wav
Normal file
24
src/assets/audio/sfx/z3/LA_Sword_Charge.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://cx1xe4yldwhfv"
|
||||||
|
path="res://.godot/imported/LA_Sword_Charge.wav-114f2c20f427b0b1b9b9d3ce1a114cce.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Sword_Charge.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Sword_Charge.wav-114f2c20f427b0b1b9b9d3ce1a114cce.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/z3/LA_Sword_Slash1.wav
Normal file
24
src/assets/audio/sfx/z3/LA_Sword_Slash1.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://tyi5kdiw0hwg"
|
||||||
|
path="res://.godot/imported/LA_Sword_Slash1.wav-d975919bfdc80e3083b3a623890f3985.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Sword_Slash1.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Sword_Slash1.wav-d975919bfdc80e3083b3a623890f3985.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/z3/LA_Sword_Slash2.wav
Normal file
24
src/assets/audio/sfx/z3/LA_Sword_Slash2.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://b2jo6lccqgma0"
|
||||||
|
path="res://.godot/imported/LA_Sword_Slash2.wav-2eb425bb94535ebab29cf42cfeb8998b.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Sword_Slash2.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Sword_Slash2.wav-2eb425bb94535ebab29cf42cfeb8998b.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/z3/LA_Sword_Slash3.wav
Normal file
24
src/assets/audio/sfx/z3/LA_Sword_Slash3.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://dr7fd80esg2c7"
|
||||||
|
path="res://.godot/imported/LA_Sword_Slash3.wav-1b3736f2c19628deb55c8c6e31aadba6.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Sword_Slash3.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Sword_Slash3.wav-1b3736f2c19628deb55c8c6e31aadba6.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/z3/LA_Sword_Slash4.wav
Normal file
24
src/assets/audio/sfx/z3/LA_Sword_Slash4.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://cslploogfk2cw"
|
||||||
|
path="res://.godot/imported/LA_Sword_Slash4.wav-c78036997283570bbe39d2a5bcb7fddc.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Sword_Slash4.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Sword_Slash4.wav-c78036997283570bbe39d2a5bcb7fddc.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/z3/LA_Sword_Spin.wav
Normal file
24
src/assets/audio/sfx/z3/LA_Sword_Spin.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://drny5i166dlhu"
|
||||||
|
path="res://.godot/imported/LA_Sword_Spin.wav-93bb3cc2ac6f0392102328bc5bae47f6.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Sword_Spin.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Sword_Spin.wav-93bb3cc2ac6f0392102328bc5bae47f6.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/z3/LA_Sword_Tap.wav
Normal file
24
src/assets/audio/sfx/z3/LA_Sword_Tap.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://c38uq1q83ltw1"
|
||||||
|
path="res://.godot/imported/LA_Sword_Tap.wav-c15f5bbfeaa5b3212f5826ca726da720.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Sword_Tap.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Sword_Tap.wav-c15f5bbfeaa5b3212f5826ca726da720.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/z3/LA_Sword_Tap_Bombable.wav
Normal file
24
src/assets/audio/sfx/z3/LA_Sword_Tap_Bombable.wav.import
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="wav"
|
||||||
|
type="AudioStreamWAV"
|
||||||
|
uid="uid://c3b8n1etek12p"
|
||||||
|
path="res://.godot/imported/LA_Sword_Tap_Bombable.wav-e1a5a82794449acf4e83d10d90551fe8.sample"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/z3/LA_Sword_Tap_Bombable.wav"
|
||||||
|
dest_files=["res://.godot/imported/LA_Sword_Tap_Bombable.wav-e1a5a82794449acf4e83d10d90551fe8.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
|
||||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 7.5 KiB |
27
src/scenes/attack_web_shot.tscn
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
[gd_scene format=3 uid="uid://ci7w4k6w75p8s"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://bibyqdhticm5i" path="res://scripts/attack_web_shot.gd" id="1_script"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="2_tex"]
|
||||||
|
[ext_resource type="AudioStreamWAV" path="res://assets/audio/sfx/enemies/boss/spider_bat_boss/webbed.wav" id="3_webbed"]
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_web"]
|
||||||
|
size = Vector2(8, 8)
|
||||||
|
|
||||||
|
[node name="AttackWebShot" type="Area2D" unique_id=38090342]
|
||||||
|
collision_layer = 8
|
||||||
|
collision_mask = 1
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1512354021]
|
||||||
|
centered = true
|
||||||
|
texture = ExtResource("2_tex")
|
||||||
|
hframes = 105
|
||||||
|
vframes = 79
|
||||||
|
frame = 568
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=817117840]
|
||||||
|
shape = SubResource("RectangleShape2D_web")
|
||||||
|
|
||||||
|
[node name="SfxWebbed" type="AudioStreamPlayer2D" parent="." unique_id=817117841]
|
||||||
|
stream = ExtResource("3_webbed")
|
||||||
|
bus = &"Sfx"
|
||||||
@@ -4,19 +4,8 @@
|
|||||||
[ext_resource type="Script" uid="uid://c00num5rkqm5l" path="res://scripts/boss_room_test.gd" id="1_qh2jl"]
|
[ext_resource type="Script" uid="uid://c00num5rkqm5l" path="res://scripts/boss_room_test.gd" id="1_qh2jl"]
|
||||||
[ext_resource type="PackedScene" uid="uid://cxk4tjias8r18" path="res://scenes/torch_wall.tscn" id="2_qh2jl"]
|
[ext_resource type="PackedScene" uid="uid://cxk4tjias8r18" path="res://scenes/torch_wall.tscn" id="2_qh2jl"]
|
||||||
[ext_resource type="PackedScene" uid="uid://cxfvw8y7jqn2p" path="res://scenes/player.tscn" id="3_bbgrj"]
|
[ext_resource type="PackedScene" uid="uid://cxfvw8y7jqn2p" path="res://scenes/player.tscn" id="3_bbgrj"]
|
||||||
[ext_resource type="Texture2D" uid="uid://dnqqb2jw6qk5c" path="res://assets/gfx/enemies/boss/SPIDERBATNY.png" id="5_bbgrj"]
|
|
||||||
[ext_resource type="PackedScene" uid="uid://dlpxvxijydpob" path="res://scenes/fire.tscn" id="6_fwyy1"]
|
[ext_resource type="PackedScene" uid="uid://dlpxvxijydpob" path="res://scenes/fire.tscn" id="6_fwyy1"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://kmlhn1iaceg8" path="res://scenes/boss_spider_bat.tscn" id="6_w00nx"]
|
||||||
[sub_resource type="Gradient" id="Gradient_bbgrj"]
|
|
||||||
colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0)
|
|
||||||
|
|
||||||
[sub_resource type="GradientTexture2D" id="GradientTexture2D_fwyy1"]
|
|
||||||
gradient = SubResource("Gradient_bbgrj")
|
|
||||||
width = 56
|
|
||||||
height = 8
|
|
||||||
fill = 1
|
|
||||||
fill_from = Vector2(0.50427353, 0.53846157)
|
|
||||||
fill_to = Vector2(1, 1)
|
|
||||||
|
|
||||||
[node name="BossRoomTest" type="Node2D" unique_id=1788886249]
|
[node name="BossRoomTest" type="Node2D" unique_id=1788886249]
|
||||||
script = ExtResource("1_qh2jl")
|
script = ExtResource("1_qh2jl")
|
||||||
@@ -51,7 +40,7 @@ position = Vector2(323, 352)
|
|||||||
rotation = 3.1415927
|
rotation = 3.1415927
|
||||||
|
|
||||||
[node name="TorchWall4" parent="Environment" unique_id=600381662 instance=ExtResource("2_qh2jl")]
|
[node name="TorchWall4" parent="Environment" unique_id=600381662 instance=ExtResource("2_qh2jl")]
|
||||||
position = Vector2(421, 351)
|
position = Vector2(396, 351)
|
||||||
rotation = 3.1415927
|
rotation = 3.1415927
|
||||||
|
|
||||||
[node name="Entities" type="Node2D" parent="." unique_id=1341597062]
|
[node name="Entities" type="Node2D" parent="." unique_id=1341597062]
|
||||||
@@ -126,11 +115,5 @@ light_mask = 1048575
|
|||||||
visibility_layer = 1048575
|
visibility_layer = 1048575
|
||||||
color = Color(0.4140625, 0.4140625, 0.4140625, 1)
|
color = Color(0.4140625, 0.4140625, 0.4140625, 1)
|
||||||
|
|
||||||
[node name="Spiderbatny" type="Sprite2D" parent="." unique_id=1820994324]
|
[node name="BossSpiderBat" parent="." unique_id=673326165 instance=ExtResource("6_w00nx")]
|
||||||
position = Vector2(362, 221)
|
position = Vector2(362, 221)
|
||||||
texture = ExtResource("5_bbgrj")
|
|
||||||
|
|
||||||
[node name="Sprite2D" type="Sprite2D" parent="Spiderbatny" unique_id=242364332]
|
|
||||||
z_index = -1
|
|
||||||
position = Vector2(0, 35)
|
|
||||||
texture = SubResource("GradientTexture2D_fwyy1")
|
|
||||||
|
|||||||
442
src/scenes/boss_spider_bat.tscn
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
[gd_scene format=3 uid="uid://kmlhn1iaceg8"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://bgdwn43m2yrtl" path="res://scripts/boss_spider_bat.gd" id="1_kwh6a"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://fevkanam8r2s" path="res://assets/gfx/enemies/boss/SpiderBat/flying_down.png" id="1_m3p0g"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://cr5mc326wpob3" path="res://assets/gfx/enemies/boss/SpiderBat/down_right.png" id="3_ht382"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://du3q527l26q1d" path="res://assets/gfx/enemies/boss/SpiderBat/Spiderbat death.png" id="3_ygtif"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://dsb72u2au1rkq" path="res://assets/gfx/enemies/boss/SpiderBat/right.png" id="4_ygtif"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://beeix80itbwb5" path="res://assets/audio/sfx/wizard/animevox/slash_1769364693031.wav" id="5_slash"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://d0tknj36n3vwo" path="res://assets/audio/sfx/enemies/boss/spider_bat_boss/spawn_spider.wav" id="6_spawn"]
|
||||||
|
|
||||||
|
[sub_resource type="Gradient" id="Gradient_bbgrj"]
|
||||||
|
colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0)
|
||||||
|
|
||||||
|
[sub_resource type="GradientTexture2D" id="GradientTexture2D_fwyy1"]
|
||||||
|
gradient = SubResource("Gradient_bbgrj")
|
||||||
|
width = 56
|
||||||
|
height = 8
|
||||||
|
fill = 1
|
||||||
|
fill_from = Vector2(0.50427353, 0.53846157)
|
||||||
|
fill_to = Vector2(1, 1)
|
||||||
|
|
||||||
|
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_m3p0g"]
|
||||||
|
radius = 9.0
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_m3p0g"]
|
||||||
|
length = 0.001
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("Spiderbatny:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [1]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Spiderbatny:texture")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [ExtResource("1_m3p0g")]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Spiderbatny:hframes")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [5]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("Spiderbatny:scale")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_hkjy0"]
|
||||||
|
resource_name = "die"
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("Spiderbatny:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.048109833, 0.08808845, 0.14907283, 0.20124832),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [0, 2, 4, 6, 9]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Spiderbatny:texture")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [ExtResource("3_ygtif")]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Spiderbatny:hframes")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [10]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("Spiderbatny:scale")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_ow570"]
|
||||||
|
resource_name = "down"
|
||||||
|
length = 0.4
|
||||||
|
loop_mode = 1
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("Spiderbatny:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.08131247, 0.16262487, 0.30153358),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [1, 2, 3, 2]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Spiderbatny:texture")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [ExtResource("1_m3p0g")]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Spiderbatny:hframes")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [5]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("Spiderbatny:scale")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_kwh6a"]
|
||||||
|
resource_name = "down_left"
|
||||||
|
length = 0.4
|
||||||
|
loop_mode = 1
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("Spiderbatny:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0.0006776033, 0.08131247, 0.16126966, 0.30153358),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [0, 1, 2, 1]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Spiderbatny:texture")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [ExtResource("3_ht382")]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Spiderbatny:hframes")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [4]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("Spiderbatny:scale")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_5kw4h"]
|
||||||
|
resource_name = "down_right"
|
||||||
|
length = 0.4
|
||||||
|
loop_mode = 1
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("Spiderbatny:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0.0006776033, 0.08131247, 0.16126966, 0.30153358),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [0, 1, 2, 1]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Spiderbatny:texture")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [ExtResource("3_ht382")]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Spiderbatny:hframes")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [4]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("Spiderbatny:scale")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_ht382"]
|
||||||
|
resource_name = "left"
|
||||||
|
length = 0.4
|
||||||
|
loop_mode = 1
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("Spiderbatny:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.06301712, 0.15720406, 0.2893367),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [1, 2, 0, 1]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Spiderbatny:texture")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [ExtResource("4_ygtif")]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Spiderbatny:hframes")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [4]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("Spiderbatny:scale")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(-1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_dbivb"]
|
||||||
|
resource_name = "right"
|
||||||
|
length = 0.4
|
||||||
|
loop_mode = 1
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("Spiderbatny:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.06301712, 0.15720406, 0.2893367),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [1, 2, 0, 1]
|
||||||
|
}
|
||||||
|
tracks/1/type = "value"
|
||||||
|
tracks/1/imported = false
|
||||||
|
tracks/1/enabled = true
|
||||||
|
tracks/1/path = NodePath("Spiderbatny:texture")
|
||||||
|
tracks/1/interp = 1
|
||||||
|
tracks/1/loop_wrap = true
|
||||||
|
tracks/1/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [ExtResource("4_ygtif")]
|
||||||
|
}
|
||||||
|
tracks/2/type = "value"
|
||||||
|
tracks/2/imported = false
|
||||||
|
tracks/2/enabled = true
|
||||||
|
tracks/2/path = NodePath("Spiderbatny:hframes")
|
||||||
|
tracks/2/interp = 1
|
||||||
|
tracks/2/loop_wrap = true
|
||||||
|
tracks/2/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [4]
|
||||||
|
}
|
||||||
|
tracks/3/type = "value"
|
||||||
|
tracks/3/imported = false
|
||||||
|
tracks/3/enabled = true
|
||||||
|
tracks/3/path = NodePath("Spiderbatny:scale")
|
||||||
|
tracks/3/interp = 1
|
||||||
|
tracks/3/loop_wrap = true
|
||||||
|
tracks/3/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [Vector2(1, 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="AnimationLibrary" id="AnimationLibrary_o02h7"]
|
||||||
|
_data = {
|
||||||
|
&"RESET": SubResource("Animation_m3p0g"),
|
||||||
|
&"die": SubResource("Animation_hkjy0"),
|
||||||
|
&"down": SubResource("Animation_ow570"),
|
||||||
|
&"down_left": SubResource("Animation_kwh6a"),
|
||||||
|
&"down_right": SubResource("Animation_5kw4h"),
|
||||||
|
&"left": SubResource("Animation_ht382"),
|
||||||
|
&"right": SubResource("Animation_dbivb")
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="CircleShape2D" id="CircleShape2D_ht382"]
|
||||||
|
radius = 202.02228
|
||||||
|
|
||||||
|
[node name="BossSpiderBat" type="CharacterBody2D" unique_id=673326165]
|
||||||
|
script = ExtResource("1_kwh6a")
|
||||||
|
|
||||||
|
[node name="Spiderbatny" type="Sprite2D" parent="." unique_id=572346446]
|
||||||
|
texture = ExtResource("1_m3p0g")
|
||||||
|
hframes = 5
|
||||||
|
frame = 1
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="Spiderbatny" unique_id=1095428481]
|
||||||
|
position = Vector2(0, 35)
|
||||||
|
texture = SubResource("GradientTexture2D_fwyy1")
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=1518243670]
|
||||||
|
position = Vector2(0, 27)
|
||||||
|
rotation = 1.5707964
|
||||||
|
shape = SubResource("CapsuleShape2D_m3p0g")
|
||||||
|
|
||||||
|
[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=1762485835]
|
||||||
|
libraries/ = SubResource("AnimationLibrary_o02h7")
|
||||||
|
autoplay = &"down"
|
||||||
|
|
||||||
|
[node name="ActivationArea" type="Area2D" parent="." unique_id=1681865045]
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="ActivationArea" unique_id=1459593944]
|
||||||
|
position = Vector2(0, 26)
|
||||||
|
shape = SubResource("CircleShape2D_ht382")
|
||||||
|
debug_color = Color(0.70196074, 0.4646213, 0.36891928, 0.41960785)
|
||||||
|
|
||||||
|
[node name="SfxWebShot" type="AudioStreamPlayer2D" parent="." unique_id=1681865046]
|
||||||
|
stream = ExtResource("5_slash")
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[node name="SfxSpawnSpider" type="AudioStreamPlayer2D" parent="." unique_id=1681865047]
|
||||||
|
stream = ExtResource("6_spawn")
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[connection signal="body_shape_entered" from="ActivationArea" to="." method="_on_activation_area_body_shape_entered"]
|
||||||
31
src/scenes/detected_effect.tscn
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
[gd_scene format=3 uid="uid://c3ogf878jdfql"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://b45h84vbq3jw" path="res://scripts/detected_effect.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="Gradient" id="Gradient_blue"]
|
||||||
|
offsets = PackedFloat32Array(0.5, 0.85)
|
||||||
|
colors = PackedColorArray(1, 1, 1, 1, 0.4, 0.5, 0.9, 0)
|
||||||
|
|
||||||
|
[sub_resource type="GradientTexture2D" id="GradientTexture2D_blue"]
|
||||||
|
gradient = SubResource("Gradient_blue")
|
||||||
|
fill = 1
|
||||||
|
fill_from = Vector2(0.5, 0.5)
|
||||||
|
fill_to = Vector2(0.5, 0)
|
||||||
|
|
||||||
|
[node name="DetectedEffect" type="Node2D" unique_id=1116195340]
|
||||||
|
z_index = 5
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="FxSprite" type="Sprite2D" parent="." unique_id=1691079974]
|
||||||
|
z_index = 1
|
||||||
|
texture = ExtResource("2_tex")
|
||||||
|
hframes = 105
|
||||||
|
vframes = 79
|
||||||
|
frame = 169
|
||||||
|
|
||||||
|
[node name="DetectLight" type="PointLight2D" parent="." unique_id=1040746394]
|
||||||
|
color = Color(0.35, 0.5, 0.95, 1)
|
||||||
|
energy = 0.9
|
||||||
|
texture = SubResource("GradientTexture2D_blue")
|
||||||
|
texture_scale = 0.6
|
||||||
@@ -87,6 +87,9 @@ radius = 5.0
|
|||||||
[sub_resource type="CircleShape2D" id="CircleShape2D_ptw5w"]
|
[sub_resource type="CircleShape2D" id="CircleShape2D_ptw5w"]
|
||||||
radius = 58.30952
|
radius = 58.30952
|
||||||
|
|
||||||
|
[sub_resource type="CircleShape2D" id="CircleShape2D_detect"]
|
||||||
|
radius = 50.0
|
||||||
|
|
||||||
[node name="EnemyHand" type="CharacterBody2D" unique_id=512887809]
|
[node name="EnemyHand" type="CharacterBody2D" unique_id=512887809]
|
||||||
collision_mask = 64
|
collision_mask = 64
|
||||||
script = ExtResource("1_hqcsv")
|
script = ExtResource("1_hqcsv")
|
||||||
@@ -103,6 +106,13 @@ libraries/ = SubResource("AnimationLibrary_ptw5w")
|
|||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=448410993]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=448410993]
|
||||||
shape = SubResource("CircleShape2D_lpach")
|
shape = SubResource("CircleShape2D_lpach")
|
||||||
|
|
||||||
|
[node name="DetectionArea" type="Area2D" parent="." unique_id=1888073969]
|
||||||
|
collision_layer = 0
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="DetectionArea" unique_id=1403525747]
|
||||||
|
shape = SubResource("CircleShape2D_detect")
|
||||||
|
debug_color = Color(0.54, 0.05, 0.7, 0.42)
|
||||||
|
|
||||||
[node name="EmergeArea" type="Area2D" parent="." unique_id=1688073969]
|
[node name="EmergeArea" type="Area2D" parent="." unique_id=1688073969]
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
|
|
||||||
@@ -122,6 +132,7 @@ collision_layer = 0
|
|||||||
shape = SubResource("CircleShape2D_ptw5w")
|
shape = SubResource("CircleShape2D_ptw5w")
|
||||||
debug_color = Color(0.70196074, 0.6745404, 0.69139016, 0.41960785)
|
debug_color = Color(0.70196074, 0.6745404, 0.69139016, 0.41960785)
|
||||||
|
|
||||||
|
[connection signal="body_entered" from="DetectionArea" to="." method="_on_detection_area_body_entered"]
|
||||||
[connection signal="body_entered" from="EmergeArea" to="." method="_on_emerge_area_body_entered"]
|
[connection signal="body_entered" from="EmergeArea" to="." method="_on_emerge_area_body_entered"]
|
||||||
[connection signal="body_entered" from="GrabPlayerArea" to="." method="_on_grab_player_area_body_entered"]
|
[connection signal="body_entered" from="GrabPlayerArea" to="." method="_on_grab_player_area_body_entered"]
|
||||||
[connection signal="body_entered" from="PlayerInterestArea" to="." method="_on_player_interest_area_body_entered"]
|
[connection signal="body_entered" from="PlayerInterestArea" to="." method="_on_player_interest_area_body_entered"]
|
||||||
|
|||||||
@@ -340,6 +340,7 @@ vframes = 8
|
|||||||
[node name="Sprite2DShield" type="Sprite2D" parent="." unique_id=470468744]
|
[node name="Sprite2DShield" type="Sprite2D" parent="." unique_id=470468744]
|
||||||
visible = false
|
visible = false
|
||||||
material = SubResource("ShaderMaterial_shield")
|
material = SubResource("ShaderMaterial_shield")
|
||||||
|
position = Vector2(0, -4)
|
||||||
texture = ExtResource("14_shield")
|
texture = ExtResource("14_shield")
|
||||||
hframes = 35
|
hframes = 35
|
||||||
vframes = 8
|
vframes = 8
|
||||||
@@ -347,6 +348,7 @@ vframes = 8
|
|||||||
[node name="Sprite2DShieldHolding" type="Sprite2D" parent="." unique_id=1318098286]
|
[node name="Sprite2DShieldHolding" type="Sprite2D" parent="." unique_id=1318098286]
|
||||||
visible = false
|
visible = false
|
||||||
material = SubResource("ShaderMaterial_shield")
|
material = SubResource("ShaderMaterial_shield")
|
||||||
|
position = Vector2(0, -4)
|
||||||
texture = ExtResource("15_shieldh")
|
texture = ExtResource("15_shieldh")
|
||||||
hframes = 35
|
hframes = 35
|
||||||
vframes = 8
|
vframes = 8
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
[ext_resource type="Script" uid="uid://id0s5um3dac1" path="res://scripts/enemy_slime.gd" id="1"]
|
[ext_resource type="Script" uid="uid://id0s5um3dac1" path="res://scripts/enemy_slime.gd" id="1"]
|
||||||
[ext_resource type="Texture2D" uid="uid://csr5k0etreqbf" path="res://assets/gfx/enemies/Slime.png" id="2"]
|
[ext_resource type="Texture2D" uid="uid://csr5k0etreqbf" path="res://assets/gfx/enemies/Slime.png" id="2"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://csrrj2vrkbsqj" path="res://assets/audio/sfx/z3/LA_Enemy_Jump.wav" id="3_y5ckx"]
|
||||||
|
|
||||||
[sub_resource type="CircleShape2D" id="CircleShape2D_slime"]
|
[sub_resource type="CircleShape2D" id="CircleShape2D_slime"]
|
||||||
radius = 6.0
|
radius = 6.0
|
||||||
@@ -26,3 +27,8 @@ hframes = 15
|
|||||||
|
|
||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=521937817]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=521937817]
|
||||||
shape = SubResource("CircleShape2D_slime")
|
shape = SubResource("CircleShape2D_slime")
|
||||||
|
|
||||||
|
[node name="SfxJump" type="AudioStreamPlayer2D" parent="." unique_id=1770035153]
|
||||||
|
stream = ExtResource("3_y5ckx")
|
||||||
|
attenuation = 3.363586
|
||||||
|
bus = &"Sfx"
|
||||||
|
|||||||
29
src/scenes/enemy_spider.tscn
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
[gd_scene format=3 uid="uid://b46vll6yphyl5"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://7e0vssvq87hn" path="res://scripts/enemy_spider.gd" id="1_script"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://bmogyono02pl3" path="res://assets/gfx/enemies/spider.png" id="2_tex"]
|
||||||
|
|
||||||
|
[sub_resource type="CircleShape2D" id="CircleShape2D_spider"]
|
||||||
|
radius = 4.0
|
||||||
|
|
||||||
|
[node name="EnemySpider" type="CharacterBody2D" unique_id=716780254]
|
||||||
|
collision_layer = 2
|
||||||
|
collision_mask = 65
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1167304991]
|
||||||
|
scale = Vector2(0.5, 0.5)
|
||||||
|
texture = ExtResource("2_tex")
|
||||||
|
hframes = 3
|
||||||
|
vframes = 3
|
||||||
|
|
||||||
|
[node name="Shadow" type="Sprite2D" parent="." unique_id=1051112006]
|
||||||
|
modulate = Color(0, 0, 0, 0.5)
|
||||||
|
z_index = -1
|
||||||
|
scale = Vector2(0.5, 0.5)
|
||||||
|
texture = ExtResource("2_tex")
|
||||||
|
hframes = 3
|
||||||
|
vframes = 3
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=1330855476]
|
||||||
|
shape = SubResource("CircleShape2D_spider")
|
||||||
@@ -185,6 +185,44 @@ horizontal_alignment = 1
|
|||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
texture_progress = ExtResource("4_hearts_filled")
|
texture_progress = ExtResource("4_hearts_filled")
|
||||||
|
|
||||||
|
[node name="CenterBottom" type="MarginContainer" parent="."]
|
||||||
|
anchors_preset = 7
|
||||||
|
anchor_left = 0.0
|
||||||
|
anchor_top = 1.0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_left = 20.0
|
||||||
|
offset_top = -80.0
|
||||||
|
offset_right = -20.0
|
||||||
|
offset_bottom = 0.0
|
||||||
|
offset_bottom = -8.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 0
|
||||||
|
theme = SubResource("Theme_standard_font")
|
||||||
|
|
||||||
|
[node name="VBoxBoss" type="VBoxContainer" parent="CenterBottom"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme_override_constants/separation = 4
|
||||||
|
|
||||||
|
[node name="LabelBossCenter" type="Label" parent="CenterBottom/VBoxBoss"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme = SubResource("Theme_standard_font")
|
||||||
|
text = "BOSS"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="ProgressBarBossHP" type="ProgressBar" parent="CenterBottom/VBoxBoss"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
custom_minimum_size = Vector2(0, 24)
|
||||||
|
max_value = 400.0
|
||||||
|
value = 400.0
|
||||||
|
show_percentage = false
|
||||||
|
|
||||||
[node name="CenterTop" type="MarginContainer" parent="." unique_id=22752256]
|
[node name="CenterTop" type="MarginContainer" parent="." unique_id=22752256]
|
||||||
anchors_preset = 5
|
anchors_preset = 5
|
||||||
anchor_left = 0.5
|
anchor_left = 0.5
|
||||||
|
|||||||
@@ -28,11 +28,16 @@
|
|||||||
[ext_resource type="AudioStream" uid="uid://4ilddgc4lgyq" path="res://assets/audio/sfx/environment/crate/crash_table-04.wav" id="26_vfomk"]
|
[ext_resource type="AudioStream" uid="uid://4ilddgc4lgyq" path="res://assets/audio/sfx/environment/crate/crash_table-04.wav" id="26_vfomk"]
|
||||||
[ext_resource type="AudioStream" uid="uid://c7kc0aw0wevah" path="res://assets/audio/sfx/environment/crate/wood_impact_break.mp3" id="27_2p257"]
|
[ext_resource type="AudioStream" uid="uid://c7kc0aw0wevah" path="res://assets/audio/sfx/environment/crate/wood_impact_break.mp3" id="27_2p257"]
|
||||||
[ext_resource type="Texture2D" uid="uid://bknascfv4twmi" path="res://assets/gfx/smoke_puffs.png" id="28_2p257"]
|
[ext_resource type="Texture2D" uid="uid://bknascfv4twmi" path="res://assets/gfx/smoke_puffs.png" id="28_2p257"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://vxjjcuiam777" path="res://assets/audio/sfx/z3/LA_Get_Item2.wav" id="29_getitem"]
|
||||||
|
[ext_resource type="AudioStream" path="res://assets/audio/sfx/z3/LA_Enemy_Fall.wav" id="30_fall"]
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_nyc8x"]
|
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_nyc8x"]
|
||||||
radius = 4.0
|
radius = 4.0
|
||||||
height = 12.0
|
height = 12.0
|
||||||
|
|
||||||
|
[sub_resource type="CircleShape2D" id="CircleShape2D_detect"]
|
||||||
|
radius = 99.0
|
||||||
|
|
||||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_1u1k0"]
|
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_1u1k0"]
|
||||||
playback_mode = 1
|
playback_mode = 1
|
||||||
streams_count = 7
|
streams_count = 7
|
||||||
@@ -181,6 +186,13 @@ position = Vector2(0, 2)
|
|||||||
rotation = -1.5707964
|
rotation = -1.5707964
|
||||||
shape = SubResource("CapsuleShape2D_nyc8x")
|
shape = SubResource("CapsuleShape2D_nyc8x")
|
||||||
|
|
||||||
|
[node name="DetectionArea" type="Area2D" parent="." unique_id=1724772540]
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 1
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="DetectionArea" unique_id=1724772541]
|
||||||
|
shape = SubResource("CircleShape2D_detect")
|
||||||
|
|
||||||
[node name="SfxShatter" type="AudioStreamPlayer2D" parent="." unique_id=785438237]
|
[node name="SfxShatter" type="AudioStreamPlayer2D" parent="." unique_id=785438237]
|
||||||
stream = SubResource("AudioStreamRandomizer_1u1k0")
|
stream = SubResource("AudioStreamRandomizer_1u1k0")
|
||||||
max_distance = 1187.0
|
max_distance = 1187.0
|
||||||
@@ -206,6 +218,18 @@ bus = &"Sfx"
|
|||||||
[node name="SfxOpenChest" type="AudioStreamPlayer2D" parent="." unique_id=743332693]
|
[node name="SfxOpenChest" type="AudioStreamPlayer2D" parent="." unique_id=743332693]
|
||||||
stream = SubResource("AudioStreamRandomizer_vfomk")
|
stream = SubResource("AudioStreamRandomizer_vfomk")
|
||||||
|
|
||||||
|
[node name="SfxGetItemFromChest" type="AudioStreamPlayer2D" parent="." unique_id=1928374650]
|
||||||
|
stream = ExtResource("29_getitem")
|
||||||
|
attenuation = 7.0
|
||||||
|
panning_strength = 1.1
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[node name="SfxFall" type="AudioStreamPlayer2D" parent="." unique_id=1928374651]
|
||||||
|
stream = ExtResource("30_fall")
|
||||||
|
attenuation = 6.0
|
||||||
|
panning_strength = 1.12
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
[node name="SfxDragRock" type="AudioStreamPlayer2D" parent="." unique_id=1895903195]
|
[node name="SfxDragRock" type="AudioStreamPlayer2D" parent="." unique_id=1895903195]
|
||||||
stream = SubResource("AudioStreamRandomizer_2p257")
|
stream = SubResource("AudioStreamRandomizer_2p257")
|
||||||
volume_db = -2.611
|
volume_db = -2.611
|
||||||
@@ -234,3 +258,4 @@ process_material = SubResource("ParticleProcessMaterial_ejwle")
|
|||||||
wait_time = 0.07
|
wait_time = 0.07
|
||||||
|
|
||||||
[connection signal="timeout" from="DragParticles/TimerSmokeParticles" to="." method="_on_timer_smoke_particles_timeout"]
|
[connection signal="timeout" from="DragParticles/TimerSmokeParticles" to="." method="_on_timer_smoke_particles_timeout"]
|
||||||
|
[connection signal="body_entered" from="DetectionArea" to="." method="_on_detection_area_body_entered"]
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
[ext_resource type="AudioStream" uid="uid://b5xbv7s85sy5o" path="res://assets/audio/sfx/pickups/potion.mp3" id="7_eeo7l"]
|
[ext_resource type="AudioStream" uid="uid://b5xbv7s85sy5o" path="res://assets/audio/sfx/pickups/potion.mp3" id="7_eeo7l"]
|
||||||
[ext_resource type="AudioStream" uid="uid://cnb376ah43nqi" path="res://assets/audio/sfx/pickups/bite-food-01.mp3" id="8_0tqa7"]
|
[ext_resource type="AudioStream" uid="uid://cnb376ah43nqi" path="res://assets/audio/sfx/pickups/bite-food-01.mp3" id="8_0tqa7"]
|
||||||
[ext_resource type="AudioStream" uid="uid://bbnby1sso3f4v" path="res://assets/audio/sfx/pickups/bite-food-02.mp3" id="9_531sv"]
|
[ext_resource type="AudioStream" uid="uid://bbnby1sso3f4v" path="res://assets/audio/sfx/pickups/bite-food-02.mp3" id="9_531sv"]
|
||||||
|
[ext_resource type="AudioStream" path="res://assets/audio/sfx/z3/LA_Enemy_Fall.wav" id="10_fall"]
|
||||||
|
|
||||||
[sub_resource type="Gradient" id="Gradient_1"]
|
[sub_resource type="Gradient" id="Gradient_1"]
|
||||||
colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0)
|
colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0)
|
||||||
@@ -106,5 +107,11 @@ stream = ExtResource("6_gyjv8")
|
|||||||
[node name="SfxPotionCollect" type="AudioStreamPlayer2D" parent="." unique_id=1615824668]
|
[node name="SfxPotionCollect" type="AudioStreamPlayer2D" parent="." unique_id=1615824668]
|
||||||
stream = ExtResource("7_eeo7l")
|
stream = ExtResource("7_eeo7l")
|
||||||
|
|
||||||
|
[node name="SfxFall" type="AudioStreamPlayer2D" parent="." unique_id=1867176791]
|
||||||
|
stream = ExtResource("10_fall")
|
||||||
|
attenuation = 6.0
|
||||||
|
panning_strength = 1.12
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
[node name="SfxBananaCollect" type="AudioStreamPlayer2D" parent="." unique_id=1763488179]
|
[node name="SfxBananaCollect" type="AudioStreamPlayer2D" parent="." unique_id=1763488179]
|
||||||
stream = SubResource("AudioStreamRandomizer_37k03")
|
stream = SubResource("AudioStreamRandomizer_37k03")
|
||||||
|
|||||||
@@ -50,6 +50,8 @@
|
|||||||
[ext_resource type="AudioStream" uid="uid://cbbeyrdor7nyg" path="res://assets/audio/sfx/player/notice/lookout.mp3" id="48_6e8lb"]
|
[ext_resource type="AudioStream" uid="uid://cbbeyrdor7nyg" path="res://assets/audio/sfx/player/notice/lookout.mp3" id="48_6e8lb"]
|
||||||
[ext_resource type="AudioStream" uid="uid://cs5ruoyq80yi4" path="res://assets/audio/sfx/player/notice/aha.mp3" id="49_2gdjj"]
|
[ext_resource type="AudioStream" uid="uid://cs5ruoyq80yi4" path="res://assets/audio/sfx/player/notice/aha.mp3" id="49_2gdjj"]
|
||||||
[ext_resource type="AudioStream" uid="uid://bew7ciiygabaj" path="res://assets/audio/sfx/player/notice/whatdowehavehere.mp3" id="50_sc3ue"]
|
[ext_resource type="AudioStream" uid="uid://bew7ciiygabaj" path="res://assets/audio/sfx/player/notice/whatdowehavehere.mp3" id="50_sc3ue"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://bqt1nwobb8ox7" path="res://assets/audio/sfx/z3/LA_Fanfare_Item_Extended.wav" id="51_fanfare"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://ya85wjb2c2jq" path="res://assets/audio/sfx/z3/Hidden Treasure.wav" id="52_sc3ue"]
|
||||||
|
|
||||||
[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)
|
||||||
@@ -474,6 +476,21 @@ tracks/0/keys = {
|
|||||||
"values": [0]
|
"values": [0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_sc3ue"]
|
||||||
|
resource_name = "netted"
|
||||||
|
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),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [679]
|
||||||
|
}
|
||||||
|
|
||||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_2dvfe"]
|
[sub_resource type="AnimationLibrary" id="AnimationLibrary_2dvfe"]
|
||||||
_data = {
|
_data = {
|
||||||
&"RESET": SubResource("Animation_t4otl"),
|
&"RESET": SubResource("Animation_t4otl"),
|
||||||
@@ -484,7 +501,8 @@ _data = {
|
|||||||
&"frost_ready": SubResource("Animation_frost_rdy"),
|
&"frost_ready": SubResource("Animation_frost_rdy"),
|
||||||
&"healing_charging": SubResource("Animation_heal_ch"),
|
&"healing_charging": SubResource("Animation_heal_ch"),
|
||||||
&"healing_ready": SubResource("Animation_heal_rdy"),
|
&"healing_ready": SubResource("Animation_heal_rdy"),
|
||||||
&"idle": SubResource("Animation_hax0n")
|
&"idle": SubResource("Animation_hax0n"),
|
||||||
|
&"netted": SubResource("Animation_sc3ue")
|
||||||
}
|
}
|
||||||
|
|
||||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_u2ulf"]
|
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_u2ulf"]
|
||||||
@@ -870,26 +888,34 @@ bus = &"Sfx"
|
|||||||
|
|
||||||
[node name="SfxBirdSound" type="AudioStreamPlayer2D" parent="." unique_id=1946085725]
|
[node name="SfxBirdSound" type="AudioStreamPlayer2D" parent="." unique_id=1946085725]
|
||||||
stream = ExtResource("47_mx1m4")
|
stream = ExtResource("47_mx1m4")
|
||||||
volume_db = -13.255
|
volume_db = -9.289
|
||||||
attenuation = 3.2490087
|
attenuation = 3.2490087
|
||||||
panning_strength = 1.12
|
panning_strength = 1.12
|
||||||
bus = &"Sfx"
|
bus = &"Sfx"
|
||||||
|
|
||||||
[node name="SfxLookOut" type="AudioStreamPlayer2D" parent="." unique_id=1177750193]
|
[node name="SfxLookOut" type="AudioStreamPlayer2D" parent="." unique_id=1177750193]
|
||||||
stream = ExtResource("48_6e8lb")
|
stream = ExtResource("48_6e8lb")
|
||||||
volume_db = 0.881
|
volume_db = 8.813
|
||||||
max_distance = 1138.0
|
max_distance = 1138.0
|
||||||
attenuation = 7.999997
|
attenuation = 5.856343
|
||||||
panning_strength = 1.04
|
panning_strength = 1.04
|
||||||
bus = &"Sfx"
|
|
||||||
|
|
||||||
[node name="SfxAhaa" type="AudioStreamPlayer2D" parent="." unique_id=1556952538]
|
[node name="SfxAhaa" type="AudioStreamPlayer2D" parent="." unique_id=1556952538]
|
||||||
stream = SubResource("AudioStreamRandomizer_lxlsd")
|
stream = SubResource("AudioStreamRandomizer_lxlsd")
|
||||||
|
volume_db = 8.053
|
||||||
max_distance = 1496.0
|
max_distance = 1496.0
|
||||||
attenuation = 6.062864
|
attenuation = 6.062864
|
||||||
panning_strength = 1.13
|
panning_strength = 1.13
|
||||||
bus = &"Sfx"
|
|
||||||
|
|
||||||
[node name="SfxDeny2" type="AudioStreamPlayer2D" parent="." unique_id=1127340261]
|
[node name="SfxDeny2" type="AudioStreamPlayer2D" parent="." unique_id=1127340261]
|
||||||
stream = ExtResource("45_g5jhy")
|
stream = ExtResource("45_g5jhy")
|
||||||
max_distance = 1619.0
|
max_distance = 1619.0
|
||||||
|
|
||||||
|
[node name="SfxLevelUp" type="AudioStreamPlayer2D" parent="." unique_id=1127340262]
|
||||||
|
stream = ExtResource("51_fanfare")
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[node name="SfxSecretFound" type="AudioStreamPlayer" parent="." unique_id=485278181]
|
||||||
|
stream = ExtResource("52_sc3ue")
|
||||||
|
volume_db = 0.882
|
||||||
|
bus = &"Sfx"
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
[ext_resource type="Texture2D" uid="uid://b6eeio3gm7d4u" path="res://assets/gfx/traps/Floor_Lance.png" id="2_aucmg"]
|
[ext_resource type="Texture2D" uid="uid://b6eeio3gm7d4u" path="res://assets/gfx/traps/Floor_Lance.png" id="2_aucmg"]
|
||||||
[ext_resource type="AudioStream" uid="uid://raqmpvp1vj04" path="res://assets/audio/sfx/traps/activate.mp3" id="3_tk2q1"]
|
[ext_resource type="AudioStream" uid="uid://raqmpvp1vj04" path="res://assets/audio/sfx/traps/activate.mp3" id="3_tk2q1"]
|
||||||
[ext_resource type="AudioStream" uid="uid://dxy2phfh0ojot" path="res://assets/audio/sfx/player/dodge/Dodge.mp3" id="4_1sb0t"]
|
[ext_resource type="AudioStream" uid="uid://dxy2phfh0ojot" path="res://assets/audio/sfx/player/dodge/Dodge.mp3" id="4_1sb0t"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://fm6hrpckfknc" path="res://assets/audio/sfx/player/unlock/padlock_wiggle_03.wav" id="5_jl22o"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://be3uspidyqm3x" path="res://assets/audio/sfx/player/unlock/padlock_wiggle_04.wav" id="6_fp550"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://dvttykynr671m" path="res://assets/audio/sfx/player/unlock/padlock_wiggle_05.wav" id="7_1ywii"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://sejnuklu653m" path="res://assets/audio/sfx/player/unlock/padlock_wiggle_06.wav" id="8_2uf36"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://vxjjcuiam777" path="res://assets/audio/sfx/z3/LA_Get_Item2.wav" id="9_disarm"]
|
||||||
|
|
||||||
[sub_resource type="Animation" id="Animation_tk2q1"]
|
[sub_resource type="Animation" id="Animation_tk2q1"]
|
||||||
length = 0.001
|
length = 0.001
|
||||||
@@ -84,6 +89,14 @@ radius = 17.117243
|
|||||||
[sub_resource type="CircleShape2D" id="CircleShape2D_62q8x"]
|
[sub_resource type="CircleShape2D" id="CircleShape2D_62q8x"]
|
||||||
radius = 99.0202
|
radius = 99.0202
|
||||||
|
|
||||||
|
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_crw1k"]
|
||||||
|
random_pitch = 1.0312661
|
||||||
|
streams_count = 4
|
||||||
|
stream_0/stream = ExtResource("5_jl22o")
|
||||||
|
stream_1/stream = ExtResource("6_fp550")
|
||||||
|
stream_2/stream = ExtResource("7_1ywii")
|
||||||
|
stream_3/stream = ExtResource("8_2uf36")
|
||||||
|
|
||||||
[node name="Trap" type="Node2D" unique_id=131165873]
|
[node name="Trap" type="Node2D" unique_id=131165873]
|
||||||
script = ExtResource("1_62q8x")
|
script = ExtResource("1_62q8x")
|
||||||
|
|
||||||
@@ -128,6 +141,17 @@ attenuation = 1.9318731
|
|||||||
bus = &"Sfx"
|
bus = &"Sfx"
|
||||||
|
|
||||||
[node name="SfxDisarming" type="AudioStreamPlayer2D" parent="." unique_id=920322213]
|
[node name="SfxDisarming" type="AudioStreamPlayer2D" parent="." unique_id=920322213]
|
||||||
|
stream = SubResource("AudioStreamRandomizer_crw1k")
|
||||||
|
volume_db = -5.904
|
||||||
|
max_distance = 1860.0
|
||||||
|
attenuation = 1.9318731
|
||||||
|
panning_strength = 1.08
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[node name="SfxDisarmSuccess" type="AudioStreamPlayer2D" parent="." unique_id=920322214]
|
||||||
|
stream = ExtResource("9_disarm")
|
||||||
|
attenuation = 7.0
|
||||||
|
panning_strength = 1.2
|
||||||
bus = &"Sfx"
|
bus = &"Sfx"
|
||||||
|
|
||||||
[connection signal="body_shape_entered" from="ActivationArea" to="." method="_on_activation_area_body_shape_entered"]
|
[connection signal="body_shape_entered" from="ActivationArea" to="." method="_on_activation_area_body_shape_entered"]
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const DIR_ANIMATIONS: Array = [
|
|||||||
var elapsed_time: float = 0.0
|
var elapsed_time: float = 0.0
|
||||||
var player_owner: Node = null
|
var player_owner: Node = null
|
||||||
var hit_targets: Dictionary = {}
|
var hit_targets: Dictionary = {}
|
||||||
|
var _cut_web_rpc_sent: bool = false
|
||||||
|
|
||||||
var damage_effect_axe_scene: PackedScene = preload("res://scenes/damage_effect_axe.tscn")
|
var damage_effect_axe_scene: PackedScene = preload("res://scenes/damage_effect_axe.tscn")
|
||||||
|
|
||||||
@@ -31,6 +32,9 @@ func _ready() -> void:
|
|||||||
if hit_area:
|
if hit_area:
|
||||||
if not hit_area.body_entered.is_connected(_on_damage_area_body_entered):
|
if not hit_area.body_entered.is_connected(_on_damage_area_body_entered):
|
||||||
hit_area.body_entered.connect(_on_damage_area_body_entered)
|
hit_area.body_entered.connect(_on_damage_area_body_entered)
|
||||||
|
if not hit_area.area_entered.is_connected(_on_damage_area_area_entered):
|
||||||
|
hit_area.area_entered.connect(_on_damage_area_area_entered)
|
||||||
|
hit_area.collision_mask = hit_area.collision_mask | 8
|
||||||
if has_node("AttackSwosh"):
|
if has_node("AttackSwosh"):
|
||||||
$AttackSwosh.play()
|
$AttackSwosh.play()
|
||||||
|
|
||||||
@@ -60,9 +64,20 @@ func _process(delta: float) -> void:
|
|||||||
elapsed_time += delta
|
elapsed_time += delta
|
||||||
if player_owner and is_instance_valid(player_owner):
|
if player_owner and is_instance_valid(player_owner):
|
||||||
global_position = player_owner.global_position
|
global_position = player_owner.global_position
|
||||||
|
# So other player can free netted teammate: notify server to cut web
|
||||||
|
if not _cut_web_rpc_sent and multiplayer.has_multiplayer_peer() and not multiplayer.is_server() and elapsed_time >= 0.05:
|
||||||
|
_cut_web_rpc_sent = true
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw and gw.has_method("request_cut_web"):
|
||||||
|
gw.request_cut_web.rpc_id(1, global_position.x, global_position.y, 32.0, player_owner.get_multiplayer_authority())
|
||||||
if elapsed_time >= LIFETIME:
|
if elapsed_time >= LIFETIME:
|
||||||
queue_free()
|
queue_free()
|
||||||
|
|
||||||
|
func _on_damage_area_area_entered(area: Area2D) -> void:
|
||||||
|
if area.has_method("cut_by_attack") and area.get("state") == "hit_player":
|
||||||
|
if player_owner and is_instance_valid(player_owner) and player_owner != area.get("hit_player"):
|
||||||
|
area.cut_by_attack(player_owner)
|
||||||
|
|
||||||
func _on_damage_area_body_entered(body: Node2D) -> void:
|
func _on_damage_area_body_entered(body: Node2D) -> void:
|
||||||
if body == player_owner:
|
if body == player_owner:
|
||||||
return
|
return
|
||||||
@@ -90,6 +105,9 @@ func _on_damage_area_body_entered(body: Node2D) -> void:
|
|||||||
game_world._request_enemy_damage(enemy_name, enemy_index, damage, attacker_pos, false)
|
game_world._request_enemy_damage(enemy_name, enemy_index, damage, attacker_pos, false)
|
||||||
else:
|
else:
|
||||||
game_world._request_enemy_damage.rpc_id(1, enemy_name, enemy_index, damage, attacker_pos, false)
|
game_world._request_enemy_damage.rpc_id(1, enemy_name, enemy_index, damage, attacker_pos, false)
|
||||||
|
# Client: show damage number locally (boss/enemy won't show it on our view otherwise)
|
||||||
|
if game_world and game_world.has_method("show_damage_number_at_position") and not multiplayer.is_server():
|
||||||
|
game_world.show_damage_number_at_position(body.global_position, damage, false)
|
||||||
else:
|
else:
|
||||||
var eid = body.get_multiplayer_authority()
|
var eid = body.get_multiplayer_authority()
|
||||||
if eid != 0:
|
if eid != 0:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ var punch_direction: Vector2 = Vector2.RIGHT
|
|||||||
var player_owner: Node = null
|
var player_owner: Node = null
|
||||||
var hit_targets: Dictionary = {}
|
var hit_targets: Dictionary = {}
|
||||||
var elapsed: float = 0.0
|
var elapsed: float = 0.0
|
||||||
|
var _cut_web_rpc_sent: bool = false
|
||||||
|
|
||||||
var damage_effect_punch_scene: PackedScene = preload("res://scenes/damage_effect_punch.tscn")
|
var damage_effect_punch_scene: PackedScene = preload("res://scenes/damage_effect_punch.tscn")
|
||||||
|
|
||||||
@@ -23,8 +24,12 @@ var damage_effect_punch_scene: PackedScene = preload("res://scenes/damage_effect
|
|||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
if has_node("SfxSwosh"):
|
if has_node("SfxSwosh"):
|
||||||
$SfxSwosh.play()
|
$SfxSwosh.play()
|
||||||
if hit_area and not hit_area.body_entered.is_connected(_on_body_entered):
|
if hit_area:
|
||||||
hit_area.body_entered.connect(_on_body_entered)
|
if not hit_area.body_entered.is_connected(_on_body_entered):
|
||||||
|
hit_area.body_entered.connect(_on_body_entered)
|
||||||
|
if not hit_area.area_entered.is_connected(_on_area_entered):
|
||||||
|
hit_area.area_entered.connect(_on_area_entered)
|
||||||
|
hit_area.collision_mask = hit_area.collision_mask | 8
|
||||||
if sprite and PUNCH_FRAMES.size() > 0:
|
if sprite and PUNCH_FRAMES.size() > 0:
|
||||||
sprite.frame = PUNCH_FRAMES[0]
|
sprite.frame = PUNCH_FRAMES[0]
|
||||||
|
|
||||||
@@ -46,10 +51,21 @@ func _process(delta: float) -> void:
|
|||||||
if sprite and PUNCH_FRAMES.size() > 0:
|
if sprite and PUNCH_FRAMES.size() > 0:
|
||||||
var frame_idx = min(int(elapsed / FRAME_DURATION), PUNCH_FRAMES.size() - 1)
|
var frame_idx = min(int(elapsed / FRAME_DURATION), PUNCH_FRAMES.size() - 1)
|
||||||
sprite.frame = PUNCH_FRAMES[frame_idx]
|
sprite.frame = PUNCH_FRAMES[frame_idx]
|
||||||
|
# So other player can free netted teammate: notify server to cut web (web may only exist on server)
|
||||||
|
if not _cut_web_rpc_sent and multiplayer.has_multiplayer_peer() and not multiplayer.is_server() and player_owner and is_instance_valid(player_owner) and elapsed >= 0.03:
|
||||||
|
_cut_web_rpc_sent = true
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw and gw.has_method("request_cut_web"):
|
||||||
|
gw.request_cut_web.rpc_id(1, global_position.x, global_position.y, 28.0, player_owner.get_multiplayer_authority())
|
||||||
if elapsed >= lifetime:
|
if elapsed >= lifetime:
|
||||||
hit_area.set_deferred("monitoring", false)
|
hit_area.set_deferred("monitoring", false)
|
||||||
queue_free()
|
queue_free()
|
||||||
|
|
||||||
|
func _on_area_entered(area: Area2D) -> void:
|
||||||
|
if area.has_method("cut_by_attack") and area.get("state") == "hit_player":
|
||||||
|
if player_owner and is_instance_valid(player_owner) and player_owner != area.get("hit_player"):
|
||||||
|
area.cut_by_attack(player_owner)
|
||||||
|
|
||||||
func _on_body_entered(body: Node2D) -> void:
|
func _on_body_entered(body: Node2D) -> void:
|
||||||
if body == player_owner:
|
if body == player_owner:
|
||||||
return
|
return
|
||||||
@@ -77,6 +93,9 @@ func _on_body_entered(body: Node2D) -> void:
|
|||||||
game_world._request_enemy_damage(enemy_name, enemy_index, damage, attacker_pos, false)
|
game_world._request_enemy_damage(enemy_name, enemy_index, damage, attacker_pos, false)
|
||||||
else:
|
else:
|
||||||
game_world._request_enemy_damage.rpc_id(1, enemy_name, enemy_index, damage, attacker_pos, false)
|
game_world._request_enemy_damage.rpc_id(1, enemy_name, enemy_index, damage, attacker_pos, false)
|
||||||
|
# Client: show damage number locally (boss/enemy won't show it on our view otherwise)
|
||||||
|
if game_world and game_world.has_method("show_damage_number_at_position") and not multiplayer.is_server():
|
||||||
|
game_world.show_damage_number_at_position(body.global_position, damage, false)
|
||||||
else:
|
else:
|
||||||
var enemy_peer_id = body.get_multiplayer_authority()
|
var enemy_peer_id = body.get_multiplayer_authority()
|
||||||
if enemy_peer_id != 0:
|
if enemy_peer_id != 0:
|
||||||
@@ -92,7 +111,10 @@ func _on_body_entered(body: Node2D) -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Interactables with health (boxes, etc.) - small damage
|
# Interactables with health (boxes, etc.) - small damage
|
||||||
|
# Hidden chests must not be hit until detected (no damage, no effect)
|
||||||
if "health" in body:
|
if "health" in body:
|
||||||
|
if body.get("object_type") == "Chest" and body.get("is_hidden") and not body.get("is_detected"):
|
||||||
|
return
|
||||||
if has_node("SfxImpact"):
|
if has_node("SfxImpact"):
|
||||||
$SfxImpact.play()
|
$SfxImpact.play()
|
||||||
body.health -= damage
|
body.health -= damage
|
||||||
|
|||||||
172
src/scripts/attack_web_shot.gd
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
extends Area2D
|
||||||
|
|
||||||
|
# Web shot from boss: frame 568 in shade_spell_effects (105h x 79v). 8x8 collision.
|
||||||
|
# Flies to target; on hit player or arrival: animate 568->573.
|
||||||
|
# If arrived at point: stay 30s, fade out at end (net doesn't stick during fade).
|
||||||
|
# If hit player: player stuck 5s (frame 679 netted), fade last 0.3s; other player can cut net.
|
||||||
|
# Netted player cannot attack with main weapon.
|
||||||
|
|
||||||
|
var target_position: Vector2 = Vector2.ZERO
|
||||||
|
var speed: float = 140.0
|
||||||
|
var state: String = "flying" # flying | impact_anim | landed_net | hit_player
|
||||||
|
var impact_frame: int = 568
|
||||||
|
var impact_frame_end: int = 573
|
||||||
|
var landed_lifetime: float = 30.0
|
||||||
|
var landed_timer: float = 0.0
|
||||||
|
var fade_start: float = 4.0 # Start fade this many seconds before end
|
||||||
|
var fired_by_boss: Node = null
|
||||||
|
var hit_player: Node = null # When state == hit_player, the netted player
|
||||||
|
var netted_duration: float = 5.0
|
||||||
|
var netted_timer: float = 0.0
|
||||||
|
var netted_fade_start: float = 0.3
|
||||||
|
|
||||||
|
const TILE_SIZE: int = 16
|
||||||
|
const HFRAMES: int = 105
|
||||||
|
const VFRAMES: int = 79
|
||||||
|
|
||||||
|
@onready var sprite: Sprite2D = $Sprite2D
|
||||||
|
@onready var sfx_webbed: AudioStreamPlayer2D = $SfxWebbed
|
||||||
|
@onready var anim_timer: float = 0.0
|
||||||
|
var anim_frame: int = 568
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
# Collision: detect players (layer 1)
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 1
|
||||||
|
body_entered.connect(_on_body_entered)
|
||||||
|
anim_frame = impact_frame
|
||||||
|
if sprite:
|
||||||
|
sprite.hframes = HFRAMES
|
||||||
|
sprite.vframes = VFRAMES
|
||||||
|
sprite.frame = impact_frame
|
||||||
|
sprite.centered = true
|
||||||
|
|
||||||
|
|
||||||
|
func set_target(pos: Vector2) -> void:
|
||||||
|
target_position = pos
|
||||||
|
|
||||||
|
|
||||||
|
func set_fired_by_boss(boss: Node) -> void:
|
||||||
|
fired_by_boss = boss
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if state == "flying":
|
||||||
|
var dir = (target_position - global_position).normalized()
|
||||||
|
var dist = global_position.distance_to(target_position)
|
||||||
|
if dist <= speed * delta:
|
||||||
|
global_position = target_position
|
||||||
|
_impact_at_point()
|
||||||
|
else:
|
||||||
|
global_position += dir * speed * delta
|
||||||
|
return
|
||||||
|
if state == "impact_anim":
|
||||||
|
anim_timer += delta
|
||||||
|
var frame_count = impact_frame_end - impact_frame + 1
|
||||||
|
var duration = 0.25
|
||||||
|
var t = clamp(anim_timer / duration, 0.0, 1.0)
|
||||||
|
var idx = impact_frame + int(t * (frame_count - 1))
|
||||||
|
if sprite:
|
||||||
|
sprite.frame = idx
|
||||||
|
if anim_timer >= duration:
|
||||||
|
state = "landed_net"
|
||||||
|
landed_timer = 0.0
|
||||||
|
collision_layer = 1 # Can be hit by other players to cut?
|
||||||
|
collision_mask = 1 # Detect players (for cutting / fade stick logic)
|
||||||
|
return
|
||||||
|
if state == "landed_net":
|
||||||
|
landed_timer += delta
|
||||||
|
# Fade during last fade_start seconds
|
||||||
|
if landed_timer >= landed_lifetime - fade_start:
|
||||||
|
var fade_t = (landed_timer - (landed_lifetime - fade_start)) / fade_start
|
||||||
|
if sprite:
|
||||||
|
sprite.modulate.a = 1.0 - fade_t
|
||||||
|
# During fade, net doesn't stick - no collision with player for stick
|
||||||
|
if landed_timer >= landed_lifetime:
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
|
if state == "hit_player":
|
||||||
|
netted_timer += delta
|
||||||
|
# Keep net at player position
|
||||||
|
if hit_player and is_instance_valid(hit_player):
|
||||||
|
global_position = hit_player.global_position
|
||||||
|
# Fade last 0.3s
|
||||||
|
if netted_timer >= netted_duration - netted_fade_start:
|
||||||
|
var fade_t = (netted_timer - (netted_duration - netted_fade_start)) / netted_fade_start
|
||||||
|
if sprite:
|
||||||
|
sprite.modulate.a = 1.0 - fade_t
|
||||||
|
if netted_timer >= netted_duration:
|
||||||
|
_release_netted_player()
|
||||||
|
queue_free()
|
||||||
|
else:
|
||||||
|
queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_body_entered(body: Node2D) -> void:
|
||||||
|
if state == "flying":
|
||||||
|
if body.is_in_group("player"):
|
||||||
|
_impact_on_player(body)
|
||||||
|
return
|
||||||
|
# Reached target (or hit wall) - treat as impact at point
|
||||||
|
if global_position.distance_to(target_position) < 24.0:
|
||||||
|
_impact_at_point()
|
||||||
|
return
|
||||||
|
if state == "landed_net" and body.is_in_group("player"):
|
||||||
|
_impact_on_player(body)
|
||||||
|
|
||||||
|
|
||||||
|
func _impact_at_point() -> void:
|
||||||
|
state = "impact_anim"
|
||||||
|
anim_timer = 0.0
|
||||||
|
# Stop moving; play 568->573
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 0
|
||||||
|
# After anim, stay 30s then fade (handled in _process)
|
||||||
|
|
||||||
|
|
||||||
|
func _impact_on_player(player: Node2D) -> void:
|
||||||
|
if not player.is_in_group("player"):
|
||||||
|
return
|
||||||
|
# Already netted?
|
||||||
|
if "netted_by_web" in player and player.netted_by_web != null:
|
||||||
|
return
|
||||||
|
state = "hit_player"
|
||||||
|
hit_player = player
|
||||||
|
netted_timer = 0.0
|
||||||
|
if sfx_webbed:
|
||||||
|
sfx_webbed.play()
|
||||||
|
# Netted: cannot move? User said "player gets stuck" - so disable movement and main weapon
|
||||||
|
if player.has_method("_web_net_apply"):
|
||||||
|
player._web_net_apply(self)
|
||||||
|
else:
|
||||||
|
# Fallback: set a flag player script can check
|
||||||
|
player.set_meta("netted_by_web", self)
|
||||||
|
# Show net on player (frame 679) - player script will show overlay
|
||||||
|
if player.has_method("_web_net_show_netted_frame"):
|
||||||
|
player._web_net_show_netted_frame(true)
|
||||||
|
# Impact anim 568->573 briefly
|
||||||
|
anim_timer = 0.0
|
||||||
|
if sprite:
|
||||||
|
sprite.frame = impact_frame
|
||||||
|
# Layer 8 so player attack areas (punch/sword/axe mask 8) can hit and cut the net
|
||||||
|
collision_layer = 8
|
||||||
|
collision_mask = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _release_netted_player() -> void:
|
||||||
|
if hit_player and is_instance_valid(hit_player):
|
||||||
|
if hit_player.has_method("_web_net_release"):
|
||||||
|
hit_player._web_net_release(self)
|
||||||
|
else:
|
||||||
|
hit_player.remove_meta("netted_by_web")
|
||||||
|
if hit_player.has_method("_web_net_show_netted_frame"):
|
||||||
|
hit_player._web_net_show_netted_frame(false)
|
||||||
|
hit_player = null
|
||||||
|
|
||||||
|
|
||||||
|
# Called when another player attacks this web (to cut the net)
|
||||||
|
func cut_by_attack(_from_player: Node2D) -> void:
|
||||||
|
if state == "hit_player":
|
||||||
|
_release_netted_player()
|
||||||
|
queue_free()
|
||||||
1
src/scripts/attack_web_shot.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bibyqdhticm5i
|
||||||
417
src/scripts/boss_spider_bat.gd
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
extends "res://scripts/enemy_base.gd"
|
||||||
|
|
||||||
|
# Boss Spider Bat: flying boss, activates when player enters ActivationArea.
|
||||||
|
# Flies around, faces target player (randomize every 10s), tries to stay above players.
|
||||||
|
# Animations: down, down_right, right, down_left, left (flip down_right/right for left).
|
||||||
|
# Blinks white on damage. Fires web shots; spawns spiders when previous 3 dead.
|
||||||
|
|
||||||
|
var activated: bool = false
|
||||||
|
var target_switch_timer: float = 0.0
|
||||||
|
const TARGET_SWITCH_INTERVAL: float = 10.0
|
||||||
|
var anim_player: AnimationPlayer = null
|
||||||
|
var spiderbat_sprite: Node2D = null # Spiderbatny
|
||||||
|
var sfx_web_shot: AudioStreamPlayer2D = null
|
||||||
|
var sfx_spawn_spider: AudioStreamPlayer2D = null
|
||||||
|
var boss_room_center: Vector2 = Vector2.ZERO
|
||||||
|
var fly_speed: float = 60.0
|
||||||
|
var prefer_above_offset: float = -80.0 # Prefer this many pixels above target (negative Y)
|
||||||
|
const MIN_DISTANCE_FROM_PLAYER: float = 50.0 # Only back off when very close
|
||||||
|
# Hold one movement target for several seconds so boss doesn't zigzag
|
||||||
|
var current_fly_target: Vector2 = Vector2.ZERO
|
||||||
|
var fly_target_timer: float = 0.0
|
||||||
|
const FLY_TARGET_DURATION: float = 2.8 # Re-pick target every ~3 seconds
|
||||||
|
|
||||||
|
# Web shot attack: only down, down_left, down_right, left, right; fire all 3 at once
|
||||||
|
var web_shot_scene: PackedScene = null
|
||||||
|
var attack_state: String = "idle" # "idle" | "charging_web"
|
||||||
|
var web_attack_timer: float = 0.0
|
||||||
|
const WEB_CHARGE_TIME: float = 0.9 # Vibrate then fire all 3
|
||||||
|
const WEB_FIRE_DISTANCE: float = 180.0 # How far each net travels toward its direction
|
||||||
|
# Directions: down, down_left, down_right, left, right (normalized)
|
||||||
|
const WEB_DIRECTIONS: Array = [
|
||||||
|
Vector2(0, 1), # down
|
||||||
|
Vector2(-0.707, 0.707), # down_left
|
||||||
|
Vector2(0.707, 0.707), # down_right
|
||||||
|
Vector2(-1, 0), # left
|
||||||
|
Vector2(1, 0) # right
|
||||||
|
]
|
||||||
|
# Spider spawn
|
||||||
|
var enemy_spider_scene: PackedScene = null
|
||||||
|
var spawned_spiders: Array = []
|
||||||
|
const SPIDER_SPAWN_COUNT: int = 3
|
||||||
|
const SPIDER_SPAWN_COOLDOWN: float = 18.0 # First spawn after ~18s; same fallback as web for test scenes
|
||||||
|
var spider_spawn_timer: float = 0.0
|
||||||
|
|
||||||
|
# Deterministic RNG from dungeon seed so boss decisions (target, web, fly, spider positions) are identical on host/client if ever needed and for replay consistency
|
||||||
|
var boss_rng: RandomNumberGenerator = null
|
||||||
|
|
||||||
|
|
||||||
|
func _get_boss_rng() -> RandomNumberGenerator:
|
||||||
|
if boss_rng != null:
|
||||||
|
return boss_rng
|
||||||
|
boss_rng = RandomNumberGenerator.new()
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
var base_seed = 0
|
||||||
|
if gw and "dungeon_seed" in gw:
|
||||||
|
base_seed = gw.dungeon_seed
|
||||||
|
var idx = get_meta("enemy_index") if has_meta("enemy_index") else 0
|
||||||
|
boss_rng.seed = base_seed + 90000 + idx # Offset so boss doesn't collide with other RNG use
|
||||||
|
return boss_rng
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
super._ready()
|
||||||
|
max_health = 1200.0
|
||||||
|
current_health = 1200.0
|
||||||
|
# Same collision as normal bats: layer 2 (enemy), mask = players + objects + walls
|
||||||
|
collision_layer = 2
|
||||||
|
collision_mask = 1 | 2 | 64 # Players (1), objects (2), walls (7 = bit 6 = 64)
|
||||||
|
# Boss uses Spiderbatny, not Sprite2D
|
||||||
|
spiderbat_sprite = get_node_or_null("Spiderbatny")
|
||||||
|
if spiderbat_sprite == null:
|
||||||
|
spiderbat_sprite = get_node_or_null("Sprite2D")
|
||||||
|
anim_player = get_node_or_null("AnimationPlayer")
|
||||||
|
sfx_web_shot = get_node_or_null("SfxWebShot")
|
||||||
|
sfx_spawn_spider = get_node_or_null("SfxSpawnSpider")
|
||||||
|
# Ensure we're in enemy group and have is_boss meta (set by game_world from dungeon data)
|
||||||
|
if not has_meta("is_boss"):
|
||||||
|
set_meta("is_boss", true)
|
||||||
|
# Flying: no gravity, stay above ground (like bats)
|
||||||
|
position_z = 20.0
|
||||||
|
# Load web shot and spider scenes
|
||||||
|
if ResourceLoader.exists("res://scenes/attack_web_shot.tscn"):
|
||||||
|
web_shot_scene = load("res://scenes/attack_web_shot.tscn") as PackedScene
|
||||||
|
if ResourceLoader.exists("res://scenes/enemy_spider.tscn"):
|
||||||
|
enemy_spider_scene = load("res://scenes/enemy_spider.tscn") as PackedScene
|
||||||
|
# Start idle (not activated)
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
if anim_player:
|
||||||
|
anim_player.play("down")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_activation_area_body_shape_entered(_body_rid: RID, body: Node2D, _body_shape_index: int, _local_shape_index: int) -> void:
|
||||||
|
if not body.is_in_group("player"):
|
||||||
|
return
|
||||||
|
if activated:
|
||||||
|
return
|
||||||
|
# Only server activates boss
|
||||||
|
if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
|
||||||
|
return
|
||||||
|
# Run intro sequence (camera lerp to boss, scream effect, then start) or activate immediately if no game_world
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("start_boss_intro_sequence"):
|
||||||
|
game_world.start_boss_intro_sequence(self)
|
||||||
|
return
|
||||||
|
# No game_world (e.g. test scene): activate immediately
|
||||||
|
_finish_boss_activation()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_boss_intro_finished() -> void:
|
||||||
|
# Called by game_world after camera + scream sequence; start the boss fight
|
||||||
|
_finish_boss_activation()
|
||||||
|
|
||||||
|
|
||||||
|
func _finish_boss_activation() -> void:
|
||||||
|
activated = true
|
||||||
|
set_meta("boss_activated", true) # HUD shows bar only when activated
|
||||||
|
if has_meta("room") and get_meta("room") is Dictionary:
|
||||||
|
var room = get_meta("room")
|
||||||
|
var tile_size = 16
|
||||||
|
boss_room_center = Vector2(
|
||||||
|
(room.x + room.w / 2.0) * tile_size,
|
||||||
|
(room.y + room.h / 2.0) * tile_size
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
boss_room_center = global_position
|
||||||
|
_pick_new_target()
|
||||||
|
# So joiner sees boss move: sync activated state to clients
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
|
||||||
|
_sync_boss_activated.rpc()
|
||||||
|
# Sync initial boss health so joiner's HUD bar shows full bar
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
var idx = get_meta("enemy_index") if has_meta("enemy_index") else -1
|
||||||
|
if gw and gw.has_method("_rpc_to_ready_peers") and gw.has_method("_sync_boss_health"):
|
||||||
|
gw._rpc_to_ready_peers("_sync_boss_health", [name, idx, current_health, max_health])
|
||||||
|
|
||||||
|
|
||||||
|
func _pick_new_target() -> void:
|
||||||
|
var players = get_tree().get_nodes_in_group("player")
|
||||||
|
var valid: Array = []
|
||||||
|
for p in players:
|
||||||
|
if is_instance_valid(p) and not ("is_dead" in p and p.is_dead):
|
||||||
|
valid.append(p)
|
||||||
|
if valid.is_empty():
|
||||||
|
target_player = null
|
||||||
|
return
|
||||||
|
target_player = valid[_get_boss_rng().randi() % valid.size()]
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
if is_dead:
|
||||||
|
if anim_player and not anim_player.is_playing():
|
||||||
|
pass
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
# Only server runs AI
|
||||||
|
if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
if not activated:
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
# Switch target periodically
|
||||||
|
target_switch_timer += delta
|
||||||
|
if target_switch_timer >= TARGET_SWITCH_INTERVAL:
|
||||||
|
target_switch_timer = 0.0
|
||||||
|
_pick_new_target()
|
||||||
|
# Spider spawn cooldown (only spawn if previous 3 are defeated)
|
||||||
|
spider_spawn_timer += delta
|
||||||
|
_clean_defeated_spiders()
|
||||||
|
if spider_spawn_timer >= SPIDER_SPAWN_COOLDOWN and spawned_spiders.is_empty() and enemy_spider_scene:
|
||||||
|
_spawn_spiders()
|
||||||
|
spider_spawn_timer = 0.0
|
||||||
|
# Attack state machine
|
||||||
|
if attack_state == "charging_web":
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
web_attack_timer += delta
|
||||||
|
if spiderbat_sprite:
|
||||||
|
var r = _get_boss_rng()
|
||||||
|
spiderbat_sprite.position = Vector2(r.randf_range(-2, 2), r.randf_range(-2, 2))
|
||||||
|
if web_attack_timer >= WEB_CHARGE_TIME:
|
||||||
|
web_attack_timer = 0.0
|
||||||
|
# Fire all 3 nets at once in 3 directions: down, down_left, down_right (or include left/right)
|
||||||
|
_Fire_three_nets_at_once()
|
||||||
|
attack_state = "idle"
|
||||||
|
if spiderbat_sprite:
|
||||||
|
spiderbat_sprite.position = Vector2.ZERO
|
||||||
|
if anim_player:
|
||||||
|
anim_player.speed_scale = 1.0
|
||||||
|
move_and_slide()
|
||||||
|
_sync_boss_position_to_clients()
|
||||||
|
return
|
||||||
|
if attack_state == "idle":
|
||||||
|
web_attack_timer += delta
|
||||||
|
if web_attack_timer >= 2.5 and web_shot_scene:
|
||||||
|
web_attack_timer = 0.0
|
||||||
|
if _get_boss_rng().randf() < 0.5:
|
||||||
|
attack_state = "charging_web"
|
||||||
|
web_attack_timer = 0.0
|
||||||
|
if anim_player:
|
||||||
|
anim_player.speed_scale = 2.0
|
||||||
|
# Move: pick a target and hold it for FLY_TARGET_DURATION so we don't zigzag
|
||||||
|
fly_target_timer += delta
|
||||||
|
if fly_target_timer >= FLY_TARGET_DURATION or current_fly_target == Vector2.ZERO or global_position.distance_to(current_fly_target) < 20.0:
|
||||||
|
fly_target_timer = 0.0
|
||||||
|
if target_player and is_instance_valid(target_player):
|
||||||
|
var player_pos = target_player.global_position
|
||||||
|
var to_boss = global_position - player_pos
|
||||||
|
var dist = to_boss.length()
|
||||||
|
# Only back off when very close
|
||||||
|
if dist < MIN_DISTANCE_FROM_PLAYER and dist > 1.0:
|
||||||
|
current_fly_target = global_position + to_boss.normalized() * 80.0
|
||||||
|
else:
|
||||||
|
# Vary target so boss flies in many directions (deterministic from dungeon seed)
|
||||||
|
var r = _get_boss_rng()
|
||||||
|
var strategy = r.randi() % 3
|
||||||
|
if strategy == 0:
|
||||||
|
var side = 1.0 if r.randf() > 0.5 else -1.0
|
||||||
|
current_fly_target = player_pos + Vector2(side * r.randf_range(50.0, 130.0), r.randf_range(prefer_above_offset - 50.0, prefer_above_offset + 30.0))
|
||||||
|
elif strategy == 1:
|
||||||
|
current_fly_target = boss_room_center + Vector2(r.randf_range(-70.0, 70.0), r.randf_range(-90.0, -10.0))
|
||||||
|
else:
|
||||||
|
current_fly_target = player_pos + Vector2(r.randf_range(-110.0, 110.0), r.randf_range(-130.0, -35.0))
|
||||||
|
else:
|
||||||
|
var r = _get_boss_rng()
|
||||||
|
current_fly_target = boss_room_center + Vector2(r.randf_range(-80, 80), r.randf_range(-80, -20))
|
||||||
|
var dir = (current_fly_target - global_position).normalized()
|
||||||
|
velocity = dir * fly_speed
|
||||||
|
_update_facing(dir)
|
||||||
|
move_and_slide()
|
||||||
|
_sync_boss_position_to_clients()
|
||||||
|
|
||||||
|
|
||||||
|
func _sync_boss_position_to_clients() -> void:
|
||||||
|
# Boss overrides _physics_process and doesn't call super, so we must sync position ourselves (like enemy_base)
|
||||||
|
if not multiplayer.has_multiplayer_peer() or not is_multiplayer_authority() or not is_inside_tree():
|
||||||
|
return
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if not gw or not gw.has_method("_rpc_to_ready_peers") or not gw.has_method("_sync_enemy_position"):
|
||||||
|
return
|
||||||
|
var idx = get_meta("enemy_index") if has_meta("enemy_index") else -1
|
||||||
|
gw._rpc_to_ready_peers("_sync_enemy_position", [name, idx, position, velocity, position_z, current_direction, 0, "", 0, -1])
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_boss_activated() -> void:
|
||||||
|
# Clients: apply same activation state so boss moves and shows bar
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
return
|
||||||
|
activated = true
|
||||||
|
set_meta("boss_activated", true)
|
||||||
|
if has_meta("room") and get_meta("room") is Dictionary:
|
||||||
|
var room = get_meta("room")
|
||||||
|
var tile_size = 16
|
||||||
|
boss_room_center = Vector2(
|
||||||
|
(room.x + room.w / 2.0) * tile_size,
|
||||||
|
(room.y + room.h / 2.0) * tile_size
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
boss_room_center = global_position
|
||||||
|
_pick_new_target()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_facing(dir: Vector2) -> void:
|
||||||
|
if not anim_player or not spiderbat_sprite:
|
||||||
|
return
|
||||||
|
# Prefer staying above: if moving up (negative Y), use down/down_left/down_right
|
||||||
|
var anim_name := "down"
|
||||||
|
var flip := 1
|
||||||
|
if abs(dir.x) > 0.2:
|
||||||
|
if dir.x > 0:
|
||||||
|
anim_name = "down_right" if dir.y > -0.3 else "right"
|
||||||
|
flip = 1
|
||||||
|
else:
|
||||||
|
anim_name = "down_left" if dir.y > -0.3 else "left"
|
||||||
|
flip = -1
|
||||||
|
else:
|
||||||
|
anim_name = "down"
|
||||||
|
flip = 1
|
||||||
|
if spiderbat_sprite.scale.x != flip:
|
||||||
|
spiderbat_sprite.scale.x = flip
|
||||||
|
if anim_player.current_animation != anim_name:
|
||||||
|
anim_player.play(anim_name)
|
||||||
|
anim_player.speed_scale = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
func _Fire_three_nets_at_once() -> void:
|
||||||
|
if not web_shot_scene or not is_inside_tree():
|
||||||
|
return
|
||||||
|
if sfx_web_shot:
|
||||||
|
sfx_web_shot.play()
|
||||||
|
var parent_node: Node = null
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world:
|
||||||
|
parent_node = game_world.get_node_or_null("Entities")
|
||||||
|
if not parent_node:
|
||||||
|
parent_node = get_parent()
|
||||||
|
if not parent_node:
|
||||||
|
parent_node = get_tree().current_scene
|
||||||
|
if not parent_node:
|
||||||
|
return
|
||||||
|
# Pick 3 directions from down, down_left, down_right, left, right (no duplicates, deterministic)
|
||||||
|
var indices = [0, 1, 2, 3, 4]
|
||||||
|
var r = _get_boss_rng()
|
||||||
|
for i in range(indices.size() - 1, 0, -1):
|
||||||
|
var j = r.randi() % (i + 1)
|
||||||
|
var t = indices[i]
|
||||||
|
indices[i] = indices[j]
|
||||||
|
indices[j] = t
|
||||||
|
var target_positions: Array[Vector2] = []
|
||||||
|
for i in range(3):
|
||||||
|
var dir: Vector2 = WEB_DIRECTIONS[indices[i]]
|
||||||
|
var target_pos = global_position + dir * WEB_FIRE_DISTANCE
|
||||||
|
target_positions.append(target_pos)
|
||||||
|
var shot = web_shot_scene.instantiate()
|
||||||
|
shot.global_position = global_position
|
||||||
|
if shot.has_method("set_target"):
|
||||||
|
shot.set_target(target_pos)
|
||||||
|
if shot.has_method("set_fired_by_boss"):
|
||||||
|
shot.set_fired_by_boss(self)
|
||||||
|
parent_node.add_child(shot)
|
||||||
|
# Sync web shots to clients so they see the nets
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and gw and gw.has_method("_rpc_to_ready_peers") and target_positions.size() >= 3:
|
||||||
|
var bp = global_position
|
||||||
|
gw._rpc_to_ready_peers("_sync_boss_web_shot", [bp.x, bp.y, target_positions[0].x, target_positions[0].y, target_positions[1].x, target_positions[1].y, target_positions[2].x, target_positions[2].y])
|
||||||
|
|
||||||
|
|
||||||
|
func _clean_defeated_spiders() -> void:
|
||||||
|
for i in range(spawned_spiders.size() - 1, -1, -1):
|
||||||
|
if not is_instance_valid(spawned_spiders[i]) or ("is_dead" in spawned_spiders[i] and spawned_spiders[i].is_dead):
|
||||||
|
spawned_spiders.remove_at(i)
|
||||||
|
|
||||||
|
|
||||||
|
# Pick SPIDER_SPAWN_COUNT positions inside the boss room, near the boss (so spiders don't spawn in walls or outside).
|
||||||
|
func _get_spider_spawn_positions_in_room() -> Array:
|
||||||
|
const TILE_SIZE: int = 16
|
||||||
|
var r = _get_boss_rng()
|
||||||
|
var result: Array = []
|
||||||
|
# Reference position: prefer current boss position, but clamp to room; if no room, use boss_room_center
|
||||||
|
var ref_pos = global_position
|
||||||
|
if has_meta("room") and get_meta("room") is Dictionary:
|
||||||
|
var room: Dictionary = get_meta("room")
|
||||||
|
# Room interior (floor): tiles from (room.x+2, room.y+2) to (room.x+room.w-3, room.y+room.h-3) inclusive
|
||||||
|
var min_wx: float = (room.x + 2) * TILE_SIZE + 8.0
|
||||||
|
var max_wx: float = (room.x + room.w - 2) * TILE_SIZE - 8.0
|
||||||
|
var min_wy: float = (room.y + 2) * TILE_SIZE + 8.0
|
||||||
|
var max_wy: float = (room.y + room.h - 2) * TILE_SIZE - 8.0
|
||||||
|
if min_wx >= max_wx or min_wy >= max_wy:
|
||||||
|
# Fallback: single center tile
|
||||||
|
min_wx = (room.x + room.w / 2) * TILE_SIZE - 8.0
|
||||||
|
max_wx = min_wx + 16.0
|
||||||
|
min_wy = (room.y + room.h / 2) * TILE_SIZE - 8.0
|
||||||
|
max_wy = min_wy + 16.0
|
||||||
|
# Keep ref_pos inside room so "near boss" is still in room when boss is flying near walls
|
||||||
|
ref_pos.x = clampf(ref_pos.x, min_wx, max_wx)
|
||||||
|
ref_pos.y = clampf(ref_pos.y, min_wy, max_wy)
|
||||||
|
for i in range(SPIDER_SPAWN_COUNT):
|
||||||
|
# Random offset near ref (boss), then clamp to room interior
|
||||||
|
var pos = ref_pos + Vector2(r.randf_range(-55.0, 55.0), r.randf_range(-55.0, 55.0))
|
||||||
|
pos.x = clampf(pos.x, min_wx, max_wx)
|
||||||
|
pos.y = clampf(pos.y, min_wy, max_wy)
|
||||||
|
result.append(pos)
|
||||||
|
else:
|
||||||
|
# No room meta (e.g. test scene): spawn near boss
|
||||||
|
for i in range(SPIDER_SPAWN_COUNT):
|
||||||
|
result.append(global_position + Vector2(r.randf_range(-40.0, 40.0), r.randf_range(-20.0, 20.0)))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _spawn_spiders() -> void:
|
||||||
|
if not enemy_spider_scene or not is_inside_tree():
|
||||||
|
return
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
var positions: Array = _get_spider_spawn_positions_in_room()
|
||||||
|
if positions.is_empty():
|
||||||
|
return
|
||||||
|
if sfx_spawn_spider:
|
||||||
|
sfx_spawn_spider.play()
|
||||||
|
# Server: spawn via game_world so clients get RPC and see the spiders
|
||||||
|
if game_world and game_world.has_method("request_spawn_boss_spiders"):
|
||||||
|
spawned_spiders = game_world.request_spawn_boss_spiders(positions)
|
||||||
|
return
|
||||||
|
# Fallback (no game_world or single-player): spawn locally
|
||||||
|
var parent_node: Node = game_world.get_node_or_null("Entities") if game_world else null
|
||||||
|
if not parent_node:
|
||||||
|
parent_node = get_parent()
|
||||||
|
if not parent_node:
|
||||||
|
parent_node = get_tree().current_scene
|
||||||
|
if not parent_node:
|
||||||
|
return
|
||||||
|
for i in range(SPIDER_SPAWN_COUNT):
|
||||||
|
var spider = enemy_spider_scene.instantiate()
|
||||||
|
spider.global_position = positions[i]
|
||||||
|
parent_node.add_child(spider)
|
||||||
|
spawned_spiders.append(spider)
|
||||||
|
|
||||||
|
|
||||||
|
func _flash_damage() -> void:
|
||||||
|
# Boss blinks white (override base red flash)
|
||||||
|
var node_to_flash = spiderbat_sprite if spiderbat_sprite else sprite
|
||||||
|
if node_to_flash:
|
||||||
|
var tween = create_tween()
|
||||||
|
tween.tween_property(node_to_flash, "modulate", Color(2, 2, 2), 0.1)
|
||||||
|
tween.tween_property(node_to_flash, "modulate", Color(1, 1, 1), 0.1)
|
||||||
|
|
||||||
|
|
||||||
|
func _play_death_animation() -> void:
|
||||||
|
# Play boss death animation then free
|
||||||
|
if anim_player and anim_player.has_animation("die"):
|
||||||
|
attack_state = "idle"
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
anim_player.play("die")
|
||||||
|
await anim_player.animation_finished
|
||||||
|
await get_tree().create_timer(0.15).timeout
|
||||||
|
if is_instance_valid(self):
|
||||||
|
queue_free()
|
||||||
1
src/scripts/boss_spider_bat.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bgdwn43m2yrtl
|
||||||
@@ -4,7 +4,7 @@ extends Resource
|
|||||||
signal health_changed(new_health: float, max_health: float)
|
signal health_changed(new_health: float, max_health: float)
|
||||||
signal mana_changed(new_mana: float, max_mana: float)
|
signal mana_changed(new_mana: float, max_mana: float)
|
||||||
signal level_changed(new_level: int)
|
signal level_changed(new_level: int)
|
||||||
signal level_up_stats(stats_increased: Array) # Emitted when leveling up, contains array of stat names that were increased
|
signal level_up_stats(stats_increased: Array) # Emitted when leveling up, contains array of stat names that were increased
|
||||||
signal xp_changed(new_xp: float, xp_to_next: float)
|
signal xp_changed(new_xp: float, xp_to_next: float)
|
||||||
signal no_health
|
signal no_health
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ signal character_changed(char: CharacterStats)
|
|||||||
signal signal_drop_item(item: Item)
|
signal signal_drop_item(item: Item)
|
||||||
|
|
||||||
var character_type: String = "enemy"
|
var character_type: String = "enemy"
|
||||||
var race: String = "Human" # "Dwarf", "Elf", or "Human"
|
var race: String = "Human" # "Dwarf", "Elf", or "Human"
|
||||||
@export var level: int = 1
|
@export var level: int = 1
|
||||||
@export var character_name: String = ""
|
@export var character_name: String = ""
|
||||||
@export var xp: float = 0
|
@export var xp: float = 0
|
||||||
@@ -21,17 +21,17 @@ var race: String = "Human" # "Dwarf", "Elf", or "Human"
|
|||||||
@export var mp: float = 20.0
|
@export var mp: float = 20.0
|
||||||
|
|
||||||
# default skin is human1
|
# default skin is human1
|
||||||
var skin:String = "res://assets/gfx/Puny-Characters/Layer 0 - Skins/Human1.png"
|
var skin: String = "res://assets/gfx/Puny-Characters/Layer 0 - Skins/Human1.png"
|
||||||
# default no values for these:
|
# default no values for these:
|
||||||
var facial_hair:String = ""
|
var facial_hair: String = ""
|
||||||
var facial_hair_color:Color = Color.WHITE
|
var facial_hair_color: Color = Color.WHITE
|
||||||
var hairstyle:String = ""
|
var hairstyle: String = ""
|
||||||
var hair_color:Color = Color.WHITE
|
var hair_color: Color = Color.WHITE
|
||||||
var eyes:String = ""
|
var eyes: String = ""
|
||||||
var eye_color:Color = Color.WHITE
|
var eye_color: Color = Color.WHITE
|
||||||
var eye_lashes:String = ""
|
var eye_lashes: String = ""
|
||||||
var eyelash_color:Color = Color.WHITE
|
var eyelash_color: Color = Color.WHITE
|
||||||
var add_on:String = ""
|
var add_on: String = ""
|
||||||
|
|
||||||
var bonusmaxhp: float = 0.0
|
var bonusmaxhp: float = 0.0
|
||||||
var bonusmaxmp: float = 0.0
|
var bonusmaxmp: float = 0.0
|
||||||
@@ -47,7 +47,7 @@ var pending_stat_points: int = 0
|
|||||||
const LEVEL_UP_STAT_NAMES: Array = ["str", "dex", "int", "end", "wis", "lck", "per"]
|
const LEVEL_UP_STAT_NAMES: Array = ["str", "dex", "int", "end", "wis", "lck", "per"]
|
||||||
|
|
||||||
#calculated values
|
#calculated values
|
||||||
var stats:Array = [
|
var stats: Array = [
|
||||||
5,
|
5,
|
||||||
4,
|
4,
|
||||||
5,
|
5,
|
||||||
@@ -62,7 +62,7 @@ var stats:Array = [
|
|||||||
var inventory: Array = []
|
var inventory: Array = []
|
||||||
|
|
||||||
# mainhand, offhand, headgear, body, feet, accessory (6 total)
|
# mainhand, offhand, headgear, body, feet, accessory (6 total)
|
||||||
var equipment:Dictionary = {
|
var equipment: Dictionary = {
|
||||||
"mainhand": null,
|
"mainhand": null,
|
||||||
"offhand": null,
|
"offhand": null,
|
||||||
"headgear": null,
|
"headgear": null,
|
||||||
@@ -79,7 +79,7 @@ var equipment:Dictionary = {
|
|||||||
"wis": 10,
|
"wis": 10,
|
||||||
"cha": 10,
|
"cha": 10,
|
||||||
"lck": 10,
|
"lck": 10,
|
||||||
"per": 10 # Perception - affects trap detection
|
"per": 10 # Perception - affects trap detection
|
||||||
}
|
}
|
||||||
|
|
||||||
@export var def: int = 0
|
@export var def: int = 0
|
||||||
@@ -137,7 +137,7 @@ func getCalculatedStats():
|
|||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
func get_pass(iStr:String):
|
func get_pass(iStr: String):
|
||||||
var cnt = 0
|
var cnt = 0
|
||||||
if equipment["mainhand"] != null:
|
if equipment["mainhand"] != null:
|
||||||
for key in equipment["mainhand"].modifiers.keys():
|
for key in equipment["mainhand"].modifiers.keys():
|
||||||
@@ -217,11 +217,17 @@ var crit_chance: float:
|
|||||||
get:
|
get:
|
||||||
return (baseStats.lck + get_pass("lck")) * 1.2
|
return (baseStats.lck + get_pass("lck")) * 1.2
|
||||||
|
|
||||||
|
# Temporary dodge buff from potions (added to base dodge chance while active)
|
||||||
|
var buff_dodge_chance: float = 0.0
|
||||||
|
var buff_dodge_chance_remaining: float = 0.0
|
||||||
|
|
||||||
var dodge_chance: float:
|
var dodge_chance: float:
|
||||||
get:
|
get:
|
||||||
# Dodge chance based on DEX (very low % per point, as per user request)
|
# Dodge chance based on DEX (very low % per point, as per user request)
|
||||||
# Each point of DEX gives 0.5% dodge chance (so 20 DEX = 10% dodge)
|
# Each point of DEX gives 0.5% dodge chance (so 20 DEX = 10% dodge)
|
||||||
return (baseStats.dex + get_pass("dex")) * 0.005
|
# Plus active potion buff (e.g. dodge potion +15%)
|
||||||
|
var base_dodge = (baseStats.dex + get_pass("dex")) * 0.005
|
||||||
|
return clamp(base_dodge + buff_dodge_chance, 0.0, 1.0)
|
||||||
|
|
||||||
var hit_chance: float:
|
var hit_chance: float:
|
||||||
get:
|
get:
|
||||||
@@ -230,6 +236,21 @@ var hit_chance: float:
|
|||||||
# Formula: 95% base + (DEX * 0.3%)
|
# Formula: 95% base + (DEX * 0.3%)
|
||||||
return 0.95 + ((baseStats.dex + get_pass("dex")) * 0.003)
|
return 0.95 + ((baseStats.dex + get_pass("dex")) * 0.003)
|
||||||
|
|
||||||
|
func add_buff_dodge_chance(amount: float, duration_sec: float) -> void:
|
||||||
|
"""Apply temporary dodge chance buff (e.g. from dodge potion). Replaces existing dodge buff."""
|
||||||
|
buff_dodge_chance = amount
|
||||||
|
buff_dodge_chance_remaining = duration_sec
|
||||||
|
character_changed.emit(self)
|
||||||
|
|
||||||
|
func tick_buffs(delta: float) -> void:
|
||||||
|
"""Decrease remaining time on temporary buffs; clear when expired."""
|
||||||
|
if buff_dodge_chance_remaining > 0.0:
|
||||||
|
buff_dodge_chance_remaining -= delta
|
||||||
|
if buff_dodge_chance_remaining <= 0.0:
|
||||||
|
buff_dodge_chance_remaining = 0.0
|
||||||
|
buff_dodge_chance = 0.0
|
||||||
|
character_changed.emit(self)
|
||||||
|
|
||||||
var xp_to_next_level: float:
|
var xp_to_next_level: float:
|
||||||
get:
|
get:
|
||||||
# Scale EXP requirements more aggressively - gets harder to level as you go
|
# Scale EXP requirements more aggressively - gets harder to level as you go
|
||||||
@@ -292,12 +313,14 @@ func modify_health(amount: float, allow_overheal: bool = false) -> void:
|
|||||||
if hp <= 0.001:
|
if hp <= 0.001:
|
||||||
hp = 0.0
|
hp = 0.0
|
||||||
health_changed.emit(hp, maxhp)
|
health_changed.emit(hp, maxhp)
|
||||||
character_changed.emit(self)
|
# Do not emit character_changed here - inventory uses health_changed for HP bar only.
|
||||||
|
# Emitting character_changed would trigger full UI refresh on every heal/regen tick and cause blinking.
|
||||||
|
|
||||||
func modify_mana(amount: float) -> void:
|
func modify_mana(amount: float) -> void:
|
||||||
mp = clamp(mp + amount, 0, maxmp)
|
mp = clamp(mp + amount, 0, maxmp)
|
||||||
mana_changed.emit(mp, maxmp)
|
mana_changed.emit(mp, maxmp)
|
||||||
character_changed.emit(self)
|
# Do not emit character_changed here - inventory uses mana_changed for MP bar only.
|
||||||
|
# Emitting character_changed would trigger full UI refresh every regen tick and cause blinking.
|
||||||
|
|
||||||
func calculate_damage(base_damage: float, is_magical: bool = false, is_critical: bool = false) -> float:
|
func calculate_damage(base_damage: float, is_magical: bool = false, is_critical: bool = false) -> float:
|
||||||
# Apply defense reduction - more like D&D where defense provides minimal protection
|
# Apply defense reduction - more like D&D where defense provides minimal protection
|
||||||
@@ -307,7 +330,7 @@ func calculate_damage(base_damage: float, is_magical: bool = false, is_critical:
|
|||||||
if not is_magical:
|
if not is_magical:
|
||||||
# Physical damage: defense provides percentage reduction (like D&D armor)
|
# Physical damage: defense provides percentage reduction (like D&D armor)
|
||||||
# Defense value is converted to percentage: 1 DEF = 2% reduction, max 50% reduction
|
# Defense value is converted to percentage: 1 DEF = 2% reduction, max 50% reduction
|
||||||
var defense_percentage = min(0.5, defense * 0.02) # Max 50% reduction
|
var defense_percentage = min(0.5, defense * 0.02) # Max 50% reduction
|
||||||
var effective_defense = defense_percentage
|
var effective_defense = defense_percentage
|
||||||
if is_critical:
|
if is_critical:
|
||||||
# Critical hits pierce 80% of DEF (only 20% applies)
|
# Critical hits pierce 80% of DEF (only 20% applies)
|
||||||
@@ -327,7 +350,7 @@ func take_damage(amount: float, is_magical: bool = false) -> float:
|
|||||||
modify_health(-actual_damage)
|
modify_health(-actual_damage)
|
||||||
# Check if dead (use epsilon to handle floating point precision)
|
# Check if dead (use epsilon to handle floating point precision)
|
||||||
if hp <= 0.001:
|
if hp <= 0.001:
|
||||||
hp = 0.0 # Ensure exactly 0
|
hp = 0.0 # Ensure exactly 0
|
||||||
no_health.emit() # Emit when health reaches 0
|
no_health.emit() # Emit when health reaches 0
|
||||||
character_changed.emit(self)
|
character_changed.emit(self)
|
||||||
return actual_damage
|
return actual_damage
|
||||||
@@ -346,7 +369,7 @@ func restore_mana(amount: float) -> void:
|
|||||||
|
|
||||||
func saveInventory() -> Array:
|
func saveInventory() -> Array:
|
||||||
var inventorySave = []
|
var inventorySave = []
|
||||||
for it:Item in inventory:
|
for it: Item in inventory:
|
||||||
inventorySave.push_back(it.save())
|
inventorySave.push_back(it.save())
|
||||||
return inventorySave
|
return inventorySave
|
||||||
|
|
||||||
@@ -365,7 +388,7 @@ func loadInventory(iArr: Array):
|
|||||||
inventory.clear() # remove previous content
|
inventory.clear() # remove previous content
|
||||||
for iDic in iArr:
|
for iDic in iArr:
|
||||||
if iDic != null:
|
if iDic != null:
|
||||||
inventory.push_back( Item.new(iDic) )
|
inventory.push_back(Item.new(iDic))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
func loadEquipment(iDic: Dictionary):
|
func loadEquipment(iDic: Dictionary):
|
||||||
@@ -373,7 +396,7 @@ func loadEquipment(iDic: Dictionary):
|
|||||||
equipment["offhand"] = Item.new(iDic.get("offhand")) if iDic.has("offhand") and iDic.get("offhand") != null else null
|
equipment["offhand"] = Item.new(iDic.get("offhand")) if iDic.has("offhand") and iDic.get("offhand") != null else null
|
||||||
equipment["headgear"] = Item.new(iDic.get("headgear")) if iDic.has("headgear") and iDic.get("headgear") != null else null
|
equipment["headgear"] = Item.new(iDic.get("headgear")) if iDic.has("headgear") and iDic.get("headgear") != null else null
|
||||||
equipment["armour"] = Item.new(iDic.get("armour")) if iDic.has("armour") and iDic.get("armour") != null else null
|
equipment["armour"] = Item.new(iDic.get("armour")) if iDic.has("armour") and iDic.get("armour") != null else null
|
||||||
equipment["boots"] = Item.new(iDic.get("boots")) if iDic.has("boots") and iDic.get("boots") != null else null
|
equipment["boots"] = Item.new(iDic.get("boots")) if iDic.has("boots") and iDic.get("boots") != null else null
|
||||||
equipment["accessory"] = Item.new(iDic.get("accessory")) if iDic.has("accessory") and iDic.get("accessory") != null else null
|
equipment["accessory"] = Item.new(iDic.get("accessory")) if iDic.has("accessory") and iDic.get("accessory") != null else null
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -383,7 +406,7 @@ func save() -> Dictionary:
|
|||||||
"character_type": character_type,
|
"character_type": character_type,
|
||||||
"character_name": character_name,
|
"character_name": character_name,
|
||||||
|
|
||||||
"race": race, # Save race
|
"race": race, # Save race
|
||||||
"baseStats": baseStats,
|
"baseStats": baseStats,
|
||||||
|
|
||||||
"hp": hp,
|
"hp": hp,
|
||||||
@@ -408,8 +431,8 @@ func save() -> Dictionary:
|
|||||||
|
|
||||||
"facial_hair_color": facial_hair_color.to_html(true),
|
"facial_hair_color": facial_hair_color.to_html(true),
|
||||||
"hair_color": hair_color.to_html(true),
|
"hair_color": hair_color.to_html(true),
|
||||||
"eye_color": eye_color.to_html(true), # Save eye color
|
"eye_color": eye_color.to_html(true), # Save eye color
|
||||||
"eyelash_color": eyelash_color.to_html(true), # Save eyelash color
|
"eyelash_color": eyelash_color.to_html(true), # Save eyelash color
|
||||||
|
|
||||||
"inventory": saveInventory(),
|
"inventory": saveInventory(),
|
||||||
"equipment": saveEquipment()
|
"equipment": saveEquipment()
|
||||||
@@ -504,29 +527,29 @@ func calculateStats():
|
|||||||
|
|
||||||
pass'
|
pass'
|
||||||
|
|
||||||
func add_coin(iAmount:int):
|
func add_coin(iAmount: int):
|
||||||
coin += iAmount
|
coin += iAmount
|
||||||
emit_signal("character_changed", self)
|
emit_signal("character_changed", self)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
func drop_item(iItem:Item):
|
func drop_item(iItem: Item):
|
||||||
var index = 0
|
var index = 0
|
||||||
for item in inventory:
|
for item in inventory:
|
||||||
if item == iItem:
|
if item == iItem:
|
||||||
break
|
break
|
||||||
index+=1
|
index += 1
|
||||||
inventory.remove_at(index)
|
inventory.remove_at(index)
|
||||||
emit_signal("signal_drop_item", iItem)
|
emit_signal("signal_drop_item", iItem)
|
||||||
emit_signal("character_changed", self)
|
emit_signal("character_changed", self)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
func drop_equipment(iItem:Item):
|
func drop_equipment(iItem: Item):
|
||||||
unequip_item(iItem, false)
|
unequip_item(iItem, false)
|
||||||
# directly remove the item from the inventory
|
# directly remove the item from the inventory
|
||||||
drop_item(iItem)
|
drop_item(iItem)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
func add_item(iItem:Item):
|
func add_item(iItem: Item):
|
||||||
# Try to stack with existing items if possible
|
# Try to stack with existing items if possible
|
||||||
if iItem.can_have_multiple_of:
|
if iItem.can_have_multiple_of:
|
||||||
for existing_item in inventory:
|
for existing_item in inventory:
|
||||||
@@ -570,7 +593,7 @@ func add_item(iItem:Item):
|
|||||||
emit_signal("character_changed", self)
|
emit_signal("character_changed", self)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
func unequip_item(iItem:Item, updateChar:bool = true):
|
func unequip_item(iItem: Item, updateChar: bool = true):
|
||||||
if iItem.equipment_type == Item.EquipmentType.NONE:
|
if iItem.equipment_type == Item.EquipmentType.NONE:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -612,7 +635,7 @@ func forceUpdate():
|
|||||||
emit_signal("character_changed", self)
|
emit_signal("character_changed", self)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
func equip_item(iItem:Item, insert_index: int = -1):
|
func equip_item(iItem: Item, insert_index: int = -1):
|
||||||
# insert_index: if >= 0, place old item at this index instead of at the end
|
# insert_index: if >= 0, place old item at this index instead of at the end
|
||||||
if iItem.equipment_type == Item.EquipmentType.NONE:
|
if iItem.equipment_type == Item.EquipmentType.NONE:
|
||||||
return
|
return
|
||||||
@@ -692,14 +715,14 @@ func equip_item(iItem:Item, insert_index: int = -1):
|
|||||||
emit_signal("character_changed", self)
|
emit_signal("character_changed", self)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
func setSkin(iValue:int):
|
func setSkin(iValue: int):
|
||||||
if iValue < 0 or iValue > 6:
|
if iValue < 0 or iValue > 6:
|
||||||
return
|
return
|
||||||
skin = "res://assets/gfx/Puny-Characters/Layer 0 - Skins/Human" + str(iValue+1) + ".png"
|
skin = "res://assets/gfx/Puny-Characters/Layer 0 - Skins/Human" + str(iValue + 1) + ".png"
|
||||||
emit_signal("character_changed", self)
|
emit_signal("character_changed", self)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
func setFacialHair(iType:int):
|
func setFacialHair(iType: int):
|
||||||
if iType < 0 or iType > 3:
|
if iType < 0 or iType > 3:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -722,7 +745,7 @@ func setFacialHair(iType:int):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
func setHair(iType:int):
|
func setHair(iType: int):
|
||||||
if iType < 0 or iType > 12:
|
if iType < 0 or iType > 12:
|
||||||
return
|
return
|
||||||
if iType == 0:
|
if iType == 0:
|
||||||
@@ -731,7 +754,7 @@ func setHair(iType:int):
|
|||||||
return
|
return
|
||||||
hairstyle = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Hairstyles/FHairstyle" + str(iType)
|
hairstyle = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Hairstyles/FHairstyle" + str(iType)
|
||||||
if iType >= 5: # male hairstyles
|
if iType >= 5: # male hairstyles
|
||||||
hairstyle = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Hairstyles/MHairstyle" + str(iType-4)
|
hairstyle = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Hairstyles/MHairstyle" + str(iType - 4)
|
||||||
hairstyle += "White.png"
|
hairstyle += "White.png"
|
||||||
emit_signal("character_changed", self)
|
emit_signal("character_changed", self)
|
||||||
pass
|
pass
|
||||||
@@ -837,7 +860,7 @@ func setFacialHairColor(iColor: Color):
|
|||||||
facial_hair_color = iColor
|
facial_hair_color = iColor
|
||||||
emit_signal("character_changed", self)
|
emit_signal("character_changed", self)
|
||||||
pass
|
pass
|
||||||
func setHairColor(iColor:Color):
|
func setHairColor(iColor: Color):
|
||||||
hair_color = iColor
|
hair_color = iColor
|
||||||
emit_signal("character_changed", self)
|
emit_signal("character_changed", self)
|
||||||
pass
|
pass
|
||||||
|
|||||||
68
src/scripts/detected_effect.gd
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Visual effect spawned where a player detects something (trap, cracked ground, enemy, hidden chest).
|
||||||
|
# Effect type sets frame range and PointLight2D color (optional light_energy overrides default):
|
||||||
|
# "chest" -> frames 169-179, blue light (hidden chest)
|
||||||
|
# "trap" -> frames 274-284, purple light (trap or cracked ground)
|
||||||
|
# "enemy" -> frames 2123-2135, red light (e.g. enemy_hand)
|
||||||
|
|
||||||
|
const FRAME_RATE: float = 8.0 # frames per second
|
||||||
|
const LIFETIME: float = 30.0
|
||||||
|
const FADE_DURATION: float = 2.0
|
||||||
|
|
||||||
|
# Effect type -> { frames: Array, light_color: Color, optional light_energy: float }
|
||||||
|
const EFFECT_CONFIG: Dictionary = {
|
||||||
|
"chest": {"frames": [169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179], "light_color": Color(0.35, 0.5, 0.95, 1)},
|
||||||
|
"trap": {"frames": [274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284], "light_color": Color(0.7, 0.35, 0.95, 1)},
|
||||||
|
"enemy": {"frames": [2123, 2124, 2125, 2126, 2127, 2128, 2129, 2130, 2131, 2132, 2133, 2134, 2135], "light_color": Color(1.0, 0.1, 0.1, 1), "light_energy": 1.4}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _frames: Array = []
|
||||||
|
var _elapsed: float = 0.0
|
||||||
|
var _fading: bool = false
|
||||||
|
var _fade_elapsed: float = 0.0
|
||||||
|
var _initial_light_energy: float = 0.9
|
||||||
|
|
||||||
|
@onready var fx_sprite: Sprite2D = $FxSprite
|
||||||
|
@onready var detect_light: PointLight2D = $DetectLight
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func setup(world_pos: Vector2, effect_type: String = "chest") -> void:
|
||||||
|
global_position = world_pos
|
||||||
|
if EFFECT_CONFIG.has(effect_type):
|
||||||
|
var cfg = EFFECT_CONFIG[effect_type]
|
||||||
|
_frames = cfg.frames
|
||||||
|
if detect_light:
|
||||||
|
detect_light.color = cfg.light_color
|
||||||
|
if cfg.get("light_energy", 0.0) > 0.0:
|
||||||
|
detect_light.energy = cfg.light_energy
|
||||||
|
else:
|
||||||
|
_frames = EFFECT_CONFIG.chest.frames
|
||||||
|
if fx_sprite and _frames.size() > 0:
|
||||||
|
fx_sprite.frame = _frames[0]
|
||||||
|
if detect_light:
|
||||||
|
_initial_light_energy = detect_light.energy
|
||||||
|
detect_light.enabled = true
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
_elapsed += delta
|
||||||
|
if _fading:
|
||||||
|
_fade_elapsed += delta
|
||||||
|
var t = clampf(_fade_elapsed / FADE_DURATION, 0.0, 1.0)
|
||||||
|
var a = 1.0 - t
|
||||||
|
if fx_sprite:
|
||||||
|
fx_sprite.modulate.a = a
|
||||||
|
if detect_light:
|
||||||
|
detect_light.energy = _initial_light_energy * (1.0 - t)
|
||||||
|
if _fade_elapsed >= FADE_DURATION:
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
|
# Loop frames for this effect type
|
||||||
|
if fx_sprite and _frames.size() > 0:
|
||||||
|
var frame_idx = int(_elapsed * FRAME_RATE) % _frames.size()
|
||||||
|
fx_sprite.frame = _frames[frame_idx]
|
||||||
|
if _elapsed >= LIFETIME:
|
||||||
|
_fading = true
|
||||||
|
_fade_elapsed = 0.0
|
||||||
1
src/scripts/detected_effect.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b45h84vbq3jw
|
||||||
@@ -7,33 +7,33 @@ extends StaticBody2D
|
|||||||
#StoneDoor and GateDoor CAN be opened in start, and become closed when entering it's room
|
#StoneDoor and GateDoor CAN be opened in start, and become closed when entering it's room
|
||||||
#Then you must press a switch in the room or maybe you need to defeat all enemies in the room
|
#Then you must press a switch in the room or maybe you need to defeat all enemies in the room
|
||||||
@export var is_closed: bool = true
|
@export var is_closed: bool = true
|
||||||
var is_closing:bool = false
|
var is_closing: bool = false
|
||||||
var is_opening:bool = false
|
var is_opening: bool = false
|
||||||
var time_to_move:float = 0.2
|
var time_to_move: float = 0.2
|
||||||
var move_timer:float = 0.0
|
var move_timer: float = 0.0
|
||||||
var animation_start_position: Vector2 = Vector2.ZERO # Position when animation started
|
var animation_start_position: Vector2 = Vector2.ZERO # Position when animation started
|
||||||
|
|
||||||
var closed_position: Vector2 = Vector2.ZERO # Position when door is closed (local)
|
var closed_position: Vector2 = Vector2.ZERO # Position when door is closed (local)
|
||||||
var open_offset: Vector2 = Vector2.ZERO # Offset from closed to open position (local)
|
var open_offset: Vector2 = Vector2.ZERO # Offset from closed to open position (local)
|
||||||
|
|
||||||
# Room and puzzle state
|
# Room and puzzle state
|
||||||
var blocking_room: Dictionary = {} # Room this door blocks (the room you enter INTO)
|
var blocking_room: Dictionary = {} # Room this door blocks (the room you enter INTO)
|
||||||
var room1: Dictionary = {} # First room connected by this door (room you leave FROM)
|
var room1: Dictionary = {} # First room connected by this door (room you leave FROM)
|
||||||
var room2: Dictionary = {} # Second room connected by this door (room you enter INTO - the blocking_room)
|
var room2: Dictionary = {} # Second room connected by this door (room you enter INTO - the blocking_room)
|
||||||
var switch_room: Dictionary = {} # Room where the switch is located (before the door)
|
var switch_room: Dictionary = {} # Room where the switch is located (before the door)
|
||||||
var room_trigger_area: Area2D = null # Reference to room trigger area for the blocking room
|
var room_trigger_area: Area2D = null # Reference to room trigger area for the blocking room
|
||||||
var puzzle_solved: bool = false # True when room puzzle is solved
|
var puzzle_solved: bool = false # True when room puzzle is solved
|
||||||
var enemies_defeated: bool = false # True when all enemies in room are defeated
|
var enemies_defeated: bool = false # True when all enemies in room are defeated
|
||||||
var switches_activated: bool = false # True when required switches are activated
|
var switches_activated: bool = false # True when required switches are activated
|
||||||
|
|
||||||
# Key door state
|
# Key door state
|
||||||
var key_used: bool = false # True when key has been used
|
var key_used: bool = false # True when key has been used
|
||||||
var key_indicator: Sprite2D = null # Visual indicator showing key above door
|
var key_indicator: Sprite2D = null # Visual indicator showing key above door
|
||||||
|
|
||||||
# Floor switches this door is connected to
|
# Floor switches this door is connected to
|
||||||
var connected_switches: Array = [] # Array of floor switch nodes
|
var connected_switches: Array = [] # Array of floor switch nodes
|
||||||
var requires_enemies: bool = false # True if door requires defeating enemies to open
|
var requires_enemies: bool = false # True if door requires defeating enemies to open
|
||||||
var requires_switch: bool = false # True if door requires activating switches to open
|
var requires_switch: bool = false # True if door requires activating switches to open
|
||||||
|
|
||||||
# Smoke puff scene for StoneDoor effects
|
# Smoke puff scene for StoneDoor effects
|
||||||
var smoke_puff_scene = preload("res://scenes/smoke_puff.tscn")
|
var smoke_puff_scene = preload("res://scenes/smoke_puff.tscn")
|
||||||
@@ -45,9 +45,9 @@ func _ready() -> void:
|
|||||||
|
|
||||||
# Rotate door first based on direction (original order)
|
# Rotate door first based on direction (original order)
|
||||||
if direction == "Left":
|
if direction == "Left":
|
||||||
self.rotate(-PI/2)
|
self.rotate(-PI / 2)
|
||||||
elif direction == "Right":
|
elif direction == "Right":
|
||||||
self.rotate(PI/2)
|
self.rotate(PI / 2)
|
||||||
elif direction == "Down":
|
elif direction == "Down":
|
||||||
self.rotate(PI)
|
self.rotate(PI)
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ func _ready() -> void:
|
|||||||
# So open_offset is positive Y (door moves down when closing, so open is up)
|
# So open_offset is positive Y (door moves down when closing, so open is up)
|
||||||
# Actually wait - if closed is 16px down from open, then open is 16px up from closed
|
# Actually wait - if closed is 16px down from open, then open is 16px up from closed
|
||||||
# So open_offset should be negative Y (open position is above closed position)
|
# So open_offset should be negative Y (open position is above closed position)
|
||||||
open_offset = Vector2(0, -open_amount) # Open is 16px UP from closed
|
open_offset = Vector2(0, -open_amount) # Open is 16px UP from closed
|
||||||
elif direction == "Down":
|
elif direction == "Down":
|
||||||
# Door on bottom wall:
|
# Door on bottom wall:
|
||||||
# For StoneDoor/GateDoor: open is at (col 1, row 1), closed is at (col 1, row 0)
|
# For StoneDoor/GateDoor: open is at (col 1, row 1), closed is at (col 1, row 0)
|
||||||
@@ -76,19 +76,19 @@ func _ready() -> void:
|
|||||||
# For KeyDoor: closed is at (col 1, row 0), open is at (col 1, row 1)
|
# For KeyDoor: closed is at (col 1, row 0), open is at (col 1, row 1)
|
||||||
# So open is 16px DOWN from closed, open_offset = (0, 16)
|
# So open is 16px DOWN from closed, open_offset = (0, 16)
|
||||||
# NOTE: This is recalculated in _ready_after_setup() based on door type
|
# NOTE: This is recalculated in _ready_after_setup() based on door type
|
||||||
open_offset = Vector2(0, -open_amount) # For StoneDoor/GateDoor: open is 16px DOWN from closed
|
open_offset = Vector2(0, -open_amount) # For StoneDoor/GateDoor: open is 16px DOWN from closed
|
||||||
elif direction == "Left":
|
elif direction == "Left":
|
||||||
# Door on left wall: closed state is 16px RIGHT from open state
|
# Door on left wall: closed state is 16px RIGHT from open state
|
||||||
# So open_offset is positive X (door moves right when closing, so open is left)
|
# So open_offset is positive X (door moves right when closing, so open is left)
|
||||||
# Actually wait - if closed is 16px right from open, then open is 16px left from closed
|
# Actually wait - if closed is 16px right from open, then open is 16px left from closed
|
||||||
# So open_offset should be negative X (open position is left of closed position)
|
# So open_offset should be negative X (open position is left of closed position)
|
||||||
open_offset = Vector2(-open_amount, 0) # Open is 16px LEFT from closed
|
open_offset = Vector2(-open_amount, 0) # Open is 16px LEFT from closed
|
||||||
elif direction == "Right":
|
elif direction == "Right":
|
||||||
# Door on right wall: closed state is 16px LEFT from open state
|
# Door on right wall: closed state is 16px LEFT from open state
|
||||||
# So open_offset is negative X (door moves left when closing, so open is right)
|
# So open_offset is negative X (door moves left when closing, so open is right)
|
||||||
# Actually wait - if closed is 16px left from open, then open is 16px right from closed
|
# Actually wait - if closed is 16px left from open, then open is 16px right from closed
|
||||||
# So open_offset should be positive X (open position is right of closed position)
|
# So open_offset should be positive X (open position is right of closed position)
|
||||||
open_offset = Vector2(open_amount, 0) # Open is 16px RIGHT from closed
|
open_offset = Vector2(open_amount, 0) # Open is 16px RIGHT from closed
|
||||||
|
|
||||||
# Note: closed_position will be set in _ready_after_setup after door is positioned
|
# Note: closed_position will be set in _ready_after_setup after door is positioned
|
||||||
# For now, just initialize it
|
# For now, just initialize it
|
||||||
@@ -169,9 +169,9 @@ func _process(delta: float) -> void:
|
|||||||
var start_pos = animation_start_position if animation_start_position != Vector2.ZERO else closed_position
|
var start_pos = animation_start_position if animation_start_position != Vector2.ZERO else closed_position
|
||||||
var target_pos = closed_position + open_offset
|
var target_pos = closed_position + open_offset
|
||||||
position = start_pos.lerp(target_pos, progress)
|
position = start_pos.lerp(target_pos, progress)
|
||||||
global_position = position # Also update global position during animation
|
global_position = position # Also update global position during animation
|
||||||
# Debug: log for KeyDoors to verify movement
|
# Debug: log for KeyDoors to verify movement
|
||||||
if type == "KeyDoor" and move_timer < 0.1: # Only log once at start of animation
|
if type == "KeyDoor" and move_timer < 0.1: # Only log once at start of animation
|
||||||
LogManager.log("Door: KeyDoor opening animation - start: " + str(start_pos) + ", target: " + str(target_pos) + ", offset: " + str(open_offset) + ", direction: " + str(direction), LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: KeyDoor opening animation - start: " + str(start_pos) + ", target: " + str(target_pos) + ", offset: " + str(open_offset) + ", direction: " + str(direction), LogManager.CATEGORY_DOOR)
|
||||||
|
|
||||||
# For KeyDoors: disable collision as soon as opening starts (allow passage immediately)
|
# For KeyDoors: disable collision as soon as opening starts (allow passage immediately)
|
||||||
@@ -198,7 +198,7 @@ func _process(delta: float) -> void:
|
|||||||
# If animation_start_position wasn't set, calculate open position from closed_position + open_offset
|
# If animation_start_position wasn't set, calculate open position from closed_position + open_offset
|
||||||
var start_pos = animation_start_position if animation_start_position != Vector2.ZERO else (closed_position + open_offset)
|
var start_pos = animation_start_position if animation_start_position != Vector2.ZERO else (closed_position + open_offset)
|
||||||
position = start_pos.lerp(closed_position, progress)
|
position = start_pos.lerp(closed_position, progress)
|
||||||
global_position = position # Also update global position during animation
|
global_position = position # Also update global position during animation
|
||||||
|
|
||||||
# Update collision for StoneDoor/GateDoor only
|
# Update collision for StoneDoor/GateDoor only
|
||||||
if type == "StoneDoor" or type == "GateDoor":
|
if type == "StoneDoor" or type == "GateDoor":
|
||||||
@@ -220,7 +220,7 @@ func _process(delta: float) -> void:
|
|||||||
# Move door to open position (away from closed position)
|
# Move door to open position (away from closed position)
|
||||||
var open_position = closed_position + open_offset
|
var open_position = closed_position + open_offset
|
||||||
position = open_position
|
position = open_position
|
||||||
global_position = open_position # Also set global position
|
global_position = open_position # Also set global position
|
||||||
# When moved from closed position (open), collision should be DISABLED
|
# When moved from closed position (open), collision should be DISABLED
|
||||||
set_collision_layer_value(7, false)
|
set_collision_layer_value(7, false)
|
||||||
var key_used_str = " (key_used=" + str(key_used) + ")" if type == "KeyDoor" else ""
|
var key_used_str = " (key_used=" + str(key_used) + ")" if type == "KeyDoor" else ""
|
||||||
@@ -239,12 +239,12 @@ func _process(delta: float) -> void:
|
|||||||
is_opening = false
|
is_opening = false
|
||||||
is_closing = false
|
is_closing = false
|
||||||
move_timer = 0.0
|
move_timer = 0.0
|
||||||
animation_start_position = Vector2.ZERO # Reset animation start position
|
animation_start_position = Vector2.ZERO # Reset animation start position
|
||||||
else:
|
else:
|
||||||
# Closing animation complete
|
# Closing animation complete
|
||||||
is_closed = true
|
is_closed = true
|
||||||
position = closed_position
|
position = closed_position
|
||||||
global_position = closed_position # Also set global position
|
global_position = closed_position # Also set global position
|
||||||
# When at closed position, collision should be ENABLED
|
# When at closed position, collision should be ENABLED
|
||||||
set_collision_layer_value(7, true)
|
set_collision_layer_value(7, true)
|
||||||
LogManager.log("Door: Closing animation complete - moved to closed position: " + str(closed_position) + " - collision ENABLED", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: Closing animation complete - moved to closed position: " + str(closed_position) + " - collision ENABLED", LogManager.CATEGORY_DOOR)
|
||||||
@@ -257,7 +257,7 @@ func _process(delta: float) -> void:
|
|||||||
is_opening = false
|
is_opening = false
|
||||||
is_closing = false
|
is_closing = false
|
||||||
move_timer = 0.0
|
move_timer = 0.0
|
||||||
animation_start_position = Vector2.ZERO # Reset animation start position
|
animation_start_position = Vector2.ZERO # Reset animation start position
|
||||||
# Now that door has finished closing, check if puzzle is solved (to open it immediately if already solved)
|
# Now that door has finished closing, check if puzzle is solved (to open it immediately if already solved)
|
||||||
if type == "StoneDoor" or type == "GateDoor":
|
if type == "StoneDoor" or type == "GateDoor":
|
||||||
_check_puzzle_state()
|
_check_puzzle_state()
|
||||||
@@ -312,11 +312,11 @@ func _update_collision_based_on_position():
|
|||||||
# CRITICAL: This function ONLY updates collision, it does NOT change position or is_closed flag
|
# CRITICAL: This function ONLY updates collision, it does NOT change position or is_closed flag
|
||||||
# Position and is_closed should only be changed by explicit _open()/_close() calls or animation
|
# Position and is_closed should only be changed by explicit _open()/_close() calls or animation
|
||||||
if type == "KeyDoor":
|
if type == "KeyDoor":
|
||||||
return # Don't update KeyDoors - they handle their own state
|
return # Don't update KeyDoors - they handle their own state
|
||||||
|
|
||||||
# Only update collision, don't change position or is_closed flag
|
# Only update collision, don't change position or is_closed flag
|
||||||
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
|
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
|
||||||
var distance_threshold = 1.0 # Consider "at closed position" if within 1 pixel
|
var distance_threshold = 1.0 # Consider "at closed position" if within 1 pixel
|
||||||
|
|
||||||
if distance_to_closed <= distance_threshold:
|
if distance_to_closed <= distance_threshold:
|
||||||
# Door is at closed position - collision should be ENABLED
|
# Door is at closed position - collision should be ENABLED
|
||||||
@@ -330,7 +330,7 @@ func _update_collision_based_on_position():
|
|||||||
func _open():
|
func _open():
|
||||||
# Only open on server/authority in multiplayer, then sync to clients
|
# Only open on server/authority in multiplayer, then sync to clients
|
||||||
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
|
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
|
||||||
return # Clients wait for RPC
|
return # Clients wait for RPC
|
||||||
|
|
||||||
$TeleporterIntoClosedRoom.is_enabled = false
|
$TeleporterIntoClosedRoom.is_enabled = false
|
||||||
# CRITICAL: For KeyDoors, ensure they start from closed position before opening
|
# CRITICAL: For KeyDoors, ensure they start from closed position before opening
|
||||||
@@ -343,7 +343,7 @@ func _open():
|
|||||||
position = closed_position
|
position = closed_position
|
||||||
global_position = closed_position
|
global_position = closed_position
|
||||||
is_closed = true
|
is_closed = true
|
||||||
set_collision_layer_value(7, true) # Collision enabled at closed position
|
set_collision_layer_value(7, true) # Collision enabled at closed position
|
||||||
LogManager.log("Door: KeyDoor _open() called - reset to closed position " + str(closed_position) + " before opening", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: KeyDoor _open() called - reset to closed position " + str(closed_position) + " before opening", LogManager.CATEGORY_DOOR)
|
||||||
else:
|
else:
|
||||||
LogManager.log_error("Door: KeyDoor _open() called but closed_position is zero!", LogManager.CATEGORY_DOOR)
|
LogManager.log_error("Door: KeyDoor _open() called but closed_position is zero!", LogManager.CATEGORY_DOOR)
|
||||||
@@ -363,7 +363,7 @@ func _open():
|
|||||||
global_position = open_pos
|
global_position = open_pos
|
||||||
is_closed = false
|
is_closed = false
|
||||||
set_collision_layer_value(7, false)
|
set_collision_layer_value(7, false)
|
||||||
return # Don't start animation
|
return # Don't start animation
|
||||||
|
|
||||||
# Door is closed - ensure it's at closed position before opening
|
# Door is closed - ensure it's at closed position before opening
|
||||||
if closed_position != Vector2.ZERO:
|
if closed_position != Vector2.ZERO:
|
||||||
@@ -409,7 +409,7 @@ func _open():
|
|||||||
func _close():
|
func _close():
|
||||||
# Only close on server/authority in multiplayer, then sync to clients
|
# Only close on server/authority in multiplayer, then sync to clients
|
||||||
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
|
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
|
||||||
return # Clients wait for RPC
|
return # Clients wait for RPC
|
||||||
|
|
||||||
# CRITICAL: KeyDoors should NEVER be closed (they only open with a key and stay open)
|
# CRITICAL: KeyDoors should NEVER be closed (they only open with a key and stay open)
|
||||||
if type == "KeyDoor":
|
if type == "KeyDoor":
|
||||||
@@ -424,23 +424,23 @@ func _close():
|
|||||||
|
|
||||||
# Check both flag and actual position to determine door state
|
# Check both flag and actual position to determine door state
|
||||||
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
|
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
|
||||||
var is_actually_at_closed = distance_to_closed < 5.0 # Within 5 pixels of closed position
|
var is_actually_at_closed = distance_to_closed < 5.0 # Within 5 pixels of closed position
|
||||||
|
|
||||||
LogManager.log("Door: _close() called - is_closed: " + str(is_closed) + ", is_actually_at_closed: " + str(is_actually_at_closed) + ", position: " + str(position) + ", closed: " + str(closed_position) + ", distance: " + str(distance_to_closed), LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: _close() called - is_closed: " + str(is_closed) + ", is_actually_at_closed: " + str(is_actually_at_closed) + ", position: " + str(position) + ", closed: " + str(closed_position) + ", distance: " + str(distance_to_closed), LogManager.CATEGORY_DOOR)
|
||||||
|
|
||||||
# If door is already at closed position (both visually and by flag), don't do anything
|
# If door is already at closed position (both visually and by flag), don't do anything
|
||||||
if is_closed and is_actually_at_closed and not is_opening and not is_closing:
|
if is_closed and is_actually_at_closed and not is_opening and not is_closing:
|
||||||
LogManager.log("Door: Already closed (both flag and position match), not closing again", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: Already closed (both flag and position match), not closing again", LogManager.CATEGORY_DOOR)
|
||||||
return # Already closed, don't do anything
|
return # Already closed, don't do anything
|
||||||
|
|
||||||
# CRITICAL: If door is at closed position but flag says open, just fix the state - don't animate
|
# CRITICAL: If door is at closed position but flag says open, just fix the state - don't animate
|
||||||
if is_actually_at_closed and not is_closed:
|
if is_actually_at_closed and not is_closed:
|
||||||
# Door is visually at closed position but flag says open - fix state only
|
# Door is visually at closed position but flag says open - fix state only
|
||||||
LogManager.log("Door: Door is at closed position but flag says open! Fixing state only (no animation)", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: Door is at closed position but flag says open! Fixing state only (no animation)", LogManager.CATEGORY_DOOR)
|
||||||
position = closed_position # Ensure exact position
|
position = closed_position # Ensure exact position
|
||||||
is_closed = true
|
is_closed = true
|
||||||
set_collision_layer_value(7, true)
|
set_collision_layer_value(7, true)
|
||||||
return # Don't start animation
|
return # Don't start animation
|
||||||
|
|
||||||
# Door is actually open (position is away from closed position) - start closing animation
|
# Door is actually open (position is away from closed position) - start closing animation
|
||||||
# CRITICAL: Store starting position BEFORE starting animation
|
# CRITICAL: Store starting position BEFORE starting animation
|
||||||
@@ -493,7 +493,7 @@ func _ready_after_setup():
|
|||||||
# Called after door is fully set up with room references and positioned
|
# Called after door is fully set up with room references and positioned
|
||||||
# NEW LOGIC: Door is positioned at OPEN tile position by game_world
|
# NEW LOGIC: Door is positioned at OPEN tile position by game_world
|
||||||
# The position set by game_world is the OPEN position (initial state for blocking doors)
|
# The position set by game_world is the OPEN position (initial state for blocking doors)
|
||||||
var open_position = position # Current position is the OPEN position (from tile coordinates)
|
var open_position = position # Current position is the OPEN position (from tile coordinates)
|
||||||
|
|
||||||
LogManager.log("Door: _ready_after_setup() called - type: " + str(type) + ", direction: " + str(direction) + ", is_closed: " + str(is_closed) + ", open_position: " + str(open_position), LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: _ready_after_setup() called - type: " + str(type) + ", direction: " + str(direction) + ", is_closed: " + str(is_closed) + ", open_position: " + str(open_position), LogManager.CATEGORY_DOOR)
|
||||||
|
|
||||||
@@ -508,7 +508,7 @@ func _ready_after_setup():
|
|||||||
var closed_offset = Vector2.ZERO
|
var closed_offset = Vector2.ZERO
|
||||||
match direction:
|
match direction:
|
||||||
"Up":
|
"Up":
|
||||||
closed_offset = Vector2(0, 16) # Closed is 16px DOWN from open
|
closed_offset = Vector2(0, 16) # Closed is 16px DOWN from open
|
||||||
"Down":
|
"Down":
|
||||||
# CRITICAL: For StoneDoor/GateDoor, they start OPEN at (col 1, row 1) and close to (col 1, row 0)
|
# CRITICAL: For StoneDoor/GateDoor, they start OPEN at (col 1, row 1) and close to (col 1, row 0)
|
||||||
# So closed is 16px UP from open (negative Y)
|
# So closed is 16px UP from open (negative Y)
|
||||||
@@ -517,20 +517,20 @@ func _ready_after_setup():
|
|||||||
if type == "KeyDoor":
|
if type == "KeyDoor":
|
||||||
# KeyDoor: closed is at row 0, open is at row 1 (16px down)
|
# KeyDoor: closed is at row 0, open is at row 1 (16px down)
|
||||||
# But we calculate from open_position, so this won't be used for KeyDoor
|
# But we calculate from open_position, so this won't be used for KeyDoor
|
||||||
closed_offset = Vector2(0, -16) # Won't be used - KeyDoor uses different logic
|
closed_offset = Vector2(0, -16) # Won't be used - KeyDoor uses different logic
|
||||||
else:
|
else:
|
||||||
# StoneDoor/GateDoor: open is at row 1, closed is at row 0 (16px up)
|
# StoneDoor/GateDoor: open is at row 1, closed is at row 0 (16px up)
|
||||||
closed_offset = Vector2(0, -16) # Closed is 16px UP from open
|
closed_offset = Vector2(0, -16) # Closed is 16px UP from open
|
||||||
"Left":
|
"Left":
|
||||||
closed_offset = Vector2(16, 0) # Closed is 16px RIGHT from open
|
closed_offset = Vector2(16, 0) # Closed is 16px RIGHT from open
|
||||||
"Right":
|
"Right":
|
||||||
closed_offset = Vector2(-16, 0) # Closed is 16px LEFT from open
|
closed_offset = Vector2(-16, 0) # Closed is 16px LEFT from open
|
||||||
|
|
||||||
closed_position = open_position + closed_offset
|
closed_position = open_position + closed_offset
|
||||||
|
|
||||||
# Update open_offset for animation logic (offset from closed to open)
|
# Update open_offset for animation logic (offset from closed to open)
|
||||||
# This is used when opening from closed position
|
# This is used when opening from closed position
|
||||||
open_offset = -closed_offset # open_offset = (0, -16) means open is 16px up from closed
|
open_offset = - closed_offset # open_offset = (0, -16) means open is 16px up from closed
|
||||||
|
|
||||||
LogManager.log("Door: Calculated positions - open: " + str(open_position) + ", closed: " + str(closed_position) + ", closed_offset: " + str(closed_offset) + ", open_offset: " + str(open_offset), LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: Calculated positions - open: " + str(open_position) + ", closed: " + str(closed_position) + ", closed_offset: " + str(closed_offset) + ", open_offset: " + str(open_offset), LogManager.CATEGORY_DOOR)
|
||||||
|
|
||||||
@@ -541,38 +541,38 @@ func _ready_after_setup():
|
|||||||
if type == "KeyDoor":
|
if type == "KeyDoor":
|
||||||
# For KeyDoors, the position from game_world is the CLOSED position
|
# For KeyDoors, the position from game_world is the CLOSED position
|
||||||
# Calculate open position from closed position
|
# Calculate open position from closed position
|
||||||
var keydoor_closed_position = position # Current position is CLOSED (from game_world)
|
var keydoor_closed_position = position # Current position is CLOSED (from game_world)
|
||||||
|
|
||||||
# Calculate open position based on direction
|
# Calculate open position based on direction
|
||||||
var keydoor_open_offset = Vector2.ZERO
|
var keydoor_open_offset = Vector2.ZERO
|
||||||
match direction:
|
match direction:
|
||||||
"Up":
|
"Up":
|
||||||
keydoor_open_offset = Vector2(0, -16) # Open is 16px UP from closed
|
keydoor_open_offset = Vector2(0, -16) # Open is 16px UP from closed
|
||||||
"Down":
|
"Down":
|
||||||
keydoor_open_offset = Vector2(0, 16) # Open is 16px DOWN from closed (row 0 to row 1)
|
keydoor_open_offset = Vector2(0, 16) # Open is 16px DOWN from closed (row 0 to row 1)
|
||||||
"Left":
|
"Left":
|
||||||
keydoor_open_offset = Vector2(-16, 0) # Open is 16px LEFT from closed
|
keydoor_open_offset = Vector2(-16, 0) # Open is 16px LEFT from closed
|
||||||
"Right":
|
"Right":
|
||||||
keydoor_open_offset = Vector2(16, 0) # Open is 16px RIGHT from closed
|
keydoor_open_offset = Vector2(16, 0) # Open is 16px RIGHT from closed
|
||||||
|
|
||||||
# Set positions correctly for KeyDoor
|
# Set positions correctly for KeyDoor
|
||||||
closed_position = keydoor_closed_position # Closed is where game_world placed it
|
closed_position = keydoor_closed_position # Closed is where game_world placed it
|
||||||
open_offset = keydoor_open_offset # Offset to move from closed to open
|
open_offset = keydoor_open_offset # Offset to move from closed to open
|
||||||
|
|
||||||
# KeyDoor starts CLOSED
|
# KeyDoor starts CLOSED
|
||||||
is_closed = true
|
is_closed = true
|
||||||
position = closed_position
|
position = closed_position
|
||||||
global_position = closed_position
|
global_position = closed_position
|
||||||
set_collision_layer_value(7, true) # Collision enabled when closed
|
set_collision_layer_value(7, true) # Collision enabled when closed
|
||||||
LogManager.log("Door: KeyDoor starting CLOSED at position " + str(position) + " (direction: " + str(direction) + "), will open to " + str(closed_position + open_offset) + " - collision ENABLED", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: KeyDoor starting CLOSED at position " + str(position) + " (direction: " + str(direction) + "), will open to " + str(closed_position + open_offset) + " - collision ENABLED", LogManager.CATEGORY_DOOR)
|
||||||
# Create key indicator sprite for KeyDoor
|
# Create key indicator sprite for KeyDoor
|
||||||
_create_key_indicator()
|
_create_key_indicator()
|
||||||
return # Exit early for KeyDoors
|
return # Exit early for KeyDoors
|
||||||
elif is_closed:
|
elif is_closed:
|
||||||
# StoneDoor/GateDoor starting closed (shouldn't happen for blocking doors, but handle it)
|
# StoneDoor/GateDoor starting closed (shouldn't happen for blocking doors, but handle it)
|
||||||
position = closed_position
|
position = closed_position
|
||||||
global_position = closed_position
|
global_position = closed_position
|
||||||
is_closed = true # Ensure state matches position
|
is_closed = true # Ensure state matches position
|
||||||
set_collision_layer_value(7, true)
|
set_collision_layer_value(7, true)
|
||||||
LogManager.log("Door: Starting CLOSED at position " + str(position) + " (type: " + str(type) + ", direction: " + str(direction) + ") - collision ENABLED", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: Starting CLOSED at position " + str(position) + " (type: " + str(type) + ", direction: " + str(direction) + ") - collision ENABLED", LogManager.CATEGORY_DOOR)
|
||||||
else:
|
else:
|
||||||
@@ -584,26 +584,26 @@ func _ready_after_setup():
|
|||||||
LogManager.log("Door: WARNING - Position doesn't match open_position! Forcing to open: " + str(open_position) + " (was: " + str(position) + ")", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: WARNING - Position doesn't match open_position! Forcing to open: " + str(open_position) + " (was: " + str(position) + ")", LogManager.CATEGORY_DOOR)
|
||||||
position = open_position
|
position = open_position
|
||||||
|
|
||||||
global_position = position # Ensure global_position matches position
|
global_position = position # Ensure global_position matches position
|
||||||
is_closed = false # CRITICAL: State MUST be false (open) when at open position
|
is_closed = false # CRITICAL: State MUST be false (open) when at open position
|
||||||
set_collision_layer_value(7, false) # CRITICAL: Collision MUST be DISABLED when open
|
set_collision_layer_value(7, false) # CRITICAL: Collision MUST be DISABLED when open
|
||||||
LogManager.log("Door: Starting OPEN at position " + str(position) + " (closed: " + str(closed_position) + ", open: " + str(open_position) + ", open_offset: " + str(open_offset) + ", type: " + str(type) + ", direction: " + str(direction) + ") - collision DISABLED, is_closed: " + str(is_closed), LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: Starting OPEN at position " + str(position) + " (closed: " + str(closed_position) + ", open: " + str(open_position) + ", open_offset: " + str(open_offset) + ", type: " + str(type) + ", direction: " + str(direction) + ") - collision DISABLED, is_closed: " + str(is_closed), LogManager.CATEGORY_DOOR)
|
||||||
|
|
||||||
# CRITICAL: Verify the door is actually at open position after setting it
|
# CRITICAL: Verify the door is actually at open position after setting it
|
||||||
var actual_distance = position.distance_to(closed_position)
|
var actual_distance = position.distance_to(closed_position)
|
||||||
var expected_distance = 16.0 # Should be 16 pixels away
|
var expected_distance = 16.0 # Should be 16 pixels away
|
||||||
if abs(actual_distance - expected_distance) > 2.0:
|
if abs(actual_distance - expected_distance) > 2.0:
|
||||||
LogManager.log_error("Door: ERROR - Door open/closed distance is wrong! Position: " + str(position) + ", closed: " + str(closed_position) + ", distance: " + str(actual_distance) + " (expected: " + str(expected_distance) + ")", LogManager.CATEGORY_DOOR)
|
LogManager.log_error("Door: ERROR - Door open/closed distance is wrong! Position: " + str(position) + ", closed: " + str(closed_position) + ", distance: " + str(actual_distance) + " (expected: " + str(expected_distance) + ")", LogManager.CATEGORY_DOOR)
|
||||||
# Force it to correct open position
|
# Force it to correct open position
|
||||||
position = open_position
|
position = open_position
|
||||||
global_position = open_position
|
global_position = open_position
|
||||||
is_closed = false # CRITICAL: Ensure state is false when at open position
|
is_closed = false # CRITICAL: Ensure state is false when at open position
|
||||||
set_collision_layer_value(7, false)
|
set_collision_layer_value(7, false)
|
||||||
LogManager.log("Door: FORCED door to open position: " + str(position) + " (distance to closed: " + str(position.distance_to(closed_position)) + ", is_closed: " + str(is_closed) + ")", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: FORCED door to open position: " + str(position) + " (distance to closed: " + str(position.distance_to(closed_position)) + ", is_closed: " + str(is_closed) + ")", LogManager.CATEGORY_DOOR)
|
||||||
|
|
||||||
# FINAL VERIFICATION: Double-check state matches position
|
# FINAL VERIFICATION: Double-check state matches position
|
||||||
var distance_to_closed = position.distance_to(closed_position)
|
var distance_to_closed = position.distance_to(closed_position)
|
||||||
var should_be_open = distance_to_closed > 8.0 # If more than 8px from closed, should be open
|
var should_be_open = distance_to_closed > 8.0 # If more than 8px from closed, should be open
|
||||||
if should_be_open and is_closed:
|
if should_be_open and is_closed:
|
||||||
LogManager.log_error("Door: ERROR - Door is at open position but is_closed is true! Fixing state...", LogManager.CATEGORY_DOOR)
|
LogManager.log_error("Door: ERROR - Door is at open position but is_closed is true! Fixing state...", LogManager.CATEGORY_DOOR)
|
||||||
is_closed = false
|
is_closed = false
|
||||||
@@ -624,7 +624,7 @@ func _ready_after_setup():
|
|||||||
func _create_key_indicator():
|
func _create_key_indicator():
|
||||||
# Create visual indicator for key above door
|
# Create visual indicator for key above door
|
||||||
if key_indicator:
|
if key_indicator:
|
||||||
return # Already created
|
return # Already created
|
||||||
|
|
||||||
key_indicator = Sprite2D.new()
|
key_indicator = Sprite2D.new()
|
||||||
# Load key texture from loot system
|
# Load key texture from loot system
|
||||||
@@ -633,9 +633,9 @@ func _create_key_indicator():
|
|||||||
key_indicator.texture = key_texture
|
key_indicator.texture = key_texture
|
||||||
key_indicator.hframes = 20
|
key_indicator.hframes = 20
|
||||||
key_indicator.vframes = 14
|
key_indicator.vframes = 14
|
||||||
key_indicator.frame = (13 * 20) + 10 # Key frame from loot system
|
key_indicator.frame = (13 * 20) + 10 # Key frame from loot system
|
||||||
key_indicator.position = Vector2(0, -24) # Above door
|
key_indicator.position = Vector2(0, -24) # Above door
|
||||||
key_indicator.visible = false # Hidden until key is used
|
key_indicator.visible = false # Hidden until key is used
|
||||||
add_child(key_indicator)
|
add_child(key_indicator)
|
||||||
|
|
||||||
func _on_room_entered(body):
|
func _on_room_entered(body):
|
||||||
@@ -646,7 +646,7 @@ func _on_room_entered(body):
|
|||||||
|
|
||||||
# Verify this door is in the room we just entered
|
# Verify this door is in the room we just entered
|
||||||
if not room_trigger_area:
|
if not room_trigger_area:
|
||||||
return # No trigger set, don't do anything
|
return # No trigger set, don't do anything
|
||||||
|
|
||||||
var trigger_room = room_trigger_area.room if room_trigger_area.room else {}
|
var trigger_room = room_trigger_area.room if room_trigger_area.room else {}
|
||||||
var door_room1 = room1 if room1 else {}
|
var door_room1 = room1 if room1 else {}
|
||||||
@@ -675,7 +675,7 @@ func _on_room_entered(body):
|
|||||||
if not puzzle_solved:
|
if not puzzle_solved:
|
||||||
# Check both is_closed flag AND actual position to determine door state
|
# Check both is_closed flag AND actual position to determine door state
|
||||||
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
|
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
|
||||||
var is_actually_open = distance_to_closed > 5.0 # If door is more than 5 pixels away from closed_position, it's open
|
var is_actually_open = distance_to_closed > 5.0 # If door is more than 5 pixels away from closed_position, it's open
|
||||||
|
|
||||||
LogManager.log("Door: _on_room_entered() - type: " + str(type) + ", is_closed: " + str(is_closed) + ", is_actually_open: " + str(is_actually_open) + ", position: " + str(position) + ", closed: " + str(closed_position) + ", distance: " + str(distance_to_closed), LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: _on_room_entered() - type: " + str(type) + ", is_closed: " + str(is_closed) + ", is_actually_open: " + str(is_actually_open) + ", position: " + str(position) + ", closed: " + str(closed_position) + ", distance: " + str(distance_to_closed), LogManager.CATEGORY_DOOR)
|
||||||
|
|
||||||
@@ -699,7 +699,7 @@ func _on_room_entered(body):
|
|||||||
_close()
|
_close()
|
||||||
# Don't check puzzle state immediately - wait for door to finish closing
|
# Don't check puzzle state immediately - wait for door to finish closing
|
||||||
# Puzzle state will be checked when closing animation completes (in _process)
|
# Puzzle state will be checked when closing animation completes (in _process)
|
||||||
return # Exit early, don't check puzzle state yet
|
return # Exit early, don't check puzzle state yet
|
||||||
elif is_actually_open:
|
elif is_actually_open:
|
||||||
# Door is open but animation already in progress - don't interfere
|
# Door is open but animation already in progress - don't interfere
|
||||||
LogManager.log("Door: Door is open but animation in progress, not closing", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: Door is open but animation in progress, not closing", LogManager.CATEGORY_DOOR)
|
||||||
@@ -714,7 +714,7 @@ func _on_room_entered(body):
|
|||||||
position = closed_position
|
position = closed_position
|
||||||
global_position = closed_position
|
global_position = closed_position
|
||||||
is_closed = true
|
is_closed = true
|
||||||
set_collision_layer_value(7, true) # Collision ENABLED when closed
|
set_collision_layer_value(7, true) # Collision ENABLED when closed
|
||||||
LogManager.log("Door: Door was already closed - ensuring state is correct, position: " + str(position) + ", closed: " + str(closed_position), LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: Door was already closed - ensuring state is correct, position: " + str(position) + ", closed: " + str(closed_position), LogManager.CATEGORY_DOOR)
|
||||||
# Now that door is confirmed closed, check if puzzle is already solved
|
# Now that door is confirmed closed, check if puzzle is already solved
|
||||||
# CRITICAL: Only check puzzle state if door is closed - don't check if puzzle is already solved
|
# CRITICAL: Only check puzzle state if door is closed - don't check if puzzle is already solved
|
||||||
@@ -732,7 +732,7 @@ func _on_room_exited(body):
|
|||||||
func _check_puzzle_state():
|
func _check_puzzle_state():
|
||||||
# Only check puzzle state on server/authority in multiplayer
|
# Only check puzzle state on server/authority in multiplayer
|
||||||
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
|
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
|
||||||
return # Clients wait for server to check and sync via RPC
|
return # Clients wait for server to check and sync via RPC
|
||||||
|
|
||||||
# CRITICAL: Don't check puzzle state while door is animating (closing or opening)
|
# CRITICAL: Don't check puzzle state while door is animating (closing or opening)
|
||||||
# This prevents race conditions where switch triggers before door finishes closing
|
# This prevents race conditions where switch triggers before door finishes closing
|
||||||
@@ -742,9 +742,9 @@ func _check_puzzle_state():
|
|||||||
|
|
||||||
# Check door's actual state (position-based check is more reliable than flags)
|
# Check door's actual state (position-based check is more reliable than flags)
|
||||||
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
|
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
|
||||||
var is_actually_closed = distance_to_closed < 5.0 # Within 5 pixels of closed position
|
var is_actually_closed = distance_to_closed < 5.0 # Within 5 pixels of closed position
|
||||||
var is_actually_open = distance_to_closed > 5.0 # More than 5 pixels away from closed position
|
var is_actually_open = distance_to_closed > 5.0 # More than 5 pixels away from closed position
|
||||||
var collision_enabled = get_collision_layer_value(7) # Check if collision layer 7 is enabled
|
var collision_enabled = get_collision_layer_value(7) # Check if collision layer 7 is enabled
|
||||||
|
|
||||||
# CRITICAL: If puzzle_solved is true but door is not actually open (not in open position or collision still enabled),
|
# CRITICAL: If puzzle_solved is true but door is not actually open (not in open position or collision still enabled),
|
||||||
# allow switch to trigger again to open the door
|
# allow switch to trigger again to open the door
|
||||||
@@ -804,7 +804,35 @@ func _check_puzzle_state():
|
|||||||
switches_activated = false
|
switches_activated = false
|
||||||
puzzle_solved = false
|
puzzle_solved = false
|
||||||
|
|
||||||
|
func _are_all_enemies_defeated_boss_room() -> bool:
|
||||||
|
# Boss room: check ALL enemies in blocking_room by position (pre-placed boss counts)
|
||||||
|
var target_room = blocking_room if blocking_room and not blocking_room.is_empty() else {}
|
||||||
|
if target_room.is_empty():
|
||||||
|
return false
|
||||||
|
var tile_size = 16
|
||||||
|
var room_min_x = (target_room.x + 2) * tile_size
|
||||||
|
var room_max_x = (target_room.x + target_room.w - 2) * tile_size
|
||||||
|
var room_min_y = (target_room.y + 2) * tile_size
|
||||||
|
var room_max_y = (target_room.y + target_room.h - 2) * tile_size
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
var entities_node = game_world.get_node_or_null("Entities") if game_world else null
|
||||||
|
if not entities_node:
|
||||||
|
return false
|
||||||
|
for child in entities_node.get_children():
|
||||||
|
if not child.is_in_group("enemy"):
|
||||||
|
continue
|
||||||
|
var pos = child.global_position
|
||||||
|
if pos.x < room_min_x or pos.x >= room_max_x or pos.y < room_min_y or pos.y >= room_max_y:
|
||||||
|
continue
|
||||||
|
var is_dead = child.is_dead if "is_dead" in child else (child.is_queued_for_deletion() or not child.is_inside_tree())
|
||||||
|
if not is_dead:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
func _are_all_enemies_defeated() -> bool:
|
func _are_all_enemies_defeated() -> bool:
|
||||||
|
# Boss room door: check ALL enemies in room (pre-placed boss), not just spawner-spawned
|
||||||
|
if has_meta("boss_room_door") and get_meta("boss_room_door"):
|
||||||
|
return _are_all_enemies_defeated_boss_room()
|
||||||
# Check if all enemies spawned from spawners in the puzzle room are defeated
|
# Check if all enemies spawned from spawners in the puzzle room are defeated
|
||||||
# CRITICAL: Only check enemies that were SPAWNED from spawners (not pre-spawned enemies)
|
# CRITICAL: Only check enemies that were SPAWNED from spawners (not pre-spawned enemies)
|
||||||
# Use room1 (the room this door is IN) or blocking_room for checking enemies
|
# Use room1 (the room this door is IN) or blocking_room for checking enemies
|
||||||
@@ -829,7 +857,7 @@ func _are_all_enemies_defeated() -> bool:
|
|||||||
if child.is_in_group("enemy"):
|
if child.is_in_group("enemy"):
|
||||||
# CRITICAL: Only check enemies that were spawned from spawners (not pre-spawned)
|
# CRITICAL: Only check enemies that were spawned from spawners (not pre-spawned)
|
||||||
if not child.has_meta("spawned_from_spawner") or not child.get_meta("spawned_from_spawner"):
|
if not child.has_meta("spawned_from_spawner") or not child.get_meta("spawned_from_spawner"):
|
||||||
continue # Skip pre-spawned enemies
|
continue # Skip pre-spawned enemies
|
||||||
|
|
||||||
# Check if enemy is in this room (use position-based check, more reliable)
|
# Check if enemy is in this room (use position-based check, more reliable)
|
||||||
var enemy_in_room = false
|
var enemy_in_room = false
|
||||||
@@ -849,7 +877,7 @@ func _are_all_enemies_defeated() -> bool:
|
|||||||
var spawner_name = child.get_meta("spawner_name")
|
var spawner_name = child.get_meta("spawner_name")
|
||||||
# Spawner names are like "EnemySpawner_<room_x>_<room_y>"
|
# Spawner names are like "EnemySpawner_<room_x>_<room_y>"
|
||||||
if str(target_room.x) in spawner_name and str(target_room.y) in spawner_name:
|
if str(target_room.x) in spawner_name and str(target_room.y) in spawner_name:
|
||||||
enemy_in_room = true # Confirmed by spawner name
|
enemy_in_room = true # Confirmed by spawner name
|
||||||
|
|
||||||
if enemy_in_room:
|
if enemy_in_room:
|
||||||
room_spawned_enemies.append(child)
|
room_spawned_enemies.append(child)
|
||||||
@@ -874,12 +902,12 @@ func _are_all_enemies_defeated() -> bool:
|
|||||||
|
|
||||||
if not enemy_is_dead:
|
if not enemy_is_dead:
|
||||||
LogManager.log("Door: Enemy " + str(enemy.name) + " is still alive - puzzle not solved yet", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: Enemy " + str(enemy.name) + " is still alive - puzzle not solved yet", LogManager.CATEGORY_DOOR)
|
||||||
return false # Enemy is still alive, puzzle not solved
|
return false # Enemy is still alive, puzzle not solved
|
||||||
|
|
||||||
# If we have enemies and all are dead, puzzle is solved
|
# If we have enemies and all are dead, puzzle is solved
|
||||||
if room_spawned_enemies.size() > 0:
|
if room_spawned_enemies.size() > 0:
|
||||||
LogManager.log("Door: All " + str(room_spawned_enemies.size()) + " spawned enemies are dead! Puzzle solved!", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: All " + str(room_spawned_enemies.size()) + " spawned enemies are dead! Puzzle solved!", LogManager.CATEGORY_DOOR)
|
||||||
return true # All enemies found are dead
|
return true # All enemies found are dead
|
||||||
|
|
||||||
# No spawned enemies found - check if spawners have actually spawned enemies before
|
# No spawned enemies found - check if spawners have actually spawned enemies before
|
||||||
# CRITICAL: Only consider puzzle solved if spawners have spawned enemies and they're all dead
|
# CRITICAL: Only consider puzzle solved if spawners have spawned enemies and they're all dead
|
||||||
@@ -913,7 +941,7 @@ func _are_all_enemies_defeated() -> bool:
|
|||||||
enemy_tile_y >= room_min_y and enemy_tile_y < room_max_y)
|
enemy_tile_y >= room_min_y and enemy_tile_y < room_max_y)
|
||||||
|
|
||||||
if not enemy_in_room:
|
if not enemy_in_room:
|
||||||
continue # Skip enemies not in this room
|
continue # Skip enemies not in this room
|
||||||
|
|
||||||
# Check if enemy is alive
|
# Check if enemy is alive
|
||||||
var enemy_is_alive = false
|
var enemy_is_alive = false
|
||||||
@@ -932,7 +960,6 @@ func _are_all_enemies_defeated() -> bool:
|
|||||||
# No spawned enemies found - check if spawners have actually spawned enemies before
|
# No spawned enemies found - check if spawners have actually spawned enemies before
|
||||||
# CRITICAL: Only consider puzzle solved if spawners have spawned enemies and they're all dead
|
# CRITICAL: Only consider puzzle solved if spawners have spawned enemies and they're all dead
|
||||||
# Don't solve if spawners haven't spawned yet (e.g., spawn_on_ready=false and player hasn't entered room)
|
# Don't solve if spawners haven't spawned yet (e.g., spawn_on_ready=false and player hasn't entered room)
|
||||||
|
|
||||||
var spawners_in_room = []
|
var spawners_in_room = []
|
||||||
var spawners_that_have_spawned = []
|
var spawners_that_have_spawned = []
|
||||||
|
|
||||||
@@ -1026,7 +1053,7 @@ func _are_all_enemies_defeated() -> bool:
|
|||||||
enemy_tile_y >= room_min_y and enemy_tile_y < room_max_y)
|
enemy_tile_y >= room_min_y and enemy_tile_y < room_max_y)
|
||||||
|
|
||||||
if not enemy_in_room:
|
if not enemy_in_room:
|
||||||
continue # Skip enemies not in this room
|
continue # Skip enemies not in this room
|
||||||
|
|
||||||
# Check if enemy is dead
|
# Check if enemy is dead
|
||||||
var enemy_is_dead = false
|
var enemy_is_dead = false
|
||||||
@@ -1053,8 +1080,8 @@ func _are_all_enemies_defeated() -> bool:
|
|||||||
# Found dead enemies with matching spawner names - spawners definitely spawned and enemies are dead
|
# Found dead enemies with matching spawner names - spawners definitely spawned and enemies are dead
|
||||||
if spawners_in_room.size() == 0:
|
if spawners_in_room.size() == 0:
|
||||||
for spawner_name in unique_spawner_names_that_spawned.keys():
|
for spawner_name in unique_spawner_names_that_spawned.keys():
|
||||||
spawners_in_room.append(null) # Placeholder for destroyed spawner
|
spawners_in_room.append(null) # Placeholder for destroyed spawner
|
||||||
spawners_that_have_spawned.append(null) # Count as spawned
|
spawners_that_have_spawned.append(null) # Count as spawned
|
||||||
LogManager.log("Door: Spawner " + str(spawner_name) + " was destroyed but spawned enemies that are now all dead - counting as spawned", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: Spawner " + str(spawner_name) + " was destroyed but spawned enemies that are now all dead - counting as spawned", LogManager.CATEGORY_DOOR)
|
||||||
else:
|
else:
|
||||||
# Spawners exist - check if any weren't counted as spawned yet
|
# Spawners exist - check if any weren't counted as spawned yet
|
||||||
@@ -1122,14 +1149,14 @@ func _are_all_enemies_defeated() -> bool:
|
|||||||
LogManager.log("Door: Enemy is no longer valid (removed from scene) - counting as dead", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: Enemy is no longer valid (removed from scene) - counting as dead", LogManager.CATEGORY_DOOR)
|
||||||
|
|
||||||
LogManager.log("Door: All " + str(room_spawned_enemies.size()) + " spawned enemies are dead! Puzzle solved!", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: All " + str(room_spawned_enemies.size()) + " spawned enemies are dead! Puzzle solved!", LogManager.CATEGORY_DOOR)
|
||||||
return true # All enemies are dead
|
return true # All enemies are dead
|
||||||
|
|
||||||
func _spawn_smoke_puffs_on_close():
|
func _spawn_smoke_puffs_on_close():
|
||||||
# Spawn 1-3 smoke puffs when StoneDoor finishes closing
|
# Spawn 1-3 smoke puffs when StoneDoor finishes closing
|
||||||
if not smoke_puff_scene:
|
if not smoke_puff_scene:
|
||||||
return
|
return
|
||||||
|
|
||||||
var puff_count = randi_range(1, 3) # Random between 1-3 puffs
|
var puff_count = randi_range(1, 3) # Random between 1-3 puffs
|
||||||
|
|
||||||
for i in range(puff_count):
|
for i in range(puff_count):
|
||||||
var puff = smoke_puff_scene.instantiate()
|
var puff = smoke_puff_scene.instantiate()
|
||||||
@@ -1138,7 +1165,7 @@ func _spawn_smoke_puffs_on_close():
|
|||||||
var offset_x = randf_range(-8, 8)
|
var offset_x = randf_range(-8, 8)
|
||||||
var offset_y = randf_range(-8, 8)
|
var offset_y = randf_range(-8, 8)
|
||||||
puff.global_position = global_position + Vector2(offset_x, offset_y)
|
puff.global_position = global_position + Vector2(offset_x, offset_y)
|
||||||
puff.z_index = 10 # High z-index to ensure visibility
|
puff.z_index = 10 # High z-index to ensure visibility
|
||||||
|
|
||||||
# Add to Entities node for proper layering
|
# Add to Entities node for proper layering
|
||||||
var entities_node = get_tree().get_first_node_in_group("game_world")
|
var entities_node = get_tree().get_first_node_in_group("game_world")
|
||||||
@@ -1158,7 +1185,7 @@ func _spawn_smoke_puffs_on_open():
|
|||||||
if not smoke_puff_scene:
|
if not smoke_puff_scene:
|
||||||
return
|
return
|
||||||
|
|
||||||
var puff_count = randi_range(1, 2) # Random between 1-2 puffs
|
var puff_count = randi_range(1, 2) # Random between 1-2 puffs
|
||||||
|
|
||||||
for i in range(puff_count):
|
for i in range(puff_count):
|
||||||
var puff = smoke_puff_scene.instantiate()
|
var puff = smoke_puff_scene.instantiate()
|
||||||
@@ -1167,7 +1194,7 @@ func _spawn_smoke_puffs_on_open():
|
|||||||
var offset_x = randf_range(-8, 8)
|
var offset_x = randf_range(-8, 8)
|
||||||
var offset_y = randf_range(-8, 8)
|
var offset_y = randf_range(-8, 8)
|
||||||
puff.global_position = global_position + Vector2(offset_x, offset_y)
|
puff.global_position = global_position + Vector2(offset_x, offset_y)
|
||||||
puff.z_index = 10 # High z-index to ensure visibility
|
puff.z_index = 10 # High z-index to ensure visibility
|
||||||
|
|
||||||
# Add to Entities node for proper layering
|
# Add to Entities node for proper layering
|
||||||
var entities_node = get_tree().get_first_node_in_group("game_world")
|
var entities_node = get_tree().get_first_node_in_group("game_world")
|
||||||
@@ -1199,12 +1226,12 @@ func _are_all_switches_activated() -> bool:
|
|||||||
LogManager.log("Door: Switch " + str(switch.name) + " is NOT activated", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: Switch " + str(switch.name) + " is NOT activated", LogManager.CATEGORY_DOOR)
|
||||||
return false
|
return false
|
||||||
LogManager.log("Door: All connected switches are activated!", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: All connected switches are activated!", LogManager.CATEGORY_DOOR)
|
||||||
return true # All connected switches are activated
|
return true # All connected switches are activated
|
||||||
|
|
||||||
# CRITICAL: If no switches are connected, the puzzle is NOT solved!
|
# CRITICAL: If no switches are connected, the puzzle is NOT solved!
|
||||||
# Switches should ALWAYS be connected when spawned - if they're not, it's an error
|
# Switches should ALWAYS be connected when spawned - if they're not, it's an error
|
||||||
LogManager.log("Door: WARNING - Door " + str(name) + " has no connected switches! Puzzle cannot be solved!", LogManager.CATEGORY_DOOR)
|
LogManager.log("Door: WARNING - Door " + str(name) + " has no connected switches! Puzzle cannot be solved!", LogManager.CATEGORY_DOOR)
|
||||||
return false # No connected switches means puzzle is NOT solved
|
return false # No connected switches means puzzle is NOT solved
|
||||||
|
|
||||||
func _on_key_interaction_area_body_entered(body):
|
func _on_key_interaction_area_body_entered(body):
|
||||||
# Player entered key interaction area
|
# Player entered key interaction area
|
||||||
@@ -1228,7 +1255,7 @@ func _show_key_indicator():
|
|||||||
key_indicator.visible = true
|
key_indicator.visible = true
|
||||||
# Make sure it's on top (higher z-index or add to front)
|
# Make sure it's on top (higher z-index or add to front)
|
||||||
key_indicator.z_index = 10
|
key_indicator.z_index = 10
|
||||||
move_child(key_indicator, get_child_count() - 1) # Move to front
|
move_child(key_indicator, get_child_count() - 1) # Move to front
|
||||||
else:
|
else:
|
||||||
# Create key indicator if it doesn't exist yet
|
# Create key indicator if it doesn't exist yet
|
||||||
_create_key_indicator()
|
_create_key_indicator()
|
||||||
@@ -1244,13 +1271,13 @@ func teleportPlayer(body: Node2D):
|
|||||||
var keydoor_open_offset = Vector2.ZERO
|
var keydoor_open_offset = Vector2.ZERO
|
||||||
match direction:
|
match direction:
|
||||||
"Up":
|
"Up":
|
||||||
keydoor_open_offset = Vector2(0, 16) # Open is 16px UP from closed
|
keydoor_open_offset = Vector2(0, 16) # Open is 16px UP from closed
|
||||||
"Down":
|
"Down":
|
||||||
keydoor_open_offset = Vector2(0, -16) # Open is 16px DOWN from closed (row 0 to row 1)
|
keydoor_open_offset = Vector2(0, -16) # Open is 16px DOWN from closed (row 0 to row 1)
|
||||||
"Left":
|
"Left":
|
||||||
keydoor_open_offset = Vector2(16, 0) # Open is 16px LEFT from closed
|
keydoor_open_offset = Vector2(16, 0) # Open is 16px LEFT from closed
|
||||||
"Right":
|
"Right":
|
||||||
keydoor_open_offset = Vector2(-16, 0) # Open is 16px RIGHT from closed
|
keydoor_open_offset = Vector2(-16, 0) # Open is 16px RIGHT from closed
|
||||||
|
|
||||||
var new_position = self.global_position + keydoor_open_offset
|
var new_position = self.global_position + keydoor_open_offset
|
||||||
|
|
||||||
@@ -1284,6 +1311,8 @@ func _sync_door_open():
|
|||||||
var is_actually_open = distance_to_closed > 5.0
|
var is_actually_open = distance_to_closed > 5.0
|
||||||
|
|
||||||
if not is_actually_open and not is_opening:
|
if not is_actually_open and not is_opening:
|
||||||
|
# Disable teleporter (stops particle emitter) when door opens
|
||||||
|
$TeleporterIntoClosedRoom.is_enabled = false
|
||||||
# Door is closed - open it
|
# Door is closed - open it
|
||||||
if closed_position != Vector2.ZERO:
|
if closed_position != Vector2.ZERO:
|
||||||
position = closed_position
|
position = closed_position
|
||||||
|
|||||||
@@ -54,10 +54,19 @@ var anim_speed: float = 0.15 # Seconds per frame
|
|||||||
@onready var collision_shape = get_node_or_null("CollisionShape2D")
|
@onready var collision_shape = get_node_or_null("CollisionShape2D")
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
|
# Capture max_health set by spawn data (game_world sets it before add_child); _initialize_character_stats overwrites it
|
||||||
|
var spawn_max_health = max_health
|
||||||
# Initialize CharacterStats for RPG system
|
# Initialize CharacterStats for RPG system
|
||||||
_initialize_character_stats()
|
_initialize_character_stats()
|
||||||
|
# If spawn/scene set a higher max_health (e.g. boss with 1200), apply it to character_stats so take_damage uses it
|
||||||
current_health = max_health
|
if spawn_max_health > 0 and spawn_max_health > character_stats.maxhp:
|
||||||
|
var base_max = (character_stats.baseStats.end + character_stats.get_pass("end")) * 3.0 + character_stats.get_pass("maxhp")
|
||||||
|
character_stats.bonusmaxhp = spawn_max_health - base_max
|
||||||
|
character_stats.hp = character_stats.maxhp
|
||||||
|
max_health = character_stats.maxhp
|
||||||
|
current_health = character_stats.hp
|
||||||
|
else:
|
||||||
|
current_health = max_health
|
||||||
add_to_group("enemy")
|
add_to_group("enemy")
|
||||||
|
|
||||||
# Setup shadow (if it exists - some enemies like humanoids don't have a Shadow node)
|
# Setup shadow (if it exists - some enemies like humanoids don't have a Shadow node)
|
||||||
@@ -73,6 +82,19 @@ func _ready():
|
|||||||
# Walls are on layer 7 (bit 6 = 64), not layer 4!
|
# Walls are on layer 7 (bit 6 = 64), not layer 4!
|
||||||
collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
|
collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
|
||||||
|
|
||||||
|
# Ensure SfxFallout exists for all enemies (LA_Enemy_Fall.wav when falling into fallout)
|
||||||
|
var fall_sfx = get_node_or_null("SfxFallout")
|
||||||
|
if fall_sfx == null:
|
||||||
|
fall_sfx = AudioStreamPlayer2D.new()
|
||||||
|
fall_sfx.name = "SfxFallout"
|
||||||
|
add_child(fall_sfx)
|
||||||
|
var stream = load("res://assets/audio/sfx/z3/LA_Enemy_Fall.wav") as AudioStream
|
||||||
|
if stream:
|
||||||
|
fall_sfx.stream = stream
|
||||||
|
fall_sfx.attenuation = 7.0
|
||||||
|
fall_sfx.panning_strength = 1.14
|
||||||
|
fall_sfx.bus = "Sfx"
|
||||||
|
|
||||||
# Initialize CharacterStats for this enemy
|
# Initialize CharacterStats for this enemy
|
||||||
# Override in subclasses to set specific baseStats
|
# Override in subclasses to set specific baseStats
|
||||||
func _initialize_character_stats():
|
func _initialize_character_stats():
|
||||||
@@ -127,70 +149,9 @@ func _physics_process(delta):
|
|||||||
burn_sprite.set_meta("burn_animation_timer", anim_timer)
|
burn_sprite.set_meta("burn_animation_timer", anim_timer)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Fallout: humanoid sinks like player (FALL anim) then dies; slime rotates 45 + scale then dies
|
# Fallout: humanoid sinks like player; slime/spider/rat rotate 45 + scale then die. Subclasses that override _physics_process (e.g. spider) must call _check_and_handle_fallout(delta) and return if true.
|
||||||
if fallout_state:
|
if _check_and_handle_fallout(delta):
|
||||||
velocity = Vector2.ZERO
|
|
||||||
fallout_scale_progress -= delta / FALLOUT_SINK_DURATION
|
|
||||||
if fallout_scale_progress <= 0.0:
|
|
||||||
died_from_fallout = true
|
|
||||||
call_deferred("_die")
|
|
||||||
return
|
|
||||||
scale = Vector2.ONE * max(0.0, fallout_scale_progress)
|
|
||||||
if has_method("_set_animation"):
|
|
||||||
_set_animation("FALL")
|
|
||||||
move_and_slide()
|
|
||||||
return
|
return
|
||||||
if fallout_defeat_started:
|
|
||||||
velocity = Vector2.ZERO
|
|
||||||
fallout_scale_progress -= delta / FALLOUT_SINK_DURATION
|
|
||||||
if fallout_scale_progress <= 0.0:
|
|
||||||
died_from_fallout = true
|
|
||||||
call_deferred("_die")
|
|
||||||
return
|
|
||||||
scale = Vector2.ONE * max(0.0, fallout_scale_progress)
|
|
||||||
rotation = deg_to_rad(45.0)
|
|
||||||
move_and_slide()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Only ground enemies (position_z <= 0) can fall into fallout; bat has position_z 1 and ignores it
|
|
||||||
# Humanoid: use 16x16 box check (like player) so any part on fallout triggers; drag toward center then sink
|
|
||||||
var gw = get_tree().get_first_node_in_group("game_world")
|
|
||||||
var on_fallout = false
|
|
||||||
if position_z <= 0.0 and gw:
|
|
||||||
if "humanoid_type" in self and gw.has_method("_is_player_box_on_fallout_tile"):
|
|
||||||
on_fallout = gw._is_player_box_on_fallout_tile(global_position, 8.0)
|
|
||||||
elif gw.has_method("_is_position_on_fallout_tile"):
|
|
||||||
on_fallout = gw._is_position_on_fallout_tile(global_position)
|
|
||||||
if on_fallout:
|
|
||||||
if "humanoid_type" in self:
|
|
||||||
# Humanoid: drag toward tile center (quicksand pull) then sink when at center
|
|
||||||
var tile_center = gw._get_closest_fallout_tile_center(global_position) if gw.has_method("_get_closest_fallout_tile_center") else (gw._get_tile_center_at(global_position) if gw.has_method("_get_tile_center_at") else global_position)
|
|
||||||
var dist_to_center = global_position.distance_to(tile_center)
|
|
||||||
if dist_to_center < FALLOUT_CENTER_THRESHOLD:
|
|
||||||
global_position = tile_center
|
|
||||||
fallout_state = true
|
|
||||||
fallout_scale_progress = 1.0
|
|
||||||
velocity = Vector2.ZERO
|
|
||||||
if has_method("_set_animation"):
|
|
||||||
_set_animation("FALL")
|
|
||||||
if has_node("SfxFallout"):
|
|
||||||
$SfxFallout.play()
|
|
||||||
else:
|
|
||||||
# Drag toward center (quicksand pull)
|
|
||||||
var dir = (tile_center - global_position).normalized()
|
|
||||||
const FALLOUT_DRAG_STRENGTH: float = 820.0
|
|
||||||
velocity = dir * FALLOUT_DRAG_STRENGTH * get_process_delta_time()
|
|
||||||
if has_method("_set_animation"):
|
|
||||||
_set_animation("RUN")
|
|
||||||
move_and_slide()
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
# Slime-like: rotate 45 and scale down then die
|
|
||||||
fallout_defeat_started = true
|
|
||||||
fallout_scale_progress = 1.0
|
|
||||||
rotation = deg_to_rad(45.0)
|
|
||||||
move_and_slide()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Update attack timer
|
# Update attack timer
|
||||||
if attack_timer > 0:
|
if attack_timer > 0:
|
||||||
@@ -211,8 +172,9 @@ func _physics_process(delta):
|
|||||||
if not is_knocked_back:
|
if not is_knocked_back:
|
||||||
_ai_behavior(delta)
|
_ai_behavior(delta)
|
||||||
|
|
||||||
# Slime, rat, humanoid: try to avoid stepping onto fallout (position_z <= 0 = ground enemies only; bat has position_z 1)
|
# All ground enemies: avoid stepping onto fallout when moving under their own control (not when knocked back)
|
||||||
if position_z <= 0.0 and not fallout_state and not fallout_defeat_started and velocity.length_squared() > 1.0:
|
# Skip avoidance during knockback so thrown objects (barrel, box) can knock enemies into fallout tiles
|
||||||
|
if position_z <= 0.0 and not fallout_state and not fallout_defeat_started and not is_knocked_back and velocity.length_squared() > 1.0:
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
if game_world and game_world.has_method("_is_position_on_fallout_tile"):
|
if game_world and game_world.has_method("_is_position_on_fallout_tile"):
|
||||||
var step = velocity.normalized() * 18.0
|
var step = velocity.normalized() * 18.0
|
||||||
@@ -257,6 +219,10 @@ func _physics_process(delta):
|
|||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
if game_world and game_world.has_method("_sync_enemy_damage_visual"):
|
if game_world and game_world.has_method("_sync_enemy_damage_visual"):
|
||||||
game_world._rpc_to_ready_peers("_sync_enemy_damage_visual", [enemy_name, enemy_index, actual_damage, global_position, false])
|
game_world._rpc_to_ready_peers("_sync_enemy_damage_visual", [enemy_name, enemy_index, actual_damage, global_position, false])
|
||||||
|
# Sync boss health so joiner's HUD bar updates
|
||||||
|
if has_meta("is_boss") and get_meta("is_boss") and game_world and game_world.has_method("_rpc_to_ready_peers") and game_world.has_method("_sync_boss_health"):
|
||||||
|
var max_hp = character_stats.maxhp if character_stats else max_health
|
||||||
|
game_world._rpc_to_ready_peers("_sync_boss_health", [enemy_name, enemy_index, current_health, max_hp])
|
||||||
|
|
||||||
# Animate burn visual if it's a sprite (only on authority/server)
|
# Animate burn visual if it's a sprite (only on authority/server)
|
||||||
if is_multiplayer_authority() and burn_debuff_visual and is_instance_valid(burn_debuff_visual):
|
if is_multiplayer_authority() and burn_debuff_visual and is_instance_valid(burn_debuff_visual):
|
||||||
@@ -278,26 +244,93 @@ func _physics_process(delta):
|
|||||||
burn_damage_timer = 0.0
|
burn_damage_timer = 0.0
|
||||||
_remove_burn_debuff()
|
_remove_burn_debuff()
|
||||||
|
|
||||||
# Sync position and animation to clients (only server sends)
|
# Sync position and animation to clients (only server sends). Also call from subclasses that override _physics_process (e.g. spider).
|
||||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
|
_send_position_sync_to_clients()
|
||||||
# Get state value if enemy has a state variable (for bats/slimes)
|
|
||||||
var state_val = -1
|
|
||||||
if "state" in self:
|
|
||||||
state_val = get("state") as int
|
|
||||||
# Only send RPC if we're in the scene tree
|
|
||||||
if is_inside_tree():
|
|
||||||
# Get enemy name/index for identification
|
|
||||||
var enemy_name = name
|
|
||||||
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
|
|
||||||
|
|
||||||
# Use game_world to send RPC instead of rpc() on node instance
|
# Call this from base _physics_process or from subclasses that override _physics_process (e.g. enemy_spider) so position sync is always sent.
|
||||||
# This avoids node path resolution issues when clients haven't spawned yet
|
func _send_position_sync_to_clients() -> void:
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
if not multiplayer.has_multiplayer_peer() or not is_multiplayer_authority():
|
||||||
if game_world and game_world.has_method("_sync_enemy_position"):
|
return
|
||||||
# Send via game_world using enemy name/index and position for identification
|
var state_val = -1
|
||||||
game_world._rpc_to_ready_peers("_sync_enemy_position", [enemy_name, enemy_index, position, velocity, position_z, current_direction, anim_frame, "", 0, state_val])
|
if "state" in self:
|
||||||
# Removed fallback rpc() call - it causes node path resolution errors
|
state_val = get("state") as int
|
||||||
# If game_world is not available, skip sync (will sync next frame)
|
if not is_inside_tree():
|
||||||
|
return
|
||||||
|
var enemy_name = name
|
||||||
|
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_broadcast_enemy_position"):
|
||||||
|
game_world._broadcast_enemy_position(enemy_name, enemy_index, position, velocity, position_z, current_direction, anim_frame, "", 0, state_val)
|
||||||
|
elif game_world and game_world.has_method("_sync_enemy_position"):
|
||||||
|
game_world._rpc_to_ready_peers("_sync_enemy_position", [enemy_name, enemy_index, position, velocity, position_z, current_direction, anim_frame, "", 0, state_val])
|
||||||
|
|
||||||
|
# Returns true if we're in a fallout state and caller should return (used by base _physics_process and by spider which overrides _physics_process).
|
||||||
|
func _check_and_handle_fallout(delta: float) -> bool:
|
||||||
|
if fallout_state:
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
fallout_scale_progress -= delta / FALLOUT_SINK_DURATION
|
||||||
|
if fallout_scale_progress <= 0.0:
|
||||||
|
died_from_fallout = true
|
||||||
|
call_deferred("_die")
|
||||||
|
return true
|
||||||
|
scale = Vector2.ONE * max(0.0, fallout_scale_progress)
|
||||||
|
if "humanoid_type" in self:
|
||||||
|
current_direction = Direction.DOWN
|
||||||
|
rotation = deg_to_rad(45.0)
|
||||||
|
if has_method("_set_animation"):
|
||||||
|
_set_animation("FALL")
|
||||||
|
move_and_slide()
|
||||||
|
return true
|
||||||
|
if fallout_defeat_started:
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
fallout_scale_progress -= delta / FALLOUT_SINK_DURATION
|
||||||
|
if fallout_scale_progress <= 0.0:
|
||||||
|
died_from_fallout = true
|
||||||
|
call_deferred("_die")
|
||||||
|
return true
|
||||||
|
scale = Vector2.ONE * max(0.0, fallout_scale_progress)
|
||||||
|
rotation = deg_to_rad(45.0)
|
||||||
|
move_and_slide()
|
||||||
|
return true
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
var on_fallout = false
|
||||||
|
if position_z <= 0.0 and gw and gw.has_method("_is_position_on_fallout_tile"):
|
||||||
|
on_fallout = gw._is_position_on_fallout_tile(global_position)
|
||||||
|
if on_fallout:
|
||||||
|
if "humanoid_type" in self:
|
||||||
|
var tile_center = gw._get_closest_fallout_tile_center(global_position) if gw.has_method("_get_closest_fallout_tile_center") else (gw._get_tile_center_at(global_position) if gw.has_method("_get_tile_center_at") else global_position)
|
||||||
|
var dist_to_center = global_position.distance_to(tile_center)
|
||||||
|
if dist_to_center < FALLOUT_CENTER_THRESHOLD:
|
||||||
|
global_position = tile_center
|
||||||
|
fallout_state = true
|
||||||
|
fallout_scale_progress = 1.0
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
current_direction = Direction.DOWN
|
||||||
|
rotation = deg_to_rad(45.0)
|
||||||
|
if has_method("_set_animation"):
|
||||||
|
_set_animation("FALL")
|
||||||
|
var fall_sfx_node = get_node_or_null("SfxFallout")
|
||||||
|
if fall_sfx_node:
|
||||||
|
fall_sfx_node.play()
|
||||||
|
else:
|
||||||
|
var dir = (tile_center - global_position).normalized()
|
||||||
|
const FALLOUT_DRAG_STRENGTH: float = 820.0
|
||||||
|
velocity = dir * FALLOUT_DRAG_STRENGTH * get_process_delta_time()
|
||||||
|
if has_method("_set_animation"):
|
||||||
|
_set_animation("RUN")
|
||||||
|
move_and_slide()
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
# Slime-like / spider / rat: rotate 45 and scale down then die
|
||||||
|
fallout_defeat_started = true
|
||||||
|
fallout_scale_progress = 1.0
|
||||||
|
rotation = deg_to_rad(45.0)
|
||||||
|
var fall_sfx_slime = get_node_or_null("SfxFallout")
|
||||||
|
if fall_sfx_slime:
|
||||||
|
fall_sfx_slime.play()
|
||||||
|
move_and_slide()
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
func _ai_behavior(_delta):
|
func _ai_behavior(_delta):
|
||||||
# Override in subclasses
|
# Override in subclasses
|
||||||
@@ -528,6 +561,12 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
|
|||||||
else:
|
else:
|
||||||
# Fallback: try direct RPC (may fail if node path doesn't match)
|
# Fallback: try direct RPC (may fail if node path doesn't match)
|
||||||
_sync_damage_visual.rpc(actual_damage, from_position, is_critical)
|
_sync_damage_visual.rpc(actual_damage, from_position, is_critical)
|
||||||
|
# Sync boss health so joiner's HUD boss bar updates
|
||||||
|
if has_meta("is_boss") and get_meta("is_boss") and game_world and game_world.has_method("_rpc_to_ready_peers") and game_world.has_method("_sync_boss_health"):
|
||||||
|
var max_hp = max_health
|
||||||
|
if character_stats:
|
||||||
|
max_hp = character_stats.maxhp
|
||||||
|
game_world._rpc_to_ready_peers("_sync_boss_health", [enemy_name, enemy_index, current_health, max_hp])
|
||||||
|
|
||||||
if current_health <= 0:
|
if current_health <= 0:
|
||||||
# Prevent multiple death triggers
|
# Prevent multiple death triggers
|
||||||
@@ -582,7 +621,7 @@ func _show_damage_number(amount: float, from_position: Vector2, is_critical: boo
|
|||||||
else:
|
else:
|
||||||
damage_label.label = str(int(amount))
|
damage_label.label = str(int(amount))
|
||||||
damage_label.color = Color(1.0, 0.6, 0.2) if is_critical else Color(1.0, 0.35, 0.35) # Bright orange / bright red
|
damage_label.color = Color(1.0, 0.6, 0.2) if is_critical else Color(1.0, 0.35, 0.35) # Bright orange / bright red
|
||||||
damage_label.z_index = 5
|
damage_label.z_index = 50 if (has_meta("is_boss") and get_meta("is_boss")) else 5 # Boss: draw above sprite
|
||||||
|
|
||||||
# Calculate direction from attacker (slight upward variation)
|
# Calculate direction from attacker (slight upward variation)
|
||||||
var direction_from_attacker = (global_position - from_position).normalized()
|
var direction_from_attacker = (global_position - from_position).normalized()
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ enum HandState {HIDDEN, EMERGING, IDLE, GRABBING}
|
|||||||
|
|
||||||
var state: HandState = HandState.HIDDEN
|
var state: HandState = HandState.HIDDEN
|
||||||
var players_in_interest: Array[Node] = []
|
var players_in_interest: Array[Node] = []
|
||||||
|
# Perception detection (like traps): once revealed, show blue effect until hand emerges
|
||||||
|
var is_detected: bool = false
|
||||||
|
var player_detection_attempts: Dictionary = {} # peer_id -> true
|
||||||
var grabbed_player: Node = null
|
var grabbed_player: Node = null
|
||||||
var random_move_dir: Vector2 = Vector2.ZERO
|
var random_move_dir: Vector2 = Vector2.ZERO
|
||||||
var random_move_timer: float = 0.0
|
var random_move_timer: float = 0.0
|
||||||
@@ -18,6 +21,7 @@ const SNATCH_DAMAGE: float = 12.0
|
|||||||
const GRAB_COOLDOWN: float = 2.0 # Cooldown after grabbing before can grab again
|
const GRAB_COOLDOWN: float = 2.0 # Cooldown after grabbing before can grab again
|
||||||
const TILE_SIZE: int = 16
|
const TILE_SIZE: int = 16
|
||||||
const TILE_STRIDE: int = 17 # 16 + separation 1
|
const TILE_STRIDE: int = 17 # 16 + separation 1
|
||||||
|
@onready var detection_area: Area2D = $DetectionArea
|
||||||
@onready var emerge_area: Area2D = $EmergeArea
|
@onready var emerge_area: Area2D = $EmergeArea
|
||||||
@onready var grab_area: Area2D = $GrabPlayerArea
|
@onready var grab_area: Area2D = $GrabPlayerArea
|
||||||
@onready var interest_area: Area2D = $PlayerInterestArea
|
@onready var interest_area: Area2D = $PlayerInterestArea
|
||||||
@@ -42,7 +46,7 @@ func _ready() -> void:
|
|||||||
# Start hidden
|
# Start hidden
|
||||||
modulate.a = 0.0
|
modulate.a = 0.0
|
||||||
# Areas detect players (layer 1)
|
# Areas detect players (layer 1)
|
||||||
for area in [emerge_area, grab_area, interest_area]:
|
for area in [detection_area, emerge_area, grab_area, interest_area]:
|
||||||
if area:
|
if area:
|
||||||
area.set_collision_mask_value(1, true)
|
area.set_collision_mask_value(1, true)
|
||||||
area.monitoring = true
|
area.monitoring = true
|
||||||
@@ -247,6 +251,59 @@ func _ai_behavior(delta: float) -> void:
|
|||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_detection_area_body_entered(body: Node2D) -> void:
|
||||||
|
if not body.is_in_group("player"):
|
||||||
|
return
|
||||||
|
if state != HandState.HIDDEN:
|
||||||
|
return
|
||||||
|
if is_detected:
|
||||||
|
return
|
||||||
|
var peer_id = body.get_multiplayer_authority()
|
||||||
|
if player_detection_attempts.has(peer_id):
|
||||||
|
return
|
||||||
|
player_detection_attempts[peer_id] = true
|
||||||
|
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer():
|
||||||
|
_roll_perception_check_hand(body)
|
||||||
|
|
||||||
|
|
||||||
|
func _roll_perception_check_hand(player: Node) -> void:
|
||||||
|
if not player or not player.character_stats:
|
||||||
|
return
|
||||||
|
var per_stat = player.character_stats.baseStats.per + player.character_stats.get_pass("per")
|
||||||
|
var roll = randi() % 20 + 1
|
||||||
|
var total = roll + int(per_stat / 2) - 5
|
||||||
|
var dc = 15
|
||||||
|
if total >= dc:
|
||||||
|
_detect_hand(player)
|
||||||
|
else:
|
||||||
|
pass # Remain hidden to this player
|
||||||
|
|
||||||
|
|
||||||
|
func _detect_hand(detecting_player: Node) -> void:
|
||||||
|
is_detected = true
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("spawn_detected_effect_at"):
|
||||||
|
game_world.spawn_detected_effect_at(global_position, name, "enemy")
|
||||||
|
if detecting_player and is_instance_valid(detecting_player) and detecting_player.has_method("_on_trap_detected"):
|
||||||
|
detecting_player._on_trap_detected()
|
||||||
|
# Effect is synced to clients via game_world.spawn_detected_effect_at -> _sync_spawn_detected_effect
|
||||||
|
|
||||||
|
|
||||||
|
func _remove_detected_effect() -> void:
|
||||||
|
# Effect is under Entities (not our child) so it stays visible while we're hidden; remove by position and sync
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("remove_detected_effect_at_position"):
|
||||||
|
game_world.remove_detected_effect_at_position(global_position)
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
game_world._sync_remove_detected_effect_at_position.rpc(global_position.x, global_position.y)
|
||||||
|
return
|
||||||
|
# Fallback: effect might be our child (legacy)
|
||||||
|
for c in get_children():
|
||||||
|
if c.name == "DetectedEffect":
|
||||||
|
c.queue_free()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
func _on_emerge_area_body_entered(body: Node2D) -> void:
|
func _on_emerge_area_body_entered(body: Node2D) -> void:
|
||||||
if state != HandState.HIDDEN:
|
if state != HandState.HIDDEN:
|
||||||
return
|
return
|
||||||
@@ -257,6 +314,8 @@ func _on_emerge_area_body_entered(body: Node2D) -> void:
|
|||||||
if not is_multiplayer_authority():
|
if not is_multiplayer_authority():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Reveal effect is removed only when emerge animation finishes (_on_animation_finished)
|
||||||
|
|
||||||
state = HandState.EMERGING
|
state = HandState.EMERGING
|
||||||
modulate.a = 1.0
|
modulate.a = 1.0
|
||||||
if anim_player and anim_player.has_animation("emerge"):
|
if anim_player and anim_player.has_animation("emerge"):
|
||||||
@@ -272,6 +331,7 @@ func _on_emerge_area_body_entered(body: Node2D) -> void:
|
|||||||
|
|
||||||
func _on_animation_finished(anim_name: StringName) -> void:
|
func _on_animation_finished(anim_name: StringName) -> void:
|
||||||
if anim_name == "emerge":
|
if anim_name == "emerge":
|
||||||
|
_remove_detected_effect() # Remove reveal effect only once hand has fully emerged
|
||||||
state = HandState.IDLE
|
state = HandState.IDLE
|
||||||
if anim_player and anim_player.has_animation("idle"):
|
if anim_player and anim_player.has_animation("idle"):
|
||||||
anim_player.play("idle")
|
anim_player.play("idle")
|
||||||
@@ -330,7 +390,7 @@ func _sync_hand_emerged():
|
|||||||
# Sync hand emergence visibility to clients
|
# Sync hand emergence visibility to clients
|
||||||
if is_multiplayer_authority():
|
if is_multiplayer_authority():
|
||||||
return # Authority already handled it locally
|
return # Authority already handled it locally
|
||||||
|
# Effect removed when emerge animation finishes on each client (_on_animation_finished)
|
||||||
if state == HandState.HIDDEN:
|
if state == HandState.HIDDEN:
|
||||||
state = HandState.EMERGING
|
state = HandState.EMERGING
|
||||||
modulate.a = 1.0
|
modulate.a = 1.0
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ var shield_block_chance: float = 0.0
|
|||||||
var is_blocking: bool = false # Whether enemy is actively blocking with shield
|
var is_blocking: bool = false # Whether enemy is actively blocking with shield
|
||||||
var shield_block_speed_multiplier: float = 0.5 # Movement speed multiplier when blocking
|
var shield_block_speed_multiplier: float = 0.5 # Movement speed multiplier when blocking
|
||||||
var shield_block_timer: float = 0.0 # Timer for how long to keep blocking
|
var shield_block_timer: float = 0.0 # Timer for how long to keep blocking
|
||||||
var shield_block_duration: float = 1.5 # How long to block after raising shield
|
var shield_block_duration: float = 1.0 # How long to block after raising shield (shorter so they attack more)
|
||||||
|
const SHIELD_BLOCK_MIN_HOLD: float = 0.35 # Minimum time to keep shield up (shorter so they can attack sooner)
|
||||||
var can_lift_throw: bool = false
|
var can_lift_throw: bool = false
|
||||||
var spell_cooldown_timer: float = 0.0
|
var spell_cooldown_timer: float = 0.0
|
||||||
var bomb_cooldown_timer: float = 0.0
|
var bomb_cooldown_timer: float = 0.0
|
||||||
@@ -766,7 +767,7 @@ func _load_random_headgear():
|
|||||||
"SoldierIronHelmBlue.png", "SoldierSteelHelmBlue.png"
|
"SoldierIronHelmBlue.png", "SoldierSteelHelmBlue.png"
|
||||||
],
|
],
|
||||||
"Basic Range": [
|
"Basic Range": [
|
||||||
"ArcherHatCyan.png", "HunterHatRed.png", "RangerHatGreen.png", "RogueHatGreen.png"
|
"ArcherHatCyan.png", "HunterHatRed.png", "RogueHatGreen.png"
|
||||||
],
|
],
|
||||||
"French": ["MusketeerHatPurple.png"],
|
"French": ["MusketeerHatPurple.png"],
|
||||||
"Japanese": [
|
"Japanese": [
|
||||||
@@ -978,10 +979,10 @@ func _assign_loadout():
|
|||||||
elif spell_roll < 0.20:
|
elif spell_roll < 0.20:
|
||||||
spell_type = "healing"
|
spell_type = "healing"
|
||||||
LogManager.log(str(name) + " assigned healing spell (tome spell enemy)", LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " assigned healing spell (tome spell enemy)", LogManager.CATEGORY_ENEMY)
|
||||||
# Shield: ~24% get shield, block chance 0.12–0.22
|
# Shield: ~24% get shield, block chance 0.06–0.12 (reduced so they attack more)
|
||||||
if appearance_rng.randf() < 0.24:
|
if appearance_rng.randf() < 0.24:
|
||||||
has_shield = true
|
has_shield = true
|
||||||
shield_block_chance = 0.12 + appearance_rng.randf() * 0.10
|
shield_block_chance = 0.06 + appearance_rng.randf() * 0.06
|
||||||
# Lift/throw: ~12% can grab and throw liftable objects when aggro
|
# Lift/throw: ~12% can grab and throw liftable objects when aggro
|
||||||
if appearance_rng.randf() < 0.12:
|
if appearance_rng.randf() < 0.12:
|
||||||
can_lift_throw = true
|
can_lift_throw = true
|
||||||
@@ -1016,32 +1017,10 @@ func _get_nearby_liftable() -> Node:
|
|||||||
return best
|
return best
|
||||||
|
|
||||||
func _physics_process(delta):
|
func _physics_process(delta):
|
||||||
# Always update animation (even when dead, for death animation)
|
# Always update animation (even when dead or in fallout, for correct visuals)
|
||||||
_update_animation(delta)
|
_update_animation(delta)
|
||||||
|
|
||||||
# Handle dead state (from parent)
|
# Humanoid-specific timers (base updates attack_timer; run before base AI)
|
||||||
if is_dead:
|
|
||||||
if is_knocked_back:
|
|
||||||
knockback_time += delta
|
|
||||||
if knockback_time >= knockback_duration:
|
|
||||||
is_knocked_back = false
|
|
||||||
knockback_time = 0.0
|
|
||||||
velocity = Vector2.ZERO
|
|
||||||
else:
|
|
||||||
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
|
|
||||||
move_and_slide()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Only server (authority) runs AI and physics
|
|
||||||
if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
|
|
||||||
return
|
|
||||||
|
|
||||||
# Update attack timer and reset attack flags when cooldown is over
|
|
||||||
if attack_timer > 0:
|
|
||||||
attack_timer -= delta
|
|
||||||
if attack_timer <= 0:
|
|
||||||
can_attack = true
|
|
||||||
is_attacking = false
|
|
||||||
if spell_cooldown_timer > 0:
|
if spell_cooldown_timer > 0:
|
||||||
spell_cooldown_timer -= delta
|
spell_cooldown_timer -= delta
|
||||||
if bomb_cooldown_timer > 0:
|
if bomb_cooldown_timer > 0:
|
||||||
@@ -1055,47 +1034,28 @@ func _physics_process(delta):
|
|||||||
if shield_block_timer <= 0:
|
if shield_block_timer <= 0:
|
||||||
is_blocking = false
|
is_blocking = false
|
||||||
_update_shield_visibility()
|
_update_shield_visibility()
|
||||||
|
|
||||||
# Update bow charge pulse timer when charging bow
|
|
||||||
if ai_state == AIState.BOW_CHARGING and is_charging_attack:
|
if ai_state == AIState.BOW_CHARGING and is_charging_attack:
|
||||||
bow_charge_tint_pulse_time += delta * bow_charge_tint_pulse_speed_charged
|
bow_charge_tint_pulse_time += delta * bow_charge_tint_pulse_speed_charged
|
||||||
_apply_bow_charge_tint()
|
_apply_bow_charge_tint()
|
||||||
elif ai_state != AIState.BOW_CHARGING:
|
elif ai_state != AIState.BOW_CHARGING:
|
||||||
# Clear bow charge tint when not charging
|
|
||||||
_clear_bow_charge_tint()
|
_clear_bow_charge_tint()
|
||||||
|
|
||||||
# Handle knockback
|
# Base handles: dead, not authority, fallout (drag/sink), knockback, attack_timer, _ai_behavior, avoidance, move_and_slide, collisions, burn
|
||||||
if is_knocked_back:
|
super._physics_process(delta)
|
||||||
knockback_time += delta
|
|
||||||
if knockback_time >= knockback_duration:
|
|
||||||
is_knocked_back = false
|
|
||||||
knockback_time = 0.0
|
|
||||||
velocity = Vector2.ZERO
|
|
||||||
else:
|
|
||||||
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
|
|
||||||
|
|
||||||
# Enemy AI - only if not knocked back
|
# Reset attack flags when cooldown expired (base decrements attack_timer only)
|
||||||
if not is_knocked_back:
|
if attack_timer <= 0:
|
||||||
_ai_behavior(delta)
|
can_attack = true
|
||||||
|
is_attacking = false
|
||||||
|
|
||||||
# Move
|
# Sync position and animation to clients (only server sends; humanoid uses game_world RPC)
|
||||||
move_and_slide()
|
|
||||||
|
|
||||||
# Don't use contact-based attack for humanoids (they use sword projectiles)
|
|
||||||
# _check_player_collision() # Disabled - humanoids attack with sword projectiles instead
|
|
||||||
|
|
||||||
# Sync position and animation to clients (only server sends)
|
|
||||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
|
||||||
# Use game_world to send RPC instead of rpc() on node instance
|
|
||||||
# This avoids node path resolution issues when clients haven't spawned yet
|
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
if game_world and game_world.has_method("_sync_enemy_position"):
|
if game_world and game_world.has_method("_sync_enemy_position"):
|
||||||
# Send via game_world using enemy name/index and position for identification
|
|
||||||
var enemy_name = name
|
var enemy_name = name
|
||||||
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
|
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
|
||||||
game_world._rpc_to_ready_peers("_sync_enemy_position", [enemy_name, enemy_index, position, velocity, position_z, current_direction, 0, current_animation, current_frame, -1])
|
game_world._rpc_to_ready_peers("_sync_enemy_position", [enemy_name, enemy_index, position, velocity, position_z, current_direction, 0, current_animation, current_frame, -1])
|
||||||
else:
|
else:
|
||||||
# Fallback: try direct call to _sync_position (not RPC)
|
|
||||||
_sync_position(position, velocity, position_z, current_direction, 0, current_animation, current_frame)
|
_sync_position(position, velocity, position_z, current_direction, 0, current_animation, current_frame)
|
||||||
|
|
||||||
func _ai_behavior(delta):
|
func _ai_behavior(delta):
|
||||||
@@ -1249,28 +1209,19 @@ func _chasing_behavior(delta_arg):
|
|||||||
|
|
||||||
var to_player = (target_player.global_position - global_position).normalized()
|
var to_player = (target_player.global_position - global_position).normalized()
|
||||||
|
|
||||||
# --- Shield blocking: raise shield when player is close and attacking ---
|
# --- Shield blocking: only when player is actually attacking; shorter duration so we attack sometimes ---
|
||||||
if has_shield and shield_block_chance > 0:
|
if has_shield and shield_block_chance > 0:
|
||||||
# Check if player is attacking (recently attacked or in melee range)
|
# Only consider blocking when player is actually attacking (sword up), not just "close"
|
||||||
var player_is_attacking = false
|
var player_is_attacking = (dist < 70.0 and "is_attacking" in target_player and target_player.is_attacking)
|
||||||
if dist < 60.0: # Close enough that player might attack
|
# Raise shield only when player is attacking (not when merely close)
|
||||||
# Check if player is facing us and might be attacking
|
if player_is_attacking and not is_blocking and randf() < 0.65:
|
||||||
if "is_attacking" in target_player and target_player.is_attacking:
|
# 65% chance to block when player is attacking (not 100%), so we attack sometimes
|
||||||
player_is_attacking = true
|
|
||||||
# Also raise shield if player is very close (within attack range)
|
|
||||||
if dist < 50.0:
|
|
||||||
player_is_attacking = true
|
|
||||||
|
|
||||||
# Raise shield if player is attacking or very close
|
|
||||||
if player_is_attacking and not is_blocking:
|
|
||||||
is_blocking = true
|
is_blocking = true
|
||||||
shield_block_timer = shield_block_duration
|
shield_block_timer = shield_block_duration * 0.5 # Shorter: 0.75s so we lower and attack sooner
|
||||||
_update_shield_visibility()
|
_update_shield_visibility()
|
||||||
# Play shield activation sound
|
|
||||||
if sfx_activate_shield:
|
if sfx_activate_shield:
|
||||||
sfx_activate_shield.play()
|
sfx_activate_shield.play()
|
||||||
elif not player_is_attacking and is_blocking and shield_block_timer <= 0:
|
elif not player_is_attacking and is_blocking and shield_block_timer <= 0:
|
||||||
# Lower shield if player is not attacking and timer expired
|
|
||||||
is_blocking = false
|
is_blocking = false
|
||||||
_update_shield_visibility()
|
_update_shield_visibility()
|
||||||
|
|
||||||
@@ -1320,6 +1271,9 @@ func _chasing_behavior(delta_arg):
|
|||||||
|
|
||||||
# --- Melee: close enough to attack ---
|
# --- Melee: close enough to attack ---
|
||||||
if dist < 45.0 and can_attack and not is_charging_attack:
|
if dist < 45.0 and can_attack and not is_charging_attack:
|
||||||
|
# When blocking, still allow attack 40% of the time so we don't only block forever
|
||||||
|
if is_blocking and shield_block_timer > 0 and randf() >= 0.40:
|
||||||
|
return
|
||||||
# Lower shield when attacking (can't block while attacking)
|
# Lower shield when attacking (can't block while attacking)
|
||||||
if is_blocking:
|
if is_blocking:
|
||||||
is_blocking = false
|
is_blocking = false
|
||||||
@@ -2183,27 +2137,41 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
|
|||||||
if has_shield and shield_block_chance > 0 and from_position != Vector2.ZERO and not is_burn_damage:
|
if has_shield and shield_block_chance > 0 and from_position != Vector2.ZERO and not is_burn_damage:
|
||||||
var to_attacker = (from_position - global_position).normalized()
|
var to_attacker = (from_position - global_position).normalized()
|
||||||
var facing = _get_attack_direction_vector()
|
var facing = _get_attack_direction_vector()
|
||||||
# Check if attack is coming from the direction we're facing (blocking direction)
|
var attack_from_front = to_attacker.dot(facing) > 0.5
|
||||||
if to_attacker.dot(facing) > 0.5 and randf() < shield_block_chance:
|
# Full block when already blocking and attack from front, or reactive block roll
|
||||||
# Successfully blocked - reduce damage
|
var did_block = (is_blocking and attack_from_front) or (attack_from_front and randf() < shield_block_chance)
|
||||||
amount = amount * 0.5
|
if did_block:
|
||||||
# Raise shield if not already blocking
|
# Full block: no damage, show BLOCKED to attacker, small knockback on attacker, chance to lower our shield
|
||||||
if not is_blocking:
|
if not is_blocking:
|
||||||
is_blocking = true
|
is_blocking = true
|
||||||
shield_block_timer = shield_block_duration
|
shield_block_timer = max(shield_block_duration, SHIELD_BLOCK_MIN_HOLD)
|
||||||
_update_shield_visibility()
|
_update_shield_visibility()
|
||||||
if sfx_activate_shield:
|
if sfx_activate_shield:
|
||||||
sfx_activate_shield.play()
|
sfx_activate_shield.play()
|
||||||
# Play block sound
|
else:
|
||||||
|
shield_block_timer = max(shield_block_timer, SHIELD_BLOCK_MIN_HOLD)
|
||||||
if sfx_block_with_shield:
|
if sfx_block_with_shield:
|
||||||
sfx_block_with_shield.play()
|
sfx_block_with_shield.play()
|
||||||
# Face the attacker
|
|
||||||
current_direction = _get_direction_from_vector(to_attacker) as Direction
|
current_direction = _get_direction_from_vector(to_attacker) as Direction
|
||||||
|
# Notify attacker (player) to show BLOCKED and apply small knockback
|
||||||
|
var attacker = _find_nearest_player_to_position(from_position)
|
||||||
|
if attacker and is_instance_valid(attacker) and attacker.has_method("_on_attack_blocked_by_enemy"):
|
||||||
|
var pid = attacker.get_multiplayer_authority()
|
||||||
|
if multiplayer.get_unique_id() == pid:
|
||||||
|
attacker._on_attack_blocked_by_enemy(global_position)
|
||||||
|
elif multiplayer.has_multiplayer_peer() and is_inside_tree() and pid != 0:
|
||||||
|
attacker._on_attack_blocked_by_enemy.rpc_id(pid, global_position)
|
||||||
|
# Chance to lower shield after blocking so they can attack (35%)
|
||||||
|
if randf() < 0.35:
|
||||||
|
is_blocking = false
|
||||||
|
shield_block_timer = 0.0
|
||||||
|
_update_shield_visibility()
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
# Attack not blocked, but raise shield anyway if we have one (defensive reaction)
|
# Attack not blocked; brief defensive raise (shorter so they attack again sooner)
|
||||||
if not is_blocking:
|
if not is_blocking and randf() < 0.5:
|
||||||
is_blocking = true
|
is_blocking = true
|
||||||
shield_block_timer = shield_block_duration * 0.8 # Shorter duration for reactive blocking
|
shield_block_timer = max(shield_block_duration * 0.4, SHIELD_BLOCK_MIN_HOLD)
|
||||||
_update_shield_visibility()
|
_update_shield_visibility()
|
||||||
if sfx_activate_shield:
|
if sfx_activate_shield:
|
||||||
sfx_activate_shield.play()
|
sfx_activate_shield.play()
|
||||||
|
|||||||
170
src/scripts/enemy_spider.gd
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
extends "res://scripts/enemy_base.gd"
|
||||||
|
|
||||||
|
# Spider: 8 directions from spider.png (3h x 3v). Slow, walks toward player.
|
||||||
|
# When close: vibrate, then lunge at player and deal damage on contact.
|
||||||
|
# Spritesheet frame order: 0=down, 1=downright, 2=right, 3=upright, 4=up, 5=upleft, 6=left, 7=downleft
|
||||||
|
|
||||||
|
const MOVE_SPEED_SPIDER: float = 28.0
|
||||||
|
const ATTACK_RANGE: float = 24.0
|
||||||
|
const VIBRATE_DURATION: float = 0.35
|
||||||
|
const ATTACK_DAMAGE: float = 8.0
|
||||||
|
const LUNGE_SPEED: float = 140.0
|
||||||
|
const LUNGE_DURATION: float = 0.22
|
||||||
|
|
||||||
|
var vibrate_timer: float = 0.0
|
||||||
|
var lunge_timer: float = 0.0
|
||||||
|
var direction_frame: int = 0 # 0-7 for 8 directions
|
||||||
|
|
||||||
|
@onready var spider_sprite: Sprite2D = $Sprite2D
|
||||||
|
@onready var shadow_sprite: Sprite2D = $Shadow
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
super._ready()
|
||||||
|
exp_reward = 1.0 # Spiders give only 1 EXP and no loot
|
||||||
|
# Boss-spawned spiders (dungeon_spawned) have less HP so they go down faster
|
||||||
|
if get_meta("dungeon_spawned", false):
|
||||||
|
max_health = 12.0
|
||||||
|
var base_max = (character_stats.baseStats.end + character_stats.get_pass("end")) * 3.0 + character_stats.get_pass("maxhp")
|
||||||
|
character_stats.bonusmaxhp = max_health - base_max
|
||||||
|
character_stats.hp = character_stats.maxhp
|
||||||
|
current_health = character_stats.hp
|
||||||
|
else:
|
||||||
|
max_health = 22.0
|
||||||
|
current_health = max_health
|
||||||
|
move_speed = MOVE_SPEED_SPIDER
|
||||||
|
damage = ATTACK_DAMAGE
|
||||||
|
attack_cooldown = 0.6
|
||||||
|
if spider_sprite:
|
||||||
|
spider_sprite.hframes = 3
|
||||||
|
spider_sprite.vframes = 3
|
||||||
|
if shadow_sprite:
|
||||||
|
shadow_sprite.hframes = 3
|
||||||
|
shadow_sprite.vframes = 3
|
||||||
|
shadow_sprite.frame = direction_frame
|
||||||
|
|
||||||
|
|
||||||
|
func take_damage(amount: float, from_position: Vector2, is_critical: bool = false, is_burn_damage: bool = false, apply_burn_debuff: bool = false) -> void:
|
||||||
|
super.take_damage(amount, from_position, is_critical, is_burn_damage, apply_burn_debuff)
|
||||||
|
# Reset attack state so spider doesn't keep lunging/vibrating through the hit
|
||||||
|
_reset_attack_behavior()
|
||||||
|
|
||||||
|
|
||||||
|
func _reset_attack_behavior() -> void:
|
||||||
|
vibrate_timer = 0.0
|
||||||
|
lunge_timer = 0.0
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
if spider_sprite:
|
||||||
|
spider_sprite.position = Vector2.ZERO
|
||||||
|
if shadow_sprite:
|
||||||
|
shadow_sprite.position = Vector2.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
if is_dead:
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
# Spiders can fall into fallout like rat/slime (base logic is in _check_and_handle_fallout)
|
||||||
|
if _check_and_handle_fallout(delta):
|
||||||
|
return
|
||||||
|
if attack_timer > 0:
|
||||||
|
attack_timer -= delta
|
||||||
|
target_player = _find_nearest_player()
|
||||||
|
if target_player and is_instance_valid(target_player):
|
||||||
|
var to_player = target_player.global_position - global_position
|
||||||
|
var dist = to_player.length()
|
||||||
|
var dir = to_player.normalized()
|
||||||
|
if lunge_timer > 0:
|
||||||
|
# In lunge: keep velocity toward player; damage is applied on contact via _check_player_collision below
|
||||||
|
lunge_timer -= delta
|
||||||
|
if lunge_timer <= 0:
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
# If we didn't hit during lunge but are still in range, deal damage (pounce "lands")
|
||||||
|
if dist <= ATTACK_RANGE * 1.5 and attack_timer <= 0:
|
||||||
|
_attack_player(target_player)
|
||||||
|
elif dist <= ATTACK_RANGE:
|
||||||
|
if attack_timer > 0:
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
else:
|
||||||
|
# Vibrate then lunge
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
vibrate_timer += delta
|
||||||
|
var vibrate_offset = Vector2(randf_range(-1.5, 1.5), randf_range(-1.5, 1.5)) if vibrate_timer < VIBRATE_DURATION else Vector2.ZERO
|
||||||
|
if spider_sprite:
|
||||||
|
spider_sprite.position = vibrate_offset
|
||||||
|
if shadow_sprite:
|
||||||
|
shadow_sprite.position = vibrate_offset
|
||||||
|
if vibrate_timer >= VIBRATE_DURATION:
|
||||||
|
vibrate_timer = 0.0
|
||||||
|
if spider_sprite:
|
||||||
|
spider_sprite.position = Vector2.ZERO
|
||||||
|
if shadow_sprite:
|
||||||
|
shadow_sprite.position = Vector2.ZERO
|
||||||
|
# Start lunge: throw ourselves at the player
|
||||||
|
_set_direction_from_vector(dir)
|
||||||
|
velocity = dir * LUNGE_SPEED
|
||||||
|
lunge_timer = LUNGE_DURATION
|
||||||
|
else:
|
||||||
|
if attack_timer > 0:
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
else:
|
||||||
|
velocity = dir * move_speed
|
||||||
|
_set_direction_from_vector(dir)
|
||||||
|
else:
|
||||||
|
velocity = velocity.lerp(Vector2.ZERO, delta * 4.0)
|
||||||
|
lunge_timer = 0.0
|
||||||
|
# Avoid stepping onto fallout (like base does for other ground enemies)
|
||||||
|
if not fallout_state and not fallout_defeat_started and not is_knocked_back and velocity.length_squared() > 1.0:
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_is_position_on_fallout_tile"):
|
||||||
|
var step = velocity.normalized() * 18.0
|
||||||
|
if game_world._is_position_on_fallout_tile(global_position + step):
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
move_and_slide()
|
||||||
|
# Deal damage on contact (during lunge or normal movement); end lunge when we hit
|
||||||
|
if get_slide_collision_count() > 0:
|
||||||
|
for i in get_slide_collision_count():
|
||||||
|
var collider = get_slide_collision(i).get_collider()
|
||||||
|
if collider and collider.is_in_group("player"):
|
||||||
|
_attack_player(collider)
|
||||||
|
if lunge_timer > 0:
|
||||||
|
lunge_timer = 0.0
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
break
|
||||||
|
|
||||||
|
# Spider overrides _physics_process and does not call super, so base never sends position sync. Send it here so joiner sees movement.
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
|
||||||
|
_send_position_sync_to_clients()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_direction_from_vector(dir: Vector2) -> void:
|
||||||
|
# Angle sector: 0=right, 1=downright, 2=down, 3=downleft, 4=left, 5=upleft, 6=up, 7=upright
|
||||||
|
var angle = dir.angle()
|
||||||
|
var sector = int(round(angle / (TAU / 8.0))) % 8
|
||||||
|
if sector < 0:
|
||||||
|
sector += 8
|
||||||
|
# Map to spritesheet order: down, downright, right, upright, up, upleft, left, downleft
|
||||||
|
const SHEET_FRAME: Array = [2, 1, 0, 7, 6, 5, 4, 3] # sector -> frame
|
||||||
|
direction_frame = SHEET_FRAME[sector]
|
||||||
|
anim_frame = direction_frame # keep in sync for network position sync
|
||||||
|
if spider_sprite:
|
||||||
|
spider_sprite.frame = direction_frame
|
||||||
|
if shadow_sprite:
|
||||||
|
shadow_sprite.frame = direction_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _spawn_loot() -> void:
|
||||||
|
# Spiders do not drop loot
|
||||||
|
return
|
||||||
|
|
||||||
|
func _update_client_visuals() -> void:
|
||||||
|
super._update_client_visuals()
|
||||||
|
# Apply synced frame so client spider faces the right direction
|
||||||
|
if spider_sprite:
|
||||||
|
spider_sprite.frame = anim_frame
|
||||||
|
if shadow_sprite:
|
||||||
|
shadow_sprite.frame = anim_frame
|
||||||
|
direction_frame = anim_frame
|
||||||
1
src/scripts/enemy_spider.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://7e0vssvq87hn
|
||||||
@@ -16,6 +16,9 @@ var label_time: Label = null
|
|||||||
var label_time_value: Label = null
|
var label_time_value: Label = null
|
||||||
var label_boss: Label = null
|
var label_boss: Label = null
|
||||||
var texture_progress_bar_boss_hp: TextureProgressBar = null
|
var texture_progress_bar_boss_hp: TextureProgressBar = null
|
||||||
|
var center_bottom_boss: Control = null
|
||||||
|
var label_boss_center: Label = null
|
||||||
|
var progress_bar_boss_hp_center: ProgressBar = null
|
||||||
var label_host: Label = null
|
var label_host: Label = null
|
||||||
var label_player_count: Label = null
|
var label_player_count: Label = null
|
||||||
var label_room_code: Label = null
|
var label_room_code: Label = null
|
||||||
@@ -46,6 +49,22 @@ func _ready():
|
|||||||
label_time_value = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerTime/LabelTimeValue")
|
label_time_value = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerTime/LabelTimeValue")
|
||||||
label_boss = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerBoss/LabelBoss")
|
label_boss = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerBoss/LabelBoss")
|
||||||
texture_progress_bar_boss_hp = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerBoss/TextureProgressBarBossHP")
|
texture_progress_bar_boss_hp = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerBoss/TextureProgressBarBossHP")
|
||||||
|
center_bottom_boss = get_node_or_null("CenterBottom")
|
||||||
|
label_boss_center = get_node_or_null("CenterBottom/VBoxBoss/LabelBossCenter")
|
||||||
|
progress_bar_boss_hp_center = get_node_or_null("CenterBottom/VBoxBoss/ProgressBarBossHP")
|
||||||
|
if progress_bar_boss_hp_center:
|
||||||
|
var bg = StyleBoxFlat.new()
|
||||||
|
bg.bg_color = Color(0.2, 0.2, 0.2, 0.9)
|
||||||
|
bg.set_border_width_all(2)
|
||||||
|
bg.border_color = Color(0.5, 0.2, 0.2)
|
||||||
|
progress_bar_boss_hp_center.add_theme_stylebox_override("background", bg)
|
||||||
|
var fill = StyleBoxFlat.new()
|
||||||
|
fill.bg_color = Color(0.9, 0.2, 0.2)
|
||||||
|
progress_bar_boss_hp_center.add_theme_stylebox_override("fill", fill)
|
||||||
|
if label_boss_center and ResourceLoader.exists("res://assets/fonts/standard_font.png"):
|
||||||
|
var fr = load("res://assets/fonts/standard_font.png")
|
||||||
|
if fr:
|
||||||
|
label_boss_center.add_theme_font_override("font", fr)
|
||||||
label_host = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelHost")
|
label_host = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelHost")
|
||||||
label_player_count = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelPlayerCount")
|
label_player_count = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelPlayerCount")
|
||||||
label_room_code = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelRoomCode")
|
label_room_code = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelRoomCode")
|
||||||
@@ -86,11 +105,13 @@ func _ready():
|
|||||||
if not game_world:
|
if not game_world:
|
||||||
print("IngameHUD: WARNING - game_world not found in group")
|
print("IngameHUD: WARNING - game_world not found in group")
|
||||||
|
|
||||||
# Initially hide boss health bar
|
# Initially hide boss health bar (upper right and center bottom)
|
||||||
if texture_progress_bar_boss_hp:
|
if texture_progress_bar_boss_hp:
|
||||||
texture_progress_bar_boss_hp.visible = false
|
texture_progress_bar_boss_hp.visible = false
|
||||||
if label_boss:
|
if label_boss:
|
||||||
label_boss.visible = false
|
label_boss.visible = false
|
||||||
|
if center_bottom_boss:
|
||||||
|
center_bottom_boss.visible = false
|
||||||
|
|
||||||
# Update host info display
|
# Update host info display
|
||||||
_update_host_info()
|
_update_host_info()
|
||||||
@@ -387,32 +408,52 @@ func start_timer():
|
|||||||
timer_running = true
|
timer_running = true
|
||||||
level_start_time = Time.get_ticks_msec() / 1000.0
|
level_start_time = Time.get_ticks_msec() / 1000.0
|
||||||
|
|
||||||
|
var _boss_bar_fill_animating: bool = false
|
||||||
|
var _boss_bar_fill_tween_started: bool = false
|
||||||
|
|
||||||
func _update_boss_health():
|
func _update_boss_health():
|
||||||
# Find boss enemy (if any)
|
# Find boss enemy that is ACTIVATED (bar only shows when boss is activated)
|
||||||
var boss_enemy = null
|
var boss_enemy = null
|
||||||
var enemies = get_tree().get_nodes_in_group("enemy")
|
var enemies = get_tree().get_nodes_in_group("enemy")
|
||||||
for enemy in enemies:
|
for enemy in enemies:
|
||||||
# Check if enemy is a boss (could check metadata or name)
|
if enemy.has_meta("is_boss") and enemy.get_meta("is_boss") and enemy.has_meta("boss_activated") and enemy.get_meta("boss_activated"):
|
||||||
if enemy.has_meta("is_boss") and enemy.get_meta("is_boss"):
|
|
||||||
boss_enemy = enemy
|
boss_enemy = enemy
|
||||||
break
|
break
|
||||||
|
|
||||||
if boss_enemy and is_instance_valid(boss_enemy):
|
if boss_enemy and is_instance_valid(boss_enemy):
|
||||||
# Show boss health bar
|
if center_bottom_boss and not center_bottom_boss.visible:
|
||||||
|
# First time showing: animate bar from 0 to full
|
||||||
|
if progress_bar_boss_hp_center:
|
||||||
|
progress_bar_boss_hp_center.value = 0.0
|
||||||
|
_boss_bar_fill_animating = true
|
||||||
|
if center_bottom_boss:
|
||||||
|
center_bottom_boss.visible = true
|
||||||
if texture_progress_bar_boss_hp:
|
if texture_progress_bar_boss_hp:
|
||||||
texture_progress_bar_boss_hp.visible = true
|
texture_progress_bar_boss_hp.visible = false
|
||||||
if label_boss:
|
if label_boss:
|
||||||
label_boss.visible = true
|
label_boss.visible = false
|
||||||
|
|
||||||
# Update boss health (properties are always defined in enemy_base.gd)
|
|
||||||
var health = boss_enemy.current_health
|
var health = boss_enemy.current_health
|
||||||
var max_health = boss_enemy.max_health
|
var max_health = max(1.0, boss_enemy.max_health)
|
||||||
|
|
||||||
|
if progress_bar_boss_hp_center:
|
||||||
|
progress_bar_boss_hp_center.max_value = max_health
|
||||||
|
if _boss_bar_fill_animating:
|
||||||
|
if not _boss_bar_fill_tween_started:
|
||||||
|
_boss_bar_fill_tween_started = true
|
||||||
|
var tween = create_tween()
|
||||||
|
tween.tween_property(progress_bar_boss_hp_center, "value", health, 0.5).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_QUAD)
|
||||||
|
tween.finished.connect(func(): _boss_bar_fill_animating = false; _boss_bar_fill_tween_started = false)
|
||||||
|
else:
|
||||||
|
progress_bar_boss_hp_center.value = health
|
||||||
if texture_progress_bar_boss_hp:
|
if texture_progress_bar_boss_hp:
|
||||||
texture_progress_bar_boss_hp.max_value = max_health
|
texture_progress_bar_boss_hp.max_value = max_health
|
||||||
texture_progress_bar_boss_hp.value = health
|
texture_progress_bar_boss_hp.value = health
|
||||||
else:
|
else:
|
||||||
# Hide boss health bar if no boss
|
_boss_bar_fill_animating = false
|
||||||
|
_boss_bar_fill_tween_started = false
|
||||||
|
if center_bottom_boss:
|
||||||
|
center_bottom_boss.visible = false
|
||||||
if texture_progress_bar_boss_hp:
|
if texture_progress_bar_boss_hp:
|
||||||
texture_progress_bar_boss_hp.visible = false
|
texture_progress_bar_boss_hp.visible = false
|
||||||
if label_boss:
|
if label_boss:
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ var chest_closed_frame: int = -1
|
|||||||
var chest_opened_frame: int = -1
|
var chest_opened_frame: int = -1
|
||||||
var is_chest_opened: bool = false
|
var is_chest_opened: bool = false
|
||||||
|
|
||||||
|
# Hidden chest: invisible until detected via perception
|
||||||
|
var is_hidden: bool = false
|
||||||
|
var is_detected: bool = false
|
||||||
|
var player_detection_attempts: Dictionary = {} # peer_id -> true once they've attempted
|
||||||
|
var player_detection_fail_time: Dictionary = {} # peer_id -> Time.get_ticks_msec() of last failed roll (retry only after 60s)
|
||||||
|
const HIDDEN_CHEST_RETRY_COOLDOWN_MS: int = 60000 # 60 seconds before allowing another perception check after a fail
|
||||||
|
|
||||||
# Network sync timer
|
# Network sync timer
|
||||||
var sync_timer: float = 0.0
|
var sync_timer: float = 0.0
|
||||||
var sync_interval: float = 0.05 # Sync 20 times per second
|
var sync_interval: float = 0.05 # Sync 20 times per second
|
||||||
@@ -73,6 +80,94 @@ 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
|
||||||
|
|
||||||
|
# Hidden chest: apply initial state (game_world may set is_hidden after we're added)
|
||||||
|
call_deferred("_apply_hidden_state_if_needed")
|
||||||
|
|
||||||
|
func _apply_hidden_state_if_needed() -> void:
|
||||||
|
if is_hidden and not is_detected:
|
||||||
|
_apply_hidden_state()
|
||||||
|
|
||||||
|
func _apply_hidden_state() -> void:
|
||||||
|
if is_hidden and not is_detected:
|
||||||
|
if sprite:
|
||||||
|
sprite.modulate.a = 0.0
|
||||||
|
if sprite_above:
|
||||||
|
sprite_above.modulate.a = 0.0
|
||||||
|
if shadow:
|
||||||
|
shadow.visible = false
|
||||||
|
# Enable detection area and connect if not already
|
||||||
|
var det = get_node_or_null("DetectionArea")
|
||||||
|
if det:
|
||||||
|
if not det.body_entered.is_connected(_on_detection_area_body_entered):
|
||||||
|
det.body_entered.connect(_on_detection_area_body_entered)
|
||||||
|
if not det.body_exited.is_connected(_on_detection_area_body_exited):
|
||||||
|
det.body_exited.connect(_on_detection_area_body_exited)
|
||||||
|
det.monitoring = true
|
||||||
|
else:
|
||||||
|
if sprite:
|
||||||
|
sprite.modulate.a = 1.0
|
||||||
|
if sprite_above:
|
||||||
|
sprite_above.modulate.a = 1.0
|
||||||
|
if shadow:
|
||||||
|
shadow.visible = true
|
||||||
|
|
||||||
|
func _on_detection_area_body_entered(body: Node) -> void:
|
||||||
|
if not body.is_in_group("player"):
|
||||||
|
return
|
||||||
|
if object_type != "Chest" or not is_hidden or is_detected:
|
||||||
|
return
|
||||||
|
var peer_id = body.get_multiplayer_authority() if "get_multiplayer_authority" in body else 0
|
||||||
|
# If this player already attempted and failed, allow retry only after 60 seconds
|
||||||
|
if player_detection_attempts.has(peer_id):
|
||||||
|
var fail_ms: int = player_detection_fail_time.get(peer_id, 0)
|
||||||
|
if fail_ms > 0 and (Time.get_ticks_msec() - fail_ms) < HIDDEN_CHEST_RETRY_COOLDOWN_MS:
|
||||||
|
return
|
||||||
|
# Mark that this player is attempting (or retrying after cooldown)
|
||||||
|
player_detection_attempts[peer_id] = true
|
||||||
|
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer():
|
||||||
|
_roll_hidden_chest_perception(body)
|
||||||
|
|
||||||
|
func _on_detection_area_body_exited(_body: Node) -> void:
|
||||||
|
# No longer erase attempts on exit; retry only after 60s cooldown (checked on re-enter)
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _roll_hidden_chest_perception(player: Node) -> void:
|
||||||
|
if not player or not player.character_stats:
|
||||||
|
return
|
||||||
|
var per_stat = player.character_stats.baseStats.per + player.character_stats.get_pass("per")
|
||||||
|
var roll = randi() % 20 + 1
|
||||||
|
var total = roll + int(per_stat / 2) - 5
|
||||||
|
var dc = 13 # Slightly easier so detection is noticeable (was 15)
|
||||||
|
if total >= dc:
|
||||||
|
_detect_hidden_chest(player)
|
||||||
|
else:
|
||||||
|
var peer_id = player.get_multiplayer_authority() if "get_multiplayer_authority" in player else 0
|
||||||
|
player_detection_fail_time[peer_id] = Time.get_ticks_msec()
|
||||||
|
|
||||||
|
func _detect_hidden_chest(detecting_player: Node) -> void:
|
||||||
|
is_detected = true
|
||||||
|
_apply_hidden_state()
|
||||||
|
# Spawn detected effect (synced)
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("spawn_detected_effect_at"):
|
||||||
|
game_world.spawn_detected_effect_at(global_position)
|
||||||
|
# Alert and sound for the detecting player only (SfxAhaa + SfxSecretFound)
|
||||||
|
if detecting_player and is_instance_valid(detecting_player) and detecting_player.has_method("_on_secret_chest_detected"):
|
||||||
|
if detecting_player.is_multiplayer_authority():
|
||||||
|
detecting_player._on_secret_chest_detected()
|
||||||
|
else:
|
||||||
|
detecting_player._on_secret_chest_detected.rpc_id(detecting_player.get_multiplayer_authority())
|
||||||
|
# Sync to all clients
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
||||||
|
var game_world_sync = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world_sync and game_world_sync.has_method("_sync_hidden_chest_detected_by_name"):
|
||||||
|
game_world_sync._sync_hidden_chest_detected_by_name.rpc(name)
|
||||||
|
|
||||||
|
func _sync_hidden_chest_detected() -> void:
|
||||||
|
# Called on clients when server detected this hidden chest
|
||||||
|
is_detected = true
|
||||||
|
_apply_hidden_state()
|
||||||
|
|
||||||
func _physics_process(delta):
|
func _physics_process(delta):
|
||||||
# All clients simulate physics locally for smooth visuals
|
# All clients simulate physics locally for smooth visuals
|
||||||
# Initial throw state is synced via player's _sync_throw RPC
|
# Initial throw state is synced via player's _sync_throw RPC
|
||||||
@@ -81,13 +176,15 @@ func _physics_process(delta):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not is_frozen:
|
if not is_frozen:
|
||||||
# Fallout: sink and disappear when on ground (not held, not airborne)
|
# Fallout: sink and disappear when on ground (not held, not airborne). Pillars must never sink.
|
||||||
if not is_airborne and position_z <= 0.0:
|
if not is_airborne and position_z <= 0.0 and object_type != "Pillar":
|
||||||
var gw = get_tree().get_first_node_in_group("game_world")
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
if gw and gw.has_method("_is_position_on_fallout_tile") and gw._is_position_on_fallout_tile(global_position):
|
if gw and gw.has_method("_is_position_on_fallout_tile") and gw._is_position_on_fallout_tile(global_position):
|
||||||
if not falling_into_fallout:
|
if not falling_into_fallout:
|
||||||
falling_into_fallout = true
|
falling_into_fallout = true
|
||||||
fallout_sink_progress = 1.0
|
fallout_sink_progress = 1.0
|
||||||
|
if has_node("SfxFall"):
|
||||||
|
$SfxFall.play()
|
||||||
fallout_sink_progress -= delta / FALLOUT_SINK_DURATION
|
fallout_sink_progress -= delta / FALLOUT_SINK_DURATION
|
||||||
if fallout_sink_progress <= 0.0:
|
if fallout_sink_progress <= 0.0:
|
||||||
queue_free()
|
queue_free()
|
||||||
@@ -388,6 +485,9 @@ func _is_wall_collider(collider) -> bool:
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
func can_be_grabbed() -> bool:
|
func can_be_grabbed() -> bool:
|
||||||
|
# Hidden chest cannot be opened until detected
|
||||||
|
if object_type == "Chest" and is_hidden and not is_detected:
|
||||||
|
return false
|
||||||
return is_grabbable and not is_being_held
|
return is_grabbable and not is_being_held
|
||||||
|
|
||||||
func _get_configured_object_type() -> String:
|
func _get_configured_object_type() -> String:
|
||||||
@@ -453,6 +553,9 @@ func take_fire_damage(amount: float, _attacker_position: Vector2) -> void:
|
|||||||
|
|
||||||
func take_damage(amount: float, _from_position: Vector2) -> void:
|
func take_damage(amount: float, _from_position: Vector2) -> void:
|
||||||
"""Generic damage from bomb, frost spike, etc. Any destroyable object."""
|
"""Generic damage from bomb, frost spike, etc. Any destroyable object."""
|
||||||
|
# Hidden chest cannot be attacked until detected (must be revealed first)
|
||||||
|
if object_type == "Chest" and is_hidden and not is_detected:
|
||||||
|
return
|
||||||
if not is_destroyable or is_broken:
|
if not is_destroyable or is_broken:
|
||||||
return
|
return
|
||||||
health -= amount
|
health -= amount
|
||||||
@@ -471,6 +574,9 @@ func take_damage(amount: float, _from_position: Vector2) -> void:
|
|||||||
_break_into_pieces()
|
_break_into_pieces()
|
||||||
|
|
||||||
func on_grabbed(by_player):
|
func on_grabbed(by_player):
|
||||||
|
# Hidden chest cannot be opened until detected
|
||||||
|
if object_type == "Chest" and is_hidden and not is_detected:
|
||||||
|
return
|
||||||
# Special handling for chests - open instead of grab
|
# Special handling for chests - open instead of grab
|
||||||
if object_type == "Chest" and not is_chest_opened:
|
if object_type == "Chest" and not is_chest_opened:
|
||||||
# In multiplayer, send RPC to server if client is opening
|
# In multiplayer, send RPC to server if client is opening
|
||||||
@@ -807,6 +913,13 @@ func _open_chest(by_player: Node = null):
|
|||||||
return
|
return
|
||||||
$SfxOpenChest.play()
|
$SfxOpenChest.play()
|
||||||
is_chest_opened = true
|
is_chest_opened = true
|
||||||
|
# Remove reveal effect when opening a hidden chest (was detected, now opened)
|
||||||
|
if is_hidden:
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw and gw.has_method("remove_detected_effect_at_position"):
|
||||||
|
gw.remove_detected_effect_at_position(global_position)
|
||||||
|
if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and gw.has_method("_rpc_to_ready_peers"):
|
||||||
|
gw._rpc_to_ready_peers("_sync_remove_detected_effect_at_position", [global_position.x, global_position.y])
|
||||||
|
|
||||||
# Track opened chest for syncing to new clients
|
# Track opened chest for syncing to new clients
|
||||||
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||||
@@ -860,69 +973,81 @@ func _open_chest(by_player: Node = null):
|
|||||||
if item_data.has("rarity") and item_data["rarity"] == ItemDatabase.ItemRarity.COMMON:
|
if item_data.has("rarity") and item_data["rarity"] == ItemDatabase.ItemRarity.COMMON:
|
||||||
candidates.append(item_id)
|
candidates.append(item_id)
|
||||||
|
|
||||||
# Select random item from candidates using deterministic RNG
|
# Chance for empty chest (no item, no coin, no loot)
|
||||||
var random_item_id = candidates[chest_rng.randi() % candidates.size()] if not candidates.is_empty() else null
|
var empty_roll = chest_rng.randf()
|
||||||
|
var is_empty_chest = (empty_roll < 0.18)
|
||||||
|
|
||||||
|
# Select random item from candidates using deterministic RNG (null if empty)
|
||||||
|
var random_item_id = null
|
||||||
|
if not is_empty_chest and not candidates.is_empty():
|
||||||
|
random_item_id = candidates[chest_rng.randi() % candidates.size()]
|
||||||
var chest_item = ItemDatabase.create_item(random_item_id) if random_item_id else null
|
var chest_item = ItemDatabase.create_item(random_item_id) if random_item_id else null
|
||||||
|
|
||||||
# CRITICAL: Instantly give item to player instead of spawning loot object
|
# CRITICAL: Instantly give item to player instead of spawning loot object (or show EMPTY CHEST)
|
||||||
if by_player and is_instance_valid(by_player) and by_player.is_in_group("player") and chest_item:
|
if by_player and is_instance_valid(by_player) and by_player.is_in_group("player"):
|
||||||
# Add item to player inventory
|
if chest_item:
|
||||||
if by_player.character_stats:
|
# Add item to player inventory
|
||||||
var was_encumbered = by_player.character_stats.is_over_encumbered()
|
if by_player.character_stats:
|
||||||
by_player.character_stats.add_item(chest_item)
|
var was_encumbered = by_player.character_stats.is_over_encumbered()
|
||||||
if not was_encumbered and by_player.character_stats.is_over_encumbered():
|
by_player.character_stats.add_item(chest_item)
|
||||||
if by_player.has_method("show_floating_status"):
|
if not was_encumbered and by_player.character_stats.is_over_encumbered():
|
||||||
by_player.show_floating_status("Encumbered!", Color(1.0, 0.5, 0.2))
|
if by_player.has_method("show_floating_status"):
|
||||||
|
by_player.show_floating_status("Encumbered!", Color(1.0, 0.5, 0.2))
|
||||||
|
|
||||||
# Show pickup notification
|
# Show pickup notification
|
||||||
var items_texture = load(chest_item.spritePath) if chest_item.spritePath != "" else null
|
var items_texture = load(chest_item.spritePath) if chest_item.spritePath != "" else null
|
||||||
var display_text = chest_item.item_name.to_upper()
|
var display_text = chest_item.item_name.to_upper()
|
||||||
var item_color = Color.WHITE
|
var item_color = Color.WHITE
|
||||||
|
|
||||||
# Determine color based on item type/rarity
|
# Determine color based on item type/rarity
|
||||||
if chest_item.item_type == Item.ItemType.Restoration:
|
if chest_item.item_type == Item.ItemType.Restoration:
|
||||||
item_color = Color.GREEN
|
item_color = Color.GREEN
|
||||||
elif chest_item.item_type == Item.ItemType.Equippable:
|
elif chest_item.item_type == Item.ItemType.Equippable:
|
||||||
item_color = Color.CYAN # Cyan for equipment (matches loot pickup color)
|
item_color = Color.CYAN # Cyan for equipment (matches loot pickup color)
|
||||||
else:
|
else:
|
||||||
item_color = Color.WHITE
|
item_color = Color.WHITE
|
||||||
|
|
||||||
# Show notification with item sprite (pass chest_item for ItemSprite colorization)
|
# Show notification with item sprite (pass chest_item for ItemSprite colorization)
|
||||||
if items_texture:
|
if items_texture:
|
||||||
_show_item_pickup_notification(by_player, display_text, item_color, items_texture, chest_item.spriteFrames.x, chest_item.spriteFrames.y, chest_item.spriteFrame, chest_item)
|
_show_item_pickup_notification(by_player, display_text, item_color, items_texture, chest_item.spriteFrames.x, chest_item.spriteFrames.y, chest_item.spriteFrame, chest_item)
|
||||||
else:
|
else:
|
||||||
_show_item_pickup_notification(by_player, display_text, item_color, null, 0, 0, 0, chest_item)
|
_show_item_pickup_notification(by_player, display_text, item_color, null, 0, 0, 0, chest_item)
|
||||||
|
# Play get-item sound when receiving inventory item from chest
|
||||||
# Play chest open sound
|
if has_node("SfxGetItemFromChest"):
|
||||||
if has_node("SfxChestOpen"):
|
$SfxGetItemFromChest.play()
|
||||||
$SfxChestOpen.play()
|
print(name, " opened by ", by_player.name, "! Item given: ", chest_item.item_name)
|
||||||
|
# Sync chest opening visual to all clients (item already given on server)
|
||||||
print(name, " opened by ", by_player.name, "! Item given: ", chest_item.item_name)
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
var player_peer_id = by_player.get_multiplayer_authority() if by_player else 0
|
||||||
# Sync chest opening visual to all clients (item already given on server)
|
if game_world and game_world.has_method("_rpc_to_ready_peers"):
|
||||||
if multiplayer.has_multiplayer_peer():
|
var chest_name = name
|
||||||
var player_peer_id = by_player.get_multiplayer_authority() if by_player else 0
|
if has_meta("object_index"):
|
||||||
# Reuse game_world from earlier in the function
|
chest_name = "InteractableObject_%d" % get_meta("object_index")
|
||||||
if game_world and game_world.has_method("_rpc_to_ready_peers"):
|
var item_data = chest_item.save() if chest_item else {}
|
||||||
var chest_name = name
|
game_world._rpc_to_ready_peers("_sync_chest_open_by_name", [chest_name, "item", player_peer_id, item_data])
|
||||||
if has_meta("object_index"):
|
if multiplayer.is_server():
|
||||||
chest_name = "InteractableObject_%d" % get_meta("object_index")
|
var owner_id = by_player.get_multiplayer_authority()
|
||||||
# Sync chest open visual with item_data so clients can show the floating text
|
if owner_id != 1 and owner_id != multiplayer.get_unique_id():
|
||||||
var item_data = chest_item.save() if chest_item else {}
|
var inv_data: Array = []
|
||||||
game_world._rpc_to_ready_peers("_sync_chest_open_by_name", [chest_name, "item", player_peer_id, item_data])
|
for inv_item in by_player.character_stats.inventory:
|
||||||
# Sync inventory+equipment to joiner (server added item; joiner's client must apply)
|
inv_data.append(inv_item.save() if inv_item else null)
|
||||||
if multiplayer.is_server():
|
var equip_data: Dictionary = {}
|
||||||
var owner_id = by_player.get_multiplayer_authority()
|
for slot_name in by_player.character_stats.equipment.keys():
|
||||||
if owner_id != 1 and owner_id != multiplayer.get_unique_id():
|
var eq = by_player.character_stats.equipment[slot_name]
|
||||||
var inv_data: Array = []
|
equip_data[slot_name] = eq.save() if eq else null
|
||||||
for inv_item in by_player.character_stats.inventory:
|
if by_player.has_method("_apply_inventory_and_equipment_from_server"):
|
||||||
inv_data.append(inv_item.save() if inv_item else null)
|
by_player._apply_inventory_and_equipment_from_server.rpc_id(owner_id, inv_data, equip_data)
|
||||||
var equip_data: Dictionary = {}
|
else:
|
||||||
for slot_name in by_player.character_stats.equipment.keys():
|
# Empty chest: show "EMPTY CHEST" floating text
|
||||||
var eq = by_player.character_stats.equipment[slot_name]
|
_show_item_pickup_notification(by_player, "EMPTY CHEST", Color(0.5, 0.5, 0.5))
|
||||||
equip_data[slot_name] = eq.save() if eq else null
|
print(name, " opened by ", by_player.name, " - empty chest")
|
||||||
if by_player.has_method("_apply_inventory_and_equipment_from_server"):
|
if multiplayer.has_multiplayer_peer():
|
||||||
by_player._apply_inventory_and_equipment_from_server.rpc_id(owner_id, inv_data, equip_data)
|
var player_peer_id = by_player.get_multiplayer_authority() if by_player else 0
|
||||||
|
if game_world and game_world.has_method("_rpc_to_ready_peers"):
|
||||||
|
var chest_name = name
|
||||||
|
if has_meta("object_index"):
|
||||||
|
chest_name = "InteractableObject_%d" % get_meta("object_index")
|
||||||
|
game_world._rpc_to_ready_peers("_sync_chest_open_by_name", [chest_name, "empty", player_peer_id, {}])
|
||||||
else:
|
else:
|
||||||
push_error("Chest: ERROR - No valid player to give item to!")
|
push_error("Chest: ERROR - No valid player to give item to!")
|
||||||
|
|
||||||
@@ -991,6 +1116,8 @@ func _sync_chest_open(loot_type_str: String = "coin", player_peer_id: int = 0, i
|
|||||||
_show_item_pickup_notification(player, display_text, item_color, items_texture, chest_item.spriteFrames.x, chest_item.spriteFrames.y, chest_item.spriteFrame, chest_item)
|
_show_item_pickup_notification(player, display_text, item_color, items_texture, chest_item.spriteFrames.x, chest_item.spriteFrames.y, chest_item.spriteFrame, chest_item)
|
||||||
else:
|
else:
|
||||||
_show_item_pickup_notification(player, display_text, item_color, null, 0, 0, 0, chest_item)
|
_show_item_pickup_notification(player, display_text, item_color, null, 0, 0, 0, chest_item)
|
||||||
|
elif loot_type_str == "empty":
|
||||||
|
_show_item_pickup_notification(player, "EMPTY CHEST", Color(0.5, 0.5, 0.5))
|
||||||
else:
|
else:
|
||||||
# Fallback to old loot type system (for backwards compatibility)
|
# Fallback to old loot type system (for backwards compatibility)
|
||||||
match loot_type_str:
|
match loot_type_str:
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ var equipment_selection_index: int = 0 # Current equipment slot index (0-5: main
|
|||||||
|
|
||||||
# Bar layout constants (align X/Y + bar across rows)
|
# Bar layout constants (align X/Y + bar across rows)
|
||||||
const _BAR_WIDTH: int = 100
|
const _BAR_WIDTH: int = 100
|
||||||
const _BAR_VALUE_MIN_WIDTH: int = 44 # "999/999"
|
const _BAR_VALUE_MIN_WIDTH: int = 44 # "999/999"
|
||||||
const _BAR_LABEL_MIN_WIDTH: int = 52 # "Weight:", "Exp:", "HP:", "MP:", "Coin:"
|
const _BAR_LABEL_MIN_WIDTH: int = 52 # "Weight:", "Exp:", "HP:", "MP:", "Coin:"
|
||||||
|
|
||||||
# Weight UI elements (created programmatically)
|
# Weight UI elements (created programmatically)
|
||||||
var weight_container: HBoxContainer = null
|
var weight_container: HBoxContainer = null
|
||||||
@@ -257,18 +257,19 @@ func _update_stats():
|
|||||||
str(char_stats.baseStats.lck) + "\n" + \
|
str(char_stats.baseStats.lck) + "\n" + \
|
||||||
str(char_stats.baseStats.get("per", 10))
|
str(char_stats.baseStats.get("per", 10))
|
||||||
|
|
||||||
# Derived stats: DMG, DEF, MovSpd, AtkSpd, Sight, SpellAmp, Crit% (XP/Coin moved to exp meter & coin UI)
|
# Derived stats: DMG, DEF, MovSpd, AtkSpd, Sight, SpellAmp, Crit%, Dodge% (XP/Coin moved to exp meter & coin UI)
|
||||||
if label_derived_stats:
|
if label_derived_stats:
|
||||||
label_derived_stats.text = "DMG\nDEF\nMovSpd\nAtkSpd\nSight\nSpellAmp\nCrit%"
|
label_derived_stats.text = "DMG\nDEF\nMovSpd\nAtkSpd\nSight\nSpellAmp\nCrit%\nDodge%"
|
||||||
if label_derived_stats_value:
|
if label_derived_stats_value:
|
||||||
label_derived_stats_value.text = "%.1f\n%.1f\n%.2f\n%.2f\n%.1f\n%.1f\n%.1f%%" % [
|
label_derived_stats_value.text = "%.1f\n%.1f\n%.2f\n%.2f\n%.1f\n%.1f\n%.1f%%\n%.1f%%" % [
|
||||||
char_stats.damage,
|
char_stats.damage,
|
||||||
char_stats.defense,
|
char_stats.defense,
|
||||||
char_stats.move_speed,
|
char_stats.move_speed,
|
||||||
char_stats.attack_speed,
|
char_stats.attack_speed,
|
||||||
char_stats.sight,
|
char_stats.sight,
|
||||||
char_stats.spell_amp,
|
char_stats.spell_amp,
|
||||||
char_stats.crit_chance
|
char_stats.crit_chance,
|
||||||
|
char_stats.dodge_chance * 100.0
|
||||||
]
|
]
|
||||||
|
|
||||||
# HP bar
|
# HP bar
|
||||||
@@ -1042,7 +1043,7 @@ func _update_selection_from_navigation():
|
|||||||
var row = inventory_rows_list[inventory_selection_row]
|
var row = inventory_rows_list[inventory_selection_row]
|
||||||
print("InventoryUI: Checking item selection - row: ", inventory_selection_row, " col: ", inventory_selection_col, " row exists: ", row != null, " row child count: ", row.get_child_count() if row else 0)
|
print("InventoryUI: Checking item selection - row: ", inventory_selection_row, " col: ", inventory_selection_col, " row exists: ", row != null, " row child count: ", row.get_child_count() if row else 0)
|
||||||
if row and inventory_selection_col >= 0 and inventory_selection_col < row.get_child_count():
|
if row and inventory_selection_col >= 0 and inventory_selection_col < row.get_child_count():
|
||||||
var items_per_row = 8 # Must match the items_per_row used when building rows
|
var items_per_row = 8 # Must match the items_per_row used when building rows
|
||||||
var item_index = inventory_selection_row * items_per_row + inventory_selection_col
|
var item_index = inventory_selection_row * items_per_row + inventory_selection_col
|
||||||
print("InventoryUI: Calculated item_index: ", item_index, " inventory_items_list.size(): ", inventory_items_list.size())
|
print("InventoryUI: Calculated item_index: ", item_index, " inventory_items_list.size(): ", inventory_items_list.size())
|
||||||
if item_index >= 0 and item_index < inventory_items_list.size():
|
if item_index >= 0 and item_index < inventory_items_list.size():
|
||||||
@@ -1368,7 +1369,7 @@ func _on_inventory_item_pressed(item: Item):
|
|||||||
|
|
||||||
_update_selection_highlight()
|
_update_selection_highlight()
|
||||||
_update_selection_rectangle()
|
_update_selection_rectangle()
|
||||||
_update_info_panel() # Show item description on single-click
|
_update_info_panel() # Show item description on single-click
|
||||||
|
|
||||||
func _on_inventory_item_gui_input(event: InputEvent, item: Item):
|
func _on_inventory_item_gui_input(event: InputEvent, item: Item):
|
||||||
# Handle double-click to equip/consume and right-click to drop
|
# Handle double-click to equip/consume and right-click to drop
|
||||||
@@ -1679,6 +1680,9 @@ func _use_consumable_item(item: Item):
|
|||||||
if item.modifiers.has("mp"):
|
if item.modifiers.has("mp"):
|
||||||
var mana_amount = item.modifiers["mp"]
|
var mana_amount = item.modifiers["mp"]
|
||||||
char_stats.restore_mana(mana_amount)
|
char_stats.restore_mana(mana_amount)
|
||||||
|
# Dodge potion (and any consumable with dodge_chance + duration): apply temporary dodge buff
|
||||||
|
if item.modifiers.has("dodge_chance") and item.duration > 0:
|
||||||
|
char_stats.add_buff_dodge_chance(item.modifiers["dodge_chance"], item.duration)
|
||||||
|
|
||||||
var index = char_stats.inventory.find(item)
|
var index = char_stats.inventory.find(item)
|
||||||
if index >= 0:
|
if index >= 0:
|
||||||
@@ -1845,3 +1849,6 @@ func _lock_player_controls(lock: bool):
|
|||||||
var local_players = player_manager.get_local_players()
|
var local_players = player_manager.get_local_players()
|
||||||
for player in local_players:
|
for player in local_players:
|
||||||
player.controls_disabled = lock
|
player.controls_disabled = lock
|
||||||
|
if lock:
|
||||||
|
# Stop movement immediately when opening inventory (physics may have already run this frame)
|
||||||
|
player.velocity = Vector2.ZERO
|
||||||
|
|||||||
@@ -186,7 +186,8 @@ func _update_stats():
|
|||||||
str(char_stats.attack_speed) + "\n" + \
|
str(char_stats.attack_speed) + "\n" + \
|
||||||
str(char_stats.sight) + "\n" + \
|
str(char_stats.sight) + "\n" + \
|
||||||
str(char_stats.spell_amp) + "\n" + \
|
str(char_stats.spell_amp) + "\n" + \
|
||||||
str(char_stats.crit_chance) + "%"
|
str(char_stats.crit_chance) + "%\n" + \
|
||||||
|
("%.1f" % (char_stats.dodge_chance * 100.0)) + "%"
|
||||||
|
|
||||||
func _create_equipment_slots():
|
func _create_equipment_slots():
|
||||||
# Equipment slot order: mainhand, offhand, headgear, armour, boots, accessory
|
# Equipment slot order: mainhand, offhand, headgear, armour, boots, accessory
|
||||||
|
|||||||
@@ -966,10 +966,7 @@ static func _load_all_items():
|
|||||||
"description": "A standard longsword",
|
"description": "A standard longsword",
|
||||||
"item_type": Item.ItemType.Equippable,
|
"item_type": Item.ItemType.Equippable,
|
||||||
"equipment_type": Item.EquipmentType.MAINHAND,
|
"equipment_type": Item.EquipmentType.MAINHAND,
|
||||||
"weapon_type": Item.WeaponType.SWORD,
|
|
||||||
"spriteFrame": 3 * 20 + 10, # 10,3
|
|
||||||
"modifiers": {"dmg": 5},
|
"modifiers": {"dmg": 5},
|
||||||
"buy_cost": 100,
|
|
||||||
"sell_worth": 30,
|
"sell_worth": 30,
|
||||||
"rarity": ItemRarity.COMMON,
|
"rarity": ItemRarity.COMMON,
|
||||||
"weight": 3.5
|
"weight": 3.5
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ var item: Item = null # Item instance (for LootType.ITEM)
|
|||||||
# Quantity badge for items with quantity > 1
|
# Quantity badge for items with quantity > 1
|
||||||
var quantity_badge: Label = null
|
var quantity_badge: Label = null
|
||||||
|
|
||||||
# Fallout: sink and disappear (no recovery)
|
# Fallout: sink and disappear (no recovery); keys respawn on nearest safe tile
|
||||||
var falling_into_fallout: bool = false
|
var falling_into_fallout: bool = false
|
||||||
var fallout_sink_progress: float = 1.0
|
var fallout_sink_progress: float = 1.0
|
||||||
const FALLOUT_SINK_DURATION: float = 0.4
|
const FALLOUT_SINK_DURATION: float = 0.4
|
||||||
@@ -234,6 +234,8 @@ func _physics_process(delta):
|
|||||||
if not falling_into_fallout:
|
if not falling_into_fallout:
|
||||||
falling_into_fallout = true
|
falling_into_fallout = true
|
||||||
fallout_sink_progress = 1.0
|
fallout_sink_progress = 1.0
|
||||||
|
if has_node("SfxFall"):
|
||||||
|
$SfxFall.play()
|
||||||
# Lock to center of fallout tile and stop all x/y movement
|
# Lock to center of fallout tile and stop all x/y movement
|
||||||
if gw.has_method("_get_tile_center_at"):
|
if gw.has_method("_get_tile_center_at"):
|
||||||
global_position = gw._get_tile_center_at(global_position)
|
global_position = gw._get_tile_center_at(global_position)
|
||||||
@@ -244,13 +246,26 @@ func _physics_process(delta):
|
|||||||
pickup_area.monitorable = false
|
pickup_area.monitorable = false
|
||||||
fallout_sink_progress -= delta / FALLOUT_SINK_DURATION
|
fallout_sink_progress -= delta / FALLOUT_SINK_DURATION
|
||||||
if fallout_sink_progress <= 0.0:
|
if fallout_sink_progress <= 0.0:
|
||||||
# Sync removal to clients so joiner sees loot disappear (same as pickup)
|
# Keys respawn on a safe tile (like player); other loot is removed
|
||||||
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
if loot_type == LootType.KEY:
|
||||||
var loot_id = get_meta("loot_id") if has_meta("loot_id") else -1
|
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
if game_world and game_world.has_method("_sync_loot_remove"):
|
if game_world and game_world.has_method("_get_nearest_safe_tile_center"):
|
||||||
game_world._rpc_to_ready_peers("_sync_loot_remove", [loot_id, global_position])
|
var safe_pos = game_world._get_nearest_safe_tile_center(global_position)
|
||||||
queue_free()
|
_respawn_key_at_safe_position(safe_pos)
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
||||||
|
var loot_id = get_meta("loot_id") if has_meta("loot_id") else -1
|
||||||
|
if game_world.has_method("_rpc_to_ready_peers"):
|
||||||
|
game_world._rpc_to_ready_peers("_sync_key_respawn", [loot_id, safe_pos])
|
||||||
|
else:
|
||||||
|
queue_free()
|
||||||
|
else:
|
||||||
|
# Sync removal to clients so joiner sees loot disappear (same as pickup)
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
||||||
|
var loot_id = get_meta("loot_id") if has_meta("loot_id") else -1
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_sync_loot_remove"):
|
||||||
|
game_world._rpc_to_ready_peers("_sync_loot_remove", [loot_id, global_position])
|
||||||
|
queue_free()
|
||||||
return
|
return
|
||||||
scale = Vector2.ONE * max(0.0, fallout_sink_progress)
|
scale = Vector2.ONE * max(0.0, fallout_sink_progress)
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
@@ -447,6 +462,20 @@ func _animate_coin(delta):
|
|||||||
var frame = int(coin_anim_time) % 6
|
var frame = int(coin_anim_time) % 6
|
||||||
sprite.frame = frame
|
sprite.frame = frame
|
||||||
|
|
||||||
|
func _respawn_key_at_safe_position(safe_pos: Vector2):
|
||||||
|
# Reposition key on a safe (non-fallout) tile and reset state so it can be picked up again
|
||||||
|
global_position = safe_pos
|
||||||
|
falling_into_fallout = false
|
||||||
|
fallout_sink_progress = 1.0
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
velocity_z = 0.0
|
||||||
|
position_z = SPAWN_POSITION_Z
|
||||||
|
is_airborne = false
|
||||||
|
scale = Vector2.ONE
|
||||||
|
if pickup_area:
|
||||||
|
pickup_area.monitoring = true
|
||||||
|
pickup_area.monitorable = true
|
||||||
|
|
||||||
func _on_pickup_area_body_entered(body):
|
func _on_pickup_area_body_entered(body):
|
||||||
if falling_into_fallout:
|
if falling_into_fallout:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -9,21 +9,24 @@ const COLOR_UNEXPLORED: Color = Color(0.08, 0.08, 0.1)
|
|||||||
const COLOR_WALL: Color = Color(0.22, 0.22, 0.26)
|
const COLOR_WALL: Color = Color(0.22, 0.22, 0.26)
|
||||||
const COLOR_FLOOR: Color = Color(0.38, 0.38, 0.44)
|
const COLOR_FLOOR: Color = Color(0.38, 0.38, 0.44)
|
||||||
const COLOR_PLAYER: Color = Color(1.0, 0.35, 0.35)
|
const COLOR_PLAYER: Color = Color(1.0, 0.35, 0.35)
|
||||||
|
const COLOR_OTHER_PLAYER: Color = Color(0.35, 0.6, 1.0) # Blue for other players
|
||||||
const COLOR_EXIT: Color = Color(1.0, 1.0, 1.0)
|
const COLOR_EXIT: Color = Color(1.0, 1.0, 1.0)
|
||||||
|
|
||||||
var _map_size: Vector2i = Vector2i.ZERO
|
var _map_size: Vector2i = Vector2i.ZERO
|
||||||
var _explored_map: PackedInt32Array = PackedInt32Array()
|
var _explored_map: PackedInt32Array = PackedInt32Array()
|
||||||
var _grid: Array = [] # 2D grid [x][y]: 0=wall, 1=floor, 2=door, 3=corridor
|
var _grid: Array = [] # 2D grid [x][y]: 0=wall, 1=floor, 2=door, 3=corridor
|
||||||
var _player_tile: Vector2i = Vector2i(-1, -1)
|
var _player_tile: Vector2i = Vector2i(-1, -1)
|
||||||
|
var _other_player_tiles: Array = [] # Array of Vector2i for other players
|
||||||
var _exit_tile: Vector2i = Vector2i(-1, -1)
|
var _exit_tile: Vector2i = Vector2i(-1, -1)
|
||||||
var _exit_discovered: bool = false
|
var _exit_discovered: bool = false
|
||||||
|
|
||||||
|
|
||||||
func set_maps(explored_map: PackedInt32Array, map_size: Vector2i, grid: Array, player_tile: Vector2i = Vector2i(-1, -1), exit_tile: Vector2i = Vector2i(-1, -1), exit_discovered: bool = false) -> void:
|
func set_maps(explored_map: PackedInt32Array, map_size: Vector2i, grid: Array, player_tile: Vector2i = Vector2i(-1, -1), exit_tile: Vector2i = Vector2i(-1, -1), exit_discovered: bool = false, other_player_tiles: Array = []) -> void:
|
||||||
_explored_map = explored_map
|
_explored_map = explored_map
|
||||||
_map_size = map_size
|
_map_size = map_size
|
||||||
_grid = grid
|
_grid = grid
|
||||||
_player_tile = player_tile
|
_player_tile = player_tile
|
||||||
|
_other_player_tiles = other_player_tiles
|
||||||
_exit_tile = exit_tile
|
_exit_tile = exit_tile
|
||||||
_exit_discovered = exit_discovered
|
_exit_discovered = exit_discovered
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
@@ -73,6 +76,12 @@ func _draw() -> void:
|
|||||||
var py := float(_player_tile.y) * th + th * 0.5
|
var py := float(_player_tile.y) * th + th * 0.5
|
||||||
var r := maxf(2.0, minf(tw, th) * 0.4)
|
var r := maxf(2.0, minf(tw, th) * 0.4)
|
||||||
draw_circle(Vector2(px, py), r, COLOR_PLAYER)
|
draw_circle(Vector2(px, py), r, COLOR_PLAYER)
|
||||||
|
for other_tile in _other_player_tiles:
|
||||||
|
if other_tile is Vector2i and other_tile.x >= 0 and other_tile.y >= 0 and other_tile.x < _map_size.x and other_tile.y < _map_size.y:
|
||||||
|
var ox := float(other_tile.x) * tw + tw * 0.5
|
||||||
|
var oy := float(other_tile.y) * th + th * 0.5
|
||||||
|
var or_ := maxf(1.5, minf(tw, th) * 0.32)
|
||||||
|
draw_circle(Vector2(ox, oy), or_, COLOR_OTHER_PLAYER)
|
||||||
if _exit_discovered and _exit_tile.x >= 0 and _exit_tile.y >= 0 and _exit_tile.x < _map_size.x and _exit_tile.y < _map_size.y:
|
if _exit_discovered and _exit_tile.x >= 0 and _exit_tile.y >= 0 and _exit_tile.x < _map_size.x and _exit_tile.y < _map_size.y:
|
||||||
var ex := float(_exit_tile.x) * tw + tw * 0.5
|
var ex := float(_exit_tile.x) * tw + tw * 0.5
|
||||||
var ey := float(_exit_tile.y) * th + th * 0.5
|
var ey := float(_exit_tile.y) * th + th * 0.5
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ var reconnection_attempting: bool = false # Track if we're attempting to reconn
|
|||||||
var reconnection_timer: float = 0.0 # Timer for reconnection delay
|
var reconnection_timer: float = 0.0 # Timer for reconnection delay
|
||||||
const RECONNECTION_DELAY: float = 1.0 # Delay before attempting reconnection (reduced from 2.0)
|
const RECONNECTION_DELAY: float = 1.0 # Delay before attempting reconnection (reduced from 2.0)
|
||||||
|
|
||||||
|
# Grace period for joiner disconnect: if they reconnect within this time, treat as same peer (don't despawn)
|
||||||
|
var disconnected_peers: Dictionary = {} # peer_id -> { player_info, disconnect_time }
|
||||||
|
const DISCONNECT_GRACE_SECONDS: float = 18.0
|
||||||
|
|
||||||
# Logging - use LogManager for categorized logging
|
# Logging - use LogManager for categorized logging
|
||||||
func log_print(message: String):
|
func log_print(message: String):
|
||||||
LogManager.log(message, LogManager.CATEGORY_NETWORK)
|
LogManager.log(message, LogManager.CATEGORY_NETWORK)
|
||||||
@@ -69,6 +73,23 @@ func _ready():
|
|||||||
add_child(room_registry)
|
add_child(room_registry)
|
||||||
room_registry.rooms_fetched.connect(_on_rooms_fetched)
|
room_registry.rooms_fetched.connect(_on_rooms_fetched)
|
||||||
|
|
||||||
|
func _finalize_disconnect(peer_id: int) -> void:
|
||||||
|
if not disconnected_peers.has(peer_id):
|
||||||
|
return
|
||||||
|
var data = disconnected_peers[peer_id]
|
||||||
|
disconnected_peers.erase(peer_id)
|
||||||
|
if players_info.has(peer_id):
|
||||||
|
players_info.erase(peer_id)
|
||||||
|
log_print("NetworkManager: Finalizing disconnect for peer " + str(peer_id) + " (grace period ended)")
|
||||||
|
player_disconnected.emit(peer_id, data.player_info)
|
||||||
|
if room_registry and not room_id.is_empty():
|
||||||
|
var player_count = get_all_player_ids().size()
|
||||||
|
var _level = 1
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world:
|
||||||
|
_level = game_world.current_level
|
||||||
|
room_registry.send_room_update(player_count)
|
||||||
|
|
||||||
func set_network_mode(mode: int):
|
func set_network_mode(mode: int):
|
||||||
# 0 = ENet, 1 = WebRTC, 2 = WebSocket
|
# 0 = ENet, 1 = WebRTC, 2 = WebSocket
|
||||||
# WebRTC is now available on native platforms with webrtc-native extension
|
# WebRTC is now available on native platforms with webrtc-native extension
|
||||||
@@ -341,7 +362,18 @@ func _generate_player_names(count: int, peer_id: int) -> Array:
|
|||||||
# Called when a peer connects to the server
|
# Called when a peer connects to the server
|
||||||
func _on_peer_connected(id: int):
|
func _on_peer_connected(id: int):
|
||||||
log_print("Peer connected: " + str(id))
|
log_print("Peer connected: " + str(id))
|
||||||
# Peer is now actually available for RPCs - emit player_connected signal
|
# Reconnection: if this peer was in grace period, treat as reconnection - don't emit player_connected (players still exist)
|
||||||
|
if is_hosting and disconnected_peers.has(id):
|
||||||
|
disconnected_peers.erase(id)
|
||||||
|
log_print("NetworkManager: Peer " + str(id) + " reconnected within grace period - keeping existing players")
|
||||||
|
if room_registry and not room_id.is_empty():
|
||||||
|
var player_count = get_all_player_ids().size()
|
||||||
|
var _level = 1
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world:
|
||||||
|
_level = game_world.current_level
|
||||||
|
room_registry.send_room_update(player_count)
|
||||||
|
return
|
||||||
# Make sure player info is registered (should have been done in _on_matchbox_peer_connected)
|
# Make sure player info is registered (should have been done in _on_matchbox_peer_connected)
|
||||||
if not players_info.has(id):
|
if not players_info.has(id):
|
||||||
# Fallback: register default player info if not already registered
|
# Fallback: register default player info if not already registered
|
||||||
@@ -349,18 +381,12 @@ func _on_peer_connected(id: int):
|
|||||||
"local_player_count": 1, # Default, will be updated via RPC
|
"local_player_count": 1, # Default, will be updated via RPC
|
||||||
"player_names": _generate_player_names(1, id)
|
"player_names": _generate_player_names(1, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
# For joiners, emit connection_succeeded when the first peer (host) connects
|
# For joiners, emit connection_succeeded when the first peer (host) connects
|
||||||
# This ensures the connection is actually established and RPCs can be received
|
|
||||||
# Note: On web, multiplayer.peer_connected might not fire automatically,
|
|
||||||
# so we also check in _on_matchbox_peer_connected as a fallback
|
|
||||||
if not is_hosting and id == 1: # Host connected to joiner
|
if not is_hosting and id == 1: # Host connected to joiner
|
||||||
log_print("NetworkManager: Joiner - host connected via multiplayer.peer_connected, emitting connection_succeeded")
|
log_print("NetworkManager: Joiner - host connected via multiplayer.peer_connected, emitting connection_succeeded")
|
||||||
connection_succeeded.emit()
|
connection_succeeded.emit()
|
||||||
|
|
||||||
# Emit player connected signal (peer is now available for RPCs)
|
# Emit player connected signal (peer is now available for RPCs)
|
||||||
player_connected.emit(id, players_info[id])
|
player_connected.emit(id, players_info[id])
|
||||||
|
|
||||||
# Update room registry if hosting (player count changed)
|
# Update room registry if hosting (player count changed)
|
||||||
if is_hosting and room_registry and not room_id.is_empty():
|
if is_hosting and room_registry and not room_id.is_empty():
|
||||||
var player_count = get_all_player_ids().size()
|
var player_count = get_all_player_ids().size()
|
||||||
@@ -373,20 +399,22 @@ func _on_peer_connected(id: int):
|
|||||||
# Called when a peer disconnects
|
# Called when a peer disconnects
|
||||||
func _on_peer_disconnected(id: int):
|
func _on_peer_disconnected(id: int):
|
||||||
log_print("Peer disconnected: " + str(id))
|
log_print("Peer disconnected: " + str(id))
|
||||||
# Get player_info before erasing it
|
# If joiner lost host, trigger server disconnection immediately (no grace)
|
||||||
var player_info = {}
|
|
||||||
if players_info.has(id):
|
|
||||||
player_info = players_info[id]
|
|
||||||
players_info.erase(id)
|
|
||||||
player_disconnected.emit(id, player_info)
|
|
||||||
|
|
||||||
# If joiner and host (peer ID 1) disconnected, trigger server disconnection logic
|
|
||||||
if not is_hosting and id == 1:
|
if not is_hosting and id == 1:
|
||||||
log_print("NetworkManager: Host (peer ID 1) disconnected, triggering reconnection...")
|
log_print("NetworkManager: Host (peer ID 1) disconnected, triggering reconnection...")
|
||||||
_on_server_disconnected()
|
_on_server_disconnected()
|
||||||
return
|
return
|
||||||
|
# Host: when a joiner disconnects, use grace period so brief drops don't "recreate" them as new
|
||||||
# Update room registry if hosting (player count changed)
|
if is_hosting and players_info.has(id):
|
||||||
|
var pinfo = players_info[id]
|
||||||
|
disconnected_peers[id] = { "player_info": pinfo, "disconnect_time": Time.get_ticks_msec() / 1000.0 }
|
||||||
|
log_print("NetworkManager: Peer " + str(id) + " in grace period for " + str(DISCONNECT_GRACE_SECONDS) + "s (reconnect = same player)")
|
||||||
|
return
|
||||||
|
# Fallback: no players_info (e.g. never fully registered) or not host - finalize immediately
|
||||||
|
var player_info = players_info.get(id, {})
|
||||||
|
if players_info.has(id):
|
||||||
|
players_info.erase(id)
|
||||||
|
player_disconnected.emit(id, player_info)
|
||||||
if is_hosting and room_registry and not room_id.is_empty():
|
if is_hosting and room_registry and not room_id.is_empty():
|
||||||
var player_count = get_all_player_ids().size()
|
var player_count = get_all_player_ids().size()
|
||||||
var _level = 1
|
var _level = 1
|
||||||
@@ -422,6 +450,9 @@ func _on_connection_failed():
|
|||||||
# Called on client when disconnected from server
|
# Called on client when disconnected from server
|
||||||
func _on_server_disconnected():
|
func _on_server_disconnected():
|
||||||
log_print("Server disconnected")
|
log_print("Server disconnected")
|
||||||
|
# Clear so reconnection can emit connection_succeeded again (hides "Disconnected - Reconnecting..." label)
|
||||||
|
if has_meta("connection_succeeded_timer_set"):
|
||||||
|
remove_meta("connection_succeeded_timer_set")
|
||||||
|
|
||||||
# Store reconnection info if we're a joiner (not host)
|
# Store reconnection info if we're a joiner (not host)
|
||||||
if not is_hosting and not room_id.is_empty():
|
if not is_hosting and not room_id.is_empty():
|
||||||
@@ -591,6 +622,12 @@ func _on_matchbox_peer_connected(peer_id: int):
|
|||||||
# For joiners, if this is the host (peer_id 1) and connection_succeeded hasn't been emitted yet,
|
# For joiners, if this is the host (peer_id 1) and connection_succeeded hasn't been emitted yet,
|
||||||
# wait a bit and then emit it as a fallback (in case multiplayer.peer_connected doesn't fire on web)
|
# wait a bit and then emit it as a fallback (in case multiplayer.peer_connected doesn't fire on web)
|
||||||
if not is_hosting and peer_id == 1:
|
if not is_hosting and peer_id == 1:
|
||||||
|
# If we're already in game scene (reconnection), emit immediately to hide "Disconnected - Reconnecting..." label
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw:
|
||||||
|
log_print("NetworkManager: Joiner - host connected via Matchbox (in game scene), emitting connection_succeeded for reconnection")
|
||||||
|
connection_succeeded.emit()
|
||||||
|
return
|
||||||
# Check if we've already set up a timer for this peer (prevent multiple timers)
|
# Check if we've already set up a timer for this peer (prevent multiple timers)
|
||||||
if has_meta("connection_succeeded_timer_set"):
|
if has_meta("connection_succeeded_timer_set"):
|
||||||
log_print("NetworkManager: Joiner - timer already set for peer " + str(peer_id) + ", skipping")
|
log_print("NetworkManager: Joiner - timer already set for peer " + str(peer_id) + ", skipping")
|
||||||
@@ -666,7 +703,10 @@ func _on_matchbox_peer_connected(peer_id: int):
|
|||||||
log_print("NetworkManager: Attempting to change scene directly to game_world.tscn")
|
log_print("NetworkManager: Attempting to change scene directly to game_world.tscn")
|
||||||
current_tree.change_scene_to_packed(game_world_scene)
|
current_tree.change_scene_to_packed(game_world_scene)
|
||||||
else:
|
else:
|
||||||
log_print("NetworkManager: Joiner - already in game scene, connection_succeeded was already emitted")
|
# In game scene: either initial join (peer_connected already fired) or RECONNECTION.
|
||||||
|
# On reconnection we never emitted connection_succeeded, so emit it now to hide "Disconnected - Reconnecting..." label.
|
||||||
|
log_print("NetworkManager: Joiner - in game scene, emitting connection_succeeded (reconnection or fallback)")
|
||||||
|
connection_succeeded.emit()
|
||||||
)
|
)
|
||||||
|
|
||||||
# On web, multiplayer.peer_connected might not fire for either host or joiner
|
# On web, multiplayer.peer_connected might not fire for either host or joiner
|
||||||
@@ -747,7 +787,17 @@ func get_room_id() -> String:
|
|||||||
return room_id
|
return room_id
|
||||||
|
|
||||||
func _process(delta: float):
|
func _process(delta: float):
|
||||||
# Handle reconnection timer
|
# Finalize disconnect for peers that didn't reconnect within grace period (host only)
|
||||||
|
if is_hosting and not disconnected_peers.is_empty():
|
||||||
|
var now = Time.get_ticks_msec() / 1000.0
|
||||||
|
var to_finalize = []
|
||||||
|
for pid in disconnected_peers.keys():
|
||||||
|
var data = disconnected_peers[pid]
|
||||||
|
if now - data.disconnect_time >= DISCONNECT_GRACE_SECONDS:
|
||||||
|
to_finalize.append(pid)
|
||||||
|
for pid in to_finalize:
|
||||||
|
_finalize_disconnect(pid)
|
||||||
|
# Handle reconnection timer (joiner only)
|
||||||
if reconnection_attempting:
|
if reconnection_attempting:
|
||||||
reconnection_timer -= delta
|
reconnection_timer -= delta
|
||||||
if reconnection_timer <= 0.0:
|
if reconnection_timer <= 0.0:
|
||||||
@@ -827,6 +877,43 @@ func _generate_room_id() -> String:
|
|||||||
code += chars[randi() % chars.length()]
|
code += chars[randi() % chars.length()]
|
||||||
return code
|
return code
|
||||||
|
|
||||||
|
# Pending boss spider spawns (client only): when RPC arrives before GameWorld is in tree, store and apply when ready
|
||||||
|
var pending_boss_spider_spawns: Array = [] # [{p1x, p1y, p2x, p2y, p3x, p3y, idx0, idx1, idx2}, ...]
|
||||||
|
|
||||||
|
# Boss spider spawn: relay RPC so joiner always receives it (autoload exists on all peers)
|
||||||
|
@rpc("authority", "reliable")
|
||||||
|
func spawn_boss_spiders_client(p1x: float, p1y: float, p2x: float, p2y: float, p3x: float, p3y: float, idx0: int, idx1: int, idx2: int) -> void:
|
||||||
|
if multiplayer.is_server():
|
||||||
|
return
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if not game_world or not game_world.has_method("_do_client_spawn_boss_spiders"):
|
||||||
|
# Joiner may not have GameWorld in tree yet; queue and apply when GameWorld calls apply_pending_boss_spider_spawns
|
||||||
|
pending_boss_spider_spawns.append({"p1x": p1x, "p1y": p1y, "p2x": p2x, "p2y": p2y, "p3x": p3x, "p3y": p3y, "idx0": idx0, "idx1": idx1, "idx2": idx2})
|
||||||
|
# Schedule retry so we apply when GameWorld appears (joiner may load game scene later)
|
||||||
|
get_tree().create_timer(3.0).timeout.connect(func():
|
||||||
|
if pending_boss_spider_spawns.is_empty():
|
||||||
|
return
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw and gw.has_method("_do_client_spawn_boss_spiders"):
|
||||||
|
apply_pending_boss_spider_spawns(gw)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
game_world._do_client_spawn_boss_spiders(p1x, p1y, p2x, p2y, p3x, p3y, idx0, idx1, idx2)
|
||||||
|
|
||||||
|
func _deferred_spawn_boss_spiders(p1x: float, p1y: float, p2x: float, p2y: float, p3x: float, p3y: float, idx0: int, idx1: int, idx2: int) -> void:
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if not game_world or not game_world.has_method("_do_client_spawn_boss_spiders"):
|
||||||
|
return
|
||||||
|
game_world._do_client_spawn_boss_spiders(p1x, p1y, p2x, p2y, p3x, p3y, idx0, idx1, idx2)
|
||||||
|
|
||||||
|
func apply_pending_boss_spider_spawns(game_world: Node) -> void:
|
||||||
|
"""Called by GameWorld when it becomes ready; spawn any boss spiders that arrived before we were in tree."""
|
||||||
|
if not game_world or not game_world.has_method("_do_client_spawn_boss_spiders"):
|
||||||
|
return
|
||||||
|
while pending_boss_spider_spawns.size() > 0:
|
||||||
|
var entry = pending_boss_spider_spawns.pop_front()
|
||||||
|
game_world._do_client_spawn_boss_spiders(entry.p1x, entry.p1y, entry.p2x, entry.p2y, entry.p3x, entry.p3y, entry.idx0, entry.idx1, entry.idx2)
|
||||||
|
|
||||||
|
|
||||||
func get_webrtc_peer() -> WebRTCMultiplayerPeer:
|
func get_webrtc_peer() -> WebRTCMultiplayerPeer:
|
||||||
if network_mode == 1 and multiplayer.multiplayer_peer is WebRTCMultiplayerPeer:
|
if network_mode == 1 and multiplayer.multiplayer_peer is WebRTCMultiplayerPeer:
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ var is_knocked_back: bool = false
|
|||||||
var knockback_time: float = 0.0
|
var knockback_time: float = 0.0
|
||||||
var knockback_duration: float = 0.3 # How long knockback lasts
|
var knockback_duration: float = 0.3 # How long knockback lasts
|
||||||
|
|
||||||
|
# Web net (boss): when netted_by_web != null, player is stuck and cannot attack with main weapon
|
||||||
|
var netted_by_web: Node = null
|
||||||
|
var netted_overlay_sprite: Sprite2D = null # Frame 679 for netted visual
|
||||||
|
|
||||||
# Attack/Combat
|
# Attack/Combat
|
||||||
var can_attack: bool = true
|
var can_attack: bool = true
|
||||||
var attack_cooldown: float = 0.0 # No cooldown - instant attacks!
|
var attack_cooldown: float = 0.0 # No cooldown - instant attacks!
|
||||||
@@ -182,6 +186,7 @@ const HELD_POSITION_Z: float = 12.0 # Z height when held/lifted (above ground; i
|
|||||||
@onready var sfx_die = $SfxDie
|
@onready var sfx_die = $SfxDie
|
||||||
@onready var sfx_look_out = $SfxLookOut
|
@onready var sfx_look_out = $SfxLookOut
|
||||||
@onready var sfx_ahaa = $SfxAhaa
|
@onready var sfx_ahaa = $SfxAhaa
|
||||||
|
@onready var sfx_secret_found = $SfxSecretFound
|
||||||
|
|
||||||
# Alert indicator (exclamation mark)
|
# Alert indicator (exclamation mark)
|
||||||
var alert_indicator: Sprite2D = null
|
var alert_indicator: Sprite2D = null
|
||||||
@@ -1493,32 +1498,23 @@ func _can_place_down_at(place_pos: Vector2, placed_obj: Node) -> bool:
|
|||||||
placed_shape_transform = Transform2D(0.0, place_pos)
|
placed_shape_transform = Transform2D(0.0, place_pos)
|
||||||
|
|
||||||
# Check if the placed object's collision shape would collide with anything
|
# Check if the placed object's collision shape would collide with anything
|
||||||
# This includes: walls, other objects, and players
|
# This includes: walls, other objects, and players (not Area2D triggers - those don't block placement)
|
||||||
var params = PhysicsShapeQueryParameters2D.new()
|
var params = PhysicsShapeQueryParameters2D.new()
|
||||||
params.shape = placed_shape
|
params.shape = placed_shape
|
||||||
params.transform = placed_shape_transform
|
params.transform = placed_shape_transform
|
||||||
params.collision_mask = 1 | 2 | 64 # Players (layer 1), objects (layer 2), walls (layer 7 = bit 6 = 64)
|
params.collision_mask = 1 | 2 | 64 # Players (layer 1), objects (layer 2), walls (layer 7 = bit 6 = 64)
|
||||||
|
params.collide_with_areas = false # Only solid bodies block; ignore trigger areas (e.g. door key zones)
|
||||||
|
params.collide_with_bodies = true
|
||||||
|
|
||||||
# CRITICAL: Exclude self, the object being placed, and make sure to exclude it properly
|
# CRITICAL: Exclude using RIDs so the physics engine actually excludes them (Node refs may not work)
|
||||||
# The object might still be in the scene tree with collision disabled, so we need to exclude it
|
var exclude_list: Array[RID] = [get_rid()]
|
||||||
var exclude_list = [self]
|
if placed_obj and is_instance_valid(placed_obj) and placed_obj is CollisionObject2D:
|
||||||
if placed_obj and is_instance_valid(placed_obj):
|
exclude_list.append(placed_obj.get_rid())
|
||||||
exclude_list.append(placed_obj)
|
|
||||||
params.exclude = exclude_list
|
params.exclude = exclude_list
|
||||||
|
|
||||||
# Test the actual collision shape at the placement position
|
# Test the actual collision shape at the placement position
|
||||||
var hits = space_state.intersect_shape(params, 32) # Check up to 32 collisions
|
var hits = space_state.intersect_shape(params, 32) # Check up to 32 collisions
|
||||||
|
|
||||||
# Debug: Log what we found
|
|
||||||
if hits.size() > 0:
|
|
||||||
print("DEBUG: Placement blocked - found ", hits.size(), " collisions at ", place_pos)
|
|
||||||
for i in min(hits.size(), 3): # Log first 3 collisions
|
|
||||||
var hit = hits[i]
|
|
||||||
if hit.has("collider"):
|
|
||||||
print(" - Collision with: ", hit.collider, " (", hit.collider.name if hit.collider else "null", ")")
|
|
||||||
if hit.has("rid"):
|
|
||||||
print(" - RID: ", hit.rid)
|
|
||||||
|
|
||||||
# If any collisions found, placement is invalid
|
# If any collisions found, placement is invalid
|
||||||
return hits.size() == 0
|
return hits.size() == 0
|
||||||
|
|
||||||
@@ -1964,6 +1960,8 @@ func _physics_process(delta):
|
|||||||
const MANA_REGEN_RATE = 2.0 # mana per second
|
const MANA_REGEN_RATE = 2.0 # mana per second
|
||||||
if character_stats.mp < character_stats.maxmp:
|
if character_stats.mp < character_stats.maxmp:
|
||||||
character_stats.restore_mana(MANA_REGEN_RATE * delta)
|
character_stats.restore_mana(MANA_REGEN_RATE * delta)
|
||||||
|
# Tick down temporary buffs (e.g. dodge potion)
|
||||||
|
character_stats.tick_buffs(delta)
|
||||||
|
|
||||||
# Spell charge visuals (incantation, tint pulse) - run for ALL players so others see the pulse
|
# Spell charge visuals (incantation, tint pulse) - run for ALL players so others see the pulse
|
||||||
if is_charging_spell:
|
if is_charging_spell:
|
||||||
@@ -2496,8 +2494,8 @@ func _handle_input():
|
|||||||
if new_direction != current_direction:
|
if new_direction != current_direction:
|
||||||
current_direction = new_direction
|
current_direction = new_direction
|
||||||
_update_cone_light_rotation()
|
_update_cone_light_rotation()
|
||||||
elif is_pushing:
|
elif is_pushing or (held_object and not is_lifting):
|
||||||
# Keep locked direction when pushing
|
# Keep direction from when grab started (don't turn to face the object)
|
||||||
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()
|
||||||
@@ -2539,9 +2537,10 @@ func _handle_input():
|
|||||||
elif is_lifting:
|
elif is_lifting:
|
||||||
if current_animation != "LIFT" and current_animation != "IDLE_HOLD":
|
if current_animation != "LIFT" and current_animation != "IDLE_HOLD":
|
||||||
_set_animation("IDLE_HOLD")
|
_set_animation("IDLE_HOLD")
|
||||||
elif is_pushing:
|
elif is_pushing or (held_object and not is_lifting):
|
||||||
_set_animation("IDLE_PUSH")
|
if is_pushing:
|
||||||
# Keep locked direction when pushing
|
_set_animation("IDLE_PUSH")
|
||||||
|
# Keep direction from when grab started
|
||||||
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()
|
||||||
@@ -2598,8 +2597,8 @@ func _handle_input():
|
|||||||
if character_stats and character_stats.is_over_encumbered():
|
if character_stats and character_stats.is_over_encumbered():
|
||||||
current_speed = base_speed * 0.25
|
current_speed = base_speed * 0.25
|
||||||
|
|
||||||
# Lock movement if movement_lock_timer is active or reviving a corpse
|
# Lock movement if movement_lock_timer is active, reviving a corpse, or netted by web
|
||||||
if movement_lock_timer > 0.0 or is_reviving:
|
if movement_lock_timer > 0.0 or is_reviving or netted_by_web:
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
else:
|
else:
|
||||||
velocity = input_vector * current_speed
|
velocity = input_vector * current_speed
|
||||||
@@ -2623,6 +2622,32 @@ func _handle_walking_sfx():
|
|||||||
if sfx_walk and sfx_walk.playing:
|
if sfx_walk and sfx_walk.playing:
|
||||||
sfx_walk.stop()
|
sfx_walk.stop()
|
||||||
|
|
||||||
|
func _web_net_apply(web_node: Node) -> void:
|
||||||
|
netted_by_web = web_node
|
||||||
|
|
||||||
|
func _web_net_release(_web_node: Node) -> void:
|
||||||
|
netted_by_web = null
|
||||||
|
_web_net_show_netted_frame(false)
|
||||||
|
|
||||||
|
func _web_net_show_netted_frame(show_net: bool) -> void:
|
||||||
|
if show_net:
|
||||||
|
if netted_overlay_sprite == null:
|
||||||
|
netted_overlay_sprite = Sprite2D.new()
|
||||||
|
var tex = load("res://assets/gfx/fx/shade_spell_effects.png") as Texture2D
|
||||||
|
if tex:
|
||||||
|
netted_overlay_sprite.texture = tex
|
||||||
|
netted_overlay_sprite.hframes = 105
|
||||||
|
netted_overlay_sprite.vframes = 79
|
||||||
|
netted_overlay_sprite.frame = 679
|
||||||
|
netted_overlay_sprite.centered = true
|
||||||
|
netted_overlay_sprite.z_index = 5
|
||||||
|
add_child(netted_overlay_sprite)
|
||||||
|
if netted_overlay_sprite:
|
||||||
|
netted_overlay_sprite.visible = true
|
||||||
|
else:
|
||||||
|
if netted_overlay_sprite:
|
||||||
|
netted_overlay_sprite.visible = false
|
||||||
|
|
||||||
func _handle_interactions():
|
func _handle_interactions():
|
||||||
var grab_button_down = false
|
var grab_button_down = false
|
||||||
var grab_just_pressed = false
|
var grab_just_pressed = false
|
||||||
@@ -3192,7 +3217,7 @@ func _handle_interactions():
|
|||||||
|
|
||||||
# Handle bow charging
|
# Handle bow charging
|
||||||
if has_bow_and_arrows and not is_lifting and not is_pushing:
|
if has_bow_and_arrows and not is_lifting and not is_pushing:
|
||||||
if attack_just_pressed and can_attack and not is_charging_bow and not spawn_landing:
|
if attack_just_pressed and can_attack and not is_charging_bow and not spawn_landing and not netted_by_web:
|
||||||
if !$SfxBuckleBow.playing:
|
if !$SfxBuckleBow.playing:
|
||||||
$SfxBuckleBow.play()
|
$SfxBuckleBow.play()
|
||||||
# Start charging bow
|
# Start charging bow
|
||||||
@@ -3260,7 +3285,7 @@ func _handle_interactions():
|
|||||||
|
|
||||||
# Normal attack (non-bow or no arrows)
|
# Normal attack (non-bow or no arrows)
|
||||||
# Also allow throwing when lifting (even if bow is equipped). Block during spawn fall.
|
# Also allow throwing when lifting (even if bow is equipped). Block during spawn fall.
|
||||||
if attack_just_pressed and can_attack and not spawn_landing:
|
if attack_just_pressed and can_attack and not spawn_landing and not netted_by_web:
|
||||||
if is_lifting:
|
if is_lifting:
|
||||||
# Attack while lifting -> throw immediately in facing direction
|
# Attack while lifting -> throw immediately in facing direction
|
||||||
_force_throw_held_object(facing_direction_vector)
|
_force_throw_held_object(facing_direction_vector)
|
||||||
@@ -3382,11 +3407,12 @@ func _try_grab():
|
|||||||
# Store the distance from player to object when grabbed (for placement)
|
# Store the distance from player to object when grabbed (for placement)
|
||||||
grab_distance = global_position.distance_to(closest_body.global_position)
|
grab_distance = global_position.distance_to(closest_body.global_position)
|
||||||
|
|
||||||
# Calculate push axis from grab direction (but don't move the object yet)
|
# Use player's current facing when grab started (do not turn to face the object)
|
||||||
var grab_direction = grab_offset.normalized()
|
var grab_direction = facing_direction_vector.normalized() if facing_direction_vector.length() > 0.1 else last_movement_direction.normalized()
|
||||||
if grab_direction.length() < 0.1:
|
if grab_direction.length() < 0.1:
|
||||||
grab_direction = last_movement_direction
|
grab_direction = Vector2.DOWN
|
||||||
push_axis = _snap_to_8_directions(grab_direction)
|
push_axis = _snap_to_8_directions(grab_direction)
|
||||||
|
push_direction_locked = _get_direction_from_vector(push_axis) as Direction
|
||||||
|
|
||||||
# Disable collision with players and other objects when grabbing
|
# Disable collision with players and other objects when grabbing
|
||||||
# But keep collision with walls (layer 7) enabled for pushing
|
# But keep collision with walls (layer 7) enabled for pushing
|
||||||
@@ -3503,15 +3529,11 @@ func _start_pushing():
|
|||||||
is_pushing = true
|
is_pushing = true
|
||||||
is_lifting = false
|
is_lifting = false
|
||||||
|
|
||||||
# Lock to the direction we're facing when we start pushing
|
# Keep the direction we had when we started the grab (do not face the object)
|
||||||
var initial_direction = grab_offset.normalized()
|
var initial_direction = facing_direction_vector.normalized() if facing_direction_vector.length() > 0.1 else last_movement_direction.normalized()
|
||||||
if initial_direction.length() < 0.1:
|
if initial_direction.length() < 0.1:
|
||||||
initial_direction = last_movement_direction.normalized()
|
initial_direction = Vector2.DOWN
|
||||||
|
|
||||||
# Snap to one of 8 directions
|
|
||||||
push_axis = _snap_to_8_directions(initial_direction)
|
push_axis = _snap_to_8_directions(initial_direction)
|
||||||
|
|
||||||
# Lock the facing direction (for both animation and attacks)
|
|
||||||
push_direction_locked = _get_direction_from_vector(push_axis)
|
push_direction_locked = _get_direction_from_vector(push_axis)
|
||||||
facing_direction_vector = push_axis.normalized()
|
facing_direction_vector = push_axis.normalized()
|
||||||
|
|
||||||
@@ -4045,7 +4067,7 @@ func _place_down_object():
|
|||||||
print("Placed down ", placed_obj.name, " at ", place_pos)
|
print("Placed down ", placed_obj.name, " at ", place_pos)
|
||||||
|
|
||||||
func _perform_attack():
|
func _perform_attack():
|
||||||
if not can_attack or is_attacking or spawn_landing:
|
if not can_attack or is_attacking or spawn_landing or netted_by_web:
|
||||||
return
|
return
|
||||||
|
|
||||||
can_attack = false
|
can_attack = false
|
||||||
@@ -6387,6 +6409,13 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
|
|||||||
# Invulnerable during fallout sink (can't take damage from anything while falling)
|
# Invulnerable during fallout sink (can't take damage from anything while falling)
|
||||||
if fallout_state:
|
if fallout_state:
|
||||||
return
|
return
|
||||||
|
# Taking damage while webbed immediately frees you from the web
|
||||||
|
if netted_by_web:
|
||||||
|
var web = netted_by_web
|
||||||
|
netted_by_web = null
|
||||||
|
_web_net_show_netted_frame(false)
|
||||||
|
if web and is_instance_valid(web) and web.has_method("cut_by_attack"):
|
||||||
|
web.cut_by_attack(null)
|
||||||
|
|
||||||
# Cancel bow charging when taking damage
|
# Cancel bow charging when taking damage
|
||||||
if is_charging_bow:
|
if is_charging_bow:
|
||||||
@@ -6552,10 +6581,9 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
|
|||||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
_rpc_to_ready_peers("_sync_damage", [actual_damage, attacker_position])
|
_rpc_to_ready_peers("_sync_damage", [actual_damage, attacker_position])
|
||||||
|
|
||||||
# Check if dead - but wait for damage animation to play first
|
# Check if dead - below 1 HP must always trigger death (trap, etc.)
|
||||||
# Use small epsilon to handle floating point precision issues (HP might be 0.0000001 instead of exactly 0.0)
|
|
||||||
var health = character_stats.hp if character_stats else current_health
|
var health = character_stats.hp if character_stats else current_health
|
||||||
if health <= 0.001: # Use epsilon to catch values very close to 0
|
if health < 1.0:
|
||||||
if character_stats:
|
if character_stats:
|
||||||
character_stats.hp = 0.0 # Clamp to exactly 0
|
character_stats.hp = 0.0 # Clamp to exactly 0
|
||||||
else:
|
else:
|
||||||
@@ -6563,7 +6591,8 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
|
|||||||
is_dead = true # Set flag immediately to prevent more damage
|
is_dead = true # Set flag immediately to prevent more damage
|
||||||
# Wait a bit for damage animation and knockback to show
|
# Wait a bit for damage animation and knockback to show
|
||||||
await get_tree().create_timer(0.3).timeout
|
await get_tree().create_timer(0.3).timeout
|
||||||
_die()
|
if is_instance_valid(self) and is_dead:
|
||||||
|
_die()
|
||||||
|
|
||||||
func _die():
|
func _die():
|
||||||
# Already processing death - prevent multiple concurrent death sequences
|
# Already processing death - prevent multiple concurrent death sequences
|
||||||
@@ -7575,6 +7604,10 @@ func _on_level_up_stats(stats_increased: Array):
|
|||||||
if not character_stats:
|
if not character_stats:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Play level-up fanfare locally only when this player (you) gained the level
|
||||||
|
if is_multiplayer_authority() and has_node("SfxLevelUp"):
|
||||||
|
$SfxLevelUp.play()
|
||||||
|
|
||||||
# Stat name to display name mapping
|
# Stat name to display name mapping
|
||||||
var stat_display_names = {
|
var stat_display_names = {
|
||||||
"str": "STR",
|
"str": "STR",
|
||||||
@@ -7651,6 +7684,17 @@ func rpc_apply_corpse_knockback(dir_x: float, dir_y: float, force: float):
|
|||||||
is_knocked_back = true
|
is_knocked_back = true
|
||||||
knockback_time = 0.0
|
knockback_time = 0.0
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _on_attack_blocked_by_enemy(blocker_position: Vector2):
|
||||||
|
# Called when this player's attack was blocked by an enemy (e.g. humanoid with shield). Show BLOCKED and small knockback.
|
||||||
|
var dir_away = (global_position - blocker_position).normalized()
|
||||||
|
if dir_away.length() < 0.01:
|
||||||
|
dir_away = Vector2.RIGHT
|
||||||
|
velocity = dir_away * 75.0
|
||||||
|
is_knocked_back = true
|
||||||
|
knockback_time = 0.0
|
||||||
|
_show_damage_number(0.0, blocker_position, false, false, false, true)
|
||||||
|
|
||||||
@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, is_blocked: 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
|
||||||
@@ -7796,6 +7840,26 @@ func _sync_trap_detected_alert():
|
|||||||
if sfx_look_out:
|
if sfx_look_out:
|
||||||
sfx_look_out.play()
|
sfx_look_out.play()
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _on_cracked_floor_detected():
|
||||||
|
# Called when this player detects a cracked floor (perception roll success). Only the detecting player plays SfxLookOut and sees alert.
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
return
|
||||||
|
_show_alert_indicator()
|
||||||
|
if sfx_look_out:
|
||||||
|
sfx_look_out.play()
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _on_secret_chest_detected():
|
||||||
|
# Called when this player detects a hidden chest (perception roll success). Only the detecting player plays SfxAhaa + SfxSecretFound and sees alert.
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
return
|
||||||
|
_show_alert_indicator()
|
||||||
|
if sfx_ahaa:
|
||||||
|
sfx_ahaa.play()
|
||||||
|
if sfx_secret_found:
|
||||||
|
sfx_secret_found.play()
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _sync_exit_found_alert():
|
func _sync_exit_found_alert():
|
||||||
# Sync exit found alert to all clients
|
# Sync exit found alert to all clients
|
||||||
|
|||||||
@@ -15,15 +15,19 @@ var elapsed_time: float = 0.0
|
|||||||
var distance_traveled: float = 0.0
|
var distance_traveled: float = 0.0
|
||||||
var player_owner: Node = null
|
var player_owner: Node = null
|
||||||
var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup)
|
var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup)
|
||||||
|
var _cut_web_rpc_sent: bool = false
|
||||||
|
|
||||||
@onready var sprite = $Sprite2D
|
@onready var sprite = $Sprite2D
|
||||||
@onready var hit_area = $Area2D
|
@onready var hit_area = $Area2D
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
$SfxSwosh.play()
|
$SfxSwosh.play()
|
||||||
# Connect area signals (only if not already connected)
|
if hit_area:
|
||||||
if hit_area and not hit_area.body_entered.is_connected(_on_body_entered):
|
if not hit_area.body_entered.is_connected(_on_body_entered):
|
||||||
hit_area.body_entered.connect(_on_body_entered)
|
hit_area.body_entered.connect(_on_body_entered)
|
||||||
|
if not hit_area.area_entered.is_connected(_on_area_entered):
|
||||||
|
hit_area.area_entered.connect(_on_area_entered)
|
||||||
|
hit_area.collision_mask = hit_area.collision_mask | 8
|
||||||
|
|
||||||
func setup(direction: Vector2, owner_player: Node, damage_value: float = 20.0):
|
func setup(direction: Vector2, owner_player: Node, damage_value: float = 20.0):
|
||||||
travel_direction = direction.normalized()
|
travel_direction = direction.normalized()
|
||||||
@@ -52,6 +56,13 @@ func _physics_process(delta):
|
|||||||
current_speed -= deceleration * delta
|
current_speed -= deceleration * delta
|
||||||
current_speed = max(0.0, current_speed) # Don't go negative
|
current_speed = max(0.0, current_speed) # Don't go negative
|
||||||
|
|
||||||
|
# So other player can free netted teammate: notify server to cut web
|
||||||
|
if not _cut_web_rpc_sent and multiplayer.has_multiplayer_peer() and not multiplayer.is_server() and player_owner and is_instance_valid(player_owner) and elapsed_time >= 0.04:
|
||||||
|
_cut_web_rpc_sent = true
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw and gw.has_method("request_cut_web"):
|
||||||
|
gw.request_cut_web.rpc_id(1, global_position.x, global_position.y, 30.0, player_owner.get_multiplayer_authority())
|
||||||
|
|
||||||
# Move in travel direction
|
# Move in travel direction
|
||||||
var movement = travel_direction * current_speed * delta
|
var movement = travel_direction * current_speed * delta
|
||||||
global_position += movement
|
global_position += movement
|
||||||
@@ -62,6 +73,11 @@ func _physics_process(delta):
|
|||||||
if sprite:
|
if sprite:
|
||||||
sprite.modulate.a = alpha
|
sprite.modulate.a = alpha
|
||||||
|
|
||||||
|
func _on_area_entered(area: Area2D) -> void:
|
||||||
|
if area.has_method("cut_by_attack") and area.get("state") == "hit_player":
|
||||||
|
if player_owner and is_instance_valid(player_owner) and player_owner != area.get("hit_player"):
|
||||||
|
area.cut_by_attack(player_owner)
|
||||||
|
|
||||||
func _on_body_entered(body):
|
func _on_body_entered(body):
|
||||||
# Don't hit the owner
|
# Don't hit the owner
|
||||||
if body == player_owner:
|
if body == player_owner:
|
||||||
@@ -134,6 +150,9 @@ func _on_body_entered(body):
|
|||||||
else:
|
else:
|
||||||
# Client sends RPC to server
|
# Client sends RPC to server
|
||||||
game_world._request_enemy_damage.rpc_id(1, enemy_name, enemy_index, damage, attacker_pos, is_crit)
|
game_world._request_enemy_damage.rpc_id(1, enemy_name, enemy_index, damage, attacker_pos, is_crit)
|
||||||
|
# Client: show damage number locally (boss/enemy won't show it on our view otherwise)
|
||||||
|
if game_world and game_world.has_method("show_damage_number_at_position") and not multiplayer.is_server():
|
||||||
|
game_world.show_damage_number_at_position(body.global_position, damage, is_crit)
|
||||||
else:
|
else:
|
||||||
# Fallback: try direct call (may fail if node path doesn't match)
|
# Fallback: try direct call (may fail if node path doesn't match)
|
||||||
var enemy_peer_id = body.get_multiplayer_authority()
|
var enemy_peer_id = body.get_multiplayer_authority()
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
extends Node2D
|
extends Node2D
|
||||||
|
|
||||||
@export var is_enabled = true # set to disabled for keydoors!
|
@export var is_enabled = true: # set to disabled for keydoors!
|
||||||
|
set(value):
|
||||||
|
is_enabled = value
|
||||||
|
if not value and is_node_ready():
|
||||||
|
$GPUParticles2D.emitting = false
|
||||||
|
|
||||||
# Called when the node enters the scene tree for the first time.
|
# Called when the node enters the scene tree for the first time.
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
pass # Replace with function body.
|
# If we were disabled before _ready (e.g. by door), stop particles once node is ready
|
||||||
|
if not is_enabled:
|
||||||
|
$GPUParticles2D.emitting = false
|
||||||
|
|
||||||
|
|
||||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ extends Node2D
|
|||||||
@onready var detection_area = $DetectionArea
|
@onready var detection_area = $DetectionArea
|
||||||
|
|
||||||
# Trap state
|
# Trap state
|
||||||
var is_detected: bool = false # Becomes true when any player detects it
|
var is_detected: bool = false # Becomes true when any player detects it
|
||||||
var is_disarmed: bool = false # True if trap has been disarmed
|
var is_disarmed: bool = false # True if trap has been disarmed
|
||||||
var is_active: bool = false # True when trap is currently triggering
|
var is_active: bool = false # True when trap is currently triggering
|
||||||
var has_cooldown: bool = false # Some traps can reset
|
var has_cooldown: bool = false # Some traps can reset
|
||||||
var cooldown_time: float = 5.0 # Time until trap can re-activate
|
var cooldown_time: float = 5.0 # Time until trap can re-activate
|
||||||
var cooldown_timer: float = 0.0
|
var cooldown_timer: float = 0.0
|
||||||
|
|
||||||
# Trap properties
|
# Trap properties
|
||||||
@@ -44,7 +44,7 @@ func _ready() -> void:
|
|||||||
if index == 0:
|
if index == 0:
|
||||||
sprite.texture = load("res://assets/gfx/traps/Floor_Lance.png")
|
sprite.texture = load("res://assets/gfx/traps/Floor_Lance.png")
|
||||||
trap_type = "Floor_Lance"
|
trap_type = "Floor_Lance"
|
||||||
has_cooldown = true # Lance traps can reset
|
has_cooldown = true # Lance traps can reset
|
||||||
|
|
||||||
# Start hidden (invisible until detected)
|
# Start hidden (invisible until detected)
|
||||||
sprite.modulate.a = 0.0
|
sprite.modulate.a = 0.0
|
||||||
@@ -91,14 +91,14 @@ func _on_detection_area_body_entered(body: Node) -> void:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if is_detected or is_disarmed:
|
if is_detected or is_disarmed:
|
||||||
return # Already detected or disarmed
|
return # Already detected or disarmed
|
||||||
|
|
||||||
# Get player peer ID
|
# Get player peer ID
|
||||||
var peer_id = body.get_multiplayer_authority()
|
var peer_id = body.get_multiplayer_authority()
|
||||||
|
|
||||||
# Check if this player has already attempted detection
|
# Check if this player has already attempted detection
|
||||||
if player_detection_attempts.has(peer_id):
|
if player_detection_attempts.has(peer_id):
|
||||||
return # Already tried once this game
|
return # Already tried once this game
|
||||||
|
|
||||||
# Mark that this player has attempted detection
|
# Mark that this player has attempted detection
|
||||||
player_detection_attempts[peer_id] = true
|
player_detection_attempts[peer_id] = true
|
||||||
@@ -108,7 +108,7 @@ func _on_detection_area_body_entered(body: Node) -> void:
|
|||||||
_roll_perception_check(body)
|
_roll_perception_check(body)
|
||||||
|
|
||||||
func _on_detection_area_body_exited(_body: Node) -> void:
|
func _on_detection_area_body_exited(_body: Node) -> void:
|
||||||
pass # Detection is permanent once attempted
|
pass # Detection is permanent once attempted
|
||||||
|
|
||||||
func _roll_perception_check(player: Node) -> void:
|
func _roll_perception_check(player: Node) -> void:
|
||||||
# Roll perception check for player
|
# Roll perception check for player
|
||||||
@@ -119,8 +119,8 @@ func _roll_perception_check(player: Node) -> void:
|
|||||||
|
|
||||||
# Perception roll: d20 + PER modifier
|
# Perception roll: d20 + PER modifier
|
||||||
# Target DC: 15 (medium difficulty)
|
# Target DC: 15 (medium difficulty)
|
||||||
var roll = randi() % 20 + 1 # 1d20
|
var roll = randi() % 20 + 1 # 1d20
|
||||||
var total = roll + int(per_stat / 2) - 5 # PER modifier: (PER - 10) / 2
|
var total = roll + int(per_stat / 2) - 5 # PER modifier: (PER - 10) / 2
|
||||||
var dc = 15
|
var dc = 15
|
||||||
|
|
||||||
print(player.name, " rolls Perception: ", roll, " + ", int(per_stat / 2) - 5, " = ", total, " vs DC ", dc)
|
print(player.name, " rolls Perception: ", roll, " + ", int(per_stat / 2) - 5, " = ", total, " vs DC ", dc)
|
||||||
@@ -143,15 +143,20 @@ func _detect_trap(detecting_player: Node) -> void:
|
|||||||
if detecting_player and is_instance_valid(detecting_player) and detecting_player.has_method("_on_trap_detected"):
|
if detecting_player and is_instance_valid(detecting_player) and detecting_player.has_method("_on_trap_detected"):
|
||||||
detecting_player._on_trap_detected()
|
detecting_player._on_trap_detected()
|
||||||
|
|
||||||
|
# Spawn detected effect at trap position (synced so all players see it; parented to trap so it can be removed when disarmed)
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("spawn_detected_effect_at"):
|
||||||
|
game_world.spawn_detected_effect_at(global_position, name, "trap")
|
||||||
|
|
||||||
# Sync detection to all clients (including server with call_local)
|
# Sync detection to all clients (including server with call_local)
|
||||||
# CRITICAL: Validate trap is still valid before sending RPC
|
# CRITICAL: Validate trap is still valid before sending RPC
|
||||||
# Use GameWorld RPC to avoid node path issues
|
# Use GameWorld RPC to avoid node path issues
|
||||||
if multiplayer.has_multiplayer_peer() and is_inside_tree() and is_instance_valid(self):
|
if multiplayer.has_multiplayer_peer() and is_inside_tree() and is_instance_valid(self):
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
# Use GameWorld RPC with trap name instead of path
|
# Use GameWorld RPC with trap name instead of path
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
if game_world and game_world.has_method("_sync_trap_state_by_name"):
|
if game_world and game_world.has_method("_sync_trap_state_by_name"):
|
||||||
game_world._sync_trap_state_by_name.rpc(name, true, false) # detected=true, disarmed=false
|
game_world._sync_trap_state_by_name.rpc(name, true, false) # detected=true, disarmed=false
|
||||||
|
|
||||||
print(detecting_player.name, " detected trap at ", global_position)
|
print(detecting_player.name, " detected trap at ", global_position)
|
||||||
|
|
||||||
@@ -200,7 +205,7 @@ func _on_disarm_area_body_exited(body: Node) -> void:
|
|||||||
func _show_disarm_text(_player: Node) -> void:
|
func _show_disarm_text(_player: Node) -> void:
|
||||||
# Create "DISARM" label above trap
|
# Create "DISARM" label above trap
|
||||||
if disarm_label:
|
if disarm_label:
|
||||||
return # Already showing
|
return # Already showing
|
||||||
|
|
||||||
disarm_label = Label.new()
|
disarm_label = Label.new()
|
||||||
disarm_label.text = "DISARM"
|
disarm_label.text = "DISARM"
|
||||||
@@ -212,6 +217,13 @@ func _show_disarm_text(_player: Node) -> void:
|
|||||||
disarm_label.z_index = 100
|
disarm_label.z_index = 100
|
||||||
add_child(disarm_label)
|
add_child(disarm_label)
|
||||||
|
|
||||||
|
func _remove_detected_effect() -> void:
|
||||||
|
# Remove the blue "detected" effect when trap is disarmed (effect is parented to this trap)
|
||||||
|
for c in get_children():
|
||||||
|
if c.name == "DetectedEffect":
|
||||||
|
c.queue_free()
|
||||||
|
return
|
||||||
|
|
||||||
func _hide_disarm_text(_player: Node) -> void:
|
func _hide_disarm_text(_player: Node) -> void:
|
||||||
if disarm_label:
|
if disarm_label:
|
||||||
disarm_label.queue_free()
|
disarm_label.queue_free()
|
||||||
@@ -246,6 +258,12 @@ func _complete_disarm() -> void:
|
|||||||
if $SfxDisarming.playing:
|
if $SfxDisarming.playing:
|
||||||
$SfxDisarming.stop()
|
$SfxDisarming.stop()
|
||||||
|
|
||||||
|
if has_node("SfxDisarmSuccess"):
|
||||||
|
$SfxDisarmSuccess.play()
|
||||||
|
|
||||||
|
# Remove detected effect (blue glow) since trap is no longer dangerous
|
||||||
|
_remove_detected_effect()
|
||||||
|
|
||||||
# Hide disarm text
|
# Hide disarm text
|
||||||
_hide_disarm_text(null)
|
_hide_disarm_text(null)
|
||||||
|
|
||||||
@@ -267,7 +285,7 @@ func _complete_disarm() -> void:
|
|||||||
# Grant EXP to all players for disarming trap (only on server)
|
# Grant EXP to all players for disarming trap (only on server)
|
||||||
# CRITICAL: Only server should grant EXP to avoid duplicates
|
# CRITICAL: Only server should grant EXP to avoid duplicates
|
||||||
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||||
var trap_exp_reward = 8.0 # EXP reward for disarming a trap
|
var trap_exp_reward = 8.0 # EXP reward for disarming a trap
|
||||||
var all_players = get_tree().get_nodes_in_group("player")
|
var all_players = get_tree().get_nodes_in_group("player")
|
||||||
var valid_players = []
|
var valid_players = []
|
||||||
for player in all_players:
|
for player in all_players:
|
||||||
@@ -303,7 +321,7 @@ func _complete_disarm() -> void:
|
|||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
# Host disarmed: broadcast to clients
|
# Host disarmed: broadcast to clients
|
||||||
if game_world.has_method("_sync_trap_state_by_name"):
|
if game_world.has_method("_sync_trap_state_by_name"):
|
||||||
game_world._sync_trap_state_by_name.rpc(name, true, true) # detected=true, disarmed=true
|
game_world._sync_trap_state_by_name.rpc(name, true, true) # detected=true, disarmed=true
|
||||||
else:
|
else:
|
||||||
# Joiner disarmed: request host to apply locally and broadcast to all
|
# Joiner disarmed: request host to apply locally and broadcast to all
|
||||||
if game_world.has_method("_request_trap_disarm"):
|
if game_world.has_method("_request_trap_disarm"):
|
||||||
@@ -318,6 +336,7 @@ func _sync_trap_disarmed() -> void:
|
|||||||
if not is_instance_valid(self) or not is_inside_tree():
|
if not is_instance_valid(self) or not is_inside_tree():
|
||||||
return
|
return
|
||||||
is_disarmed = true
|
is_disarmed = true
|
||||||
|
_remove_detected_effect()
|
||||||
if sprite:
|
if sprite:
|
||||||
sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
|
sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
|
||||||
if activation_area:
|
if activation_area:
|
||||||
@@ -335,7 +354,7 @@ func _show_exp_number(amount: float, exp_pos: Vector2):
|
|||||||
|
|
||||||
# Set text and color for EXP (green)
|
# Set text and color for EXP (green)
|
||||||
exp_label.label = "+" + str(int(amount)) + " EXP"
|
exp_label.label = "+" + str(int(amount)) + " EXP"
|
||||||
exp_label.color = Color(0.4, 1.0, 0.4) # Bright green
|
exp_label.color = Color(0.4, 1.0, 0.4) # Bright green
|
||||||
exp_label.z_index = 5
|
exp_label.z_index = 5
|
||||||
|
|
||||||
# Direction is straight up
|
# Direction is straight up
|
||||||
@@ -347,7 +366,7 @@ func _show_exp_number(amount: float, exp_pos: Vector2):
|
|||||||
var entities_node = game_world.get_node_or_null("Entities")
|
var entities_node = game_world.get_node_or_null("Entities")
|
||||||
if entities_node:
|
if entities_node:
|
||||||
entities_node.add_child(exp_label)
|
entities_node.add_child(exp_label)
|
||||||
exp_label.global_position = exp_pos + Vector2(0, -20) # Slightly above
|
exp_label.global_position = exp_pos + Vector2(0, -20) # Slightly above
|
||||||
else:
|
else:
|
||||||
get_tree().current_scene.add_child(exp_label)
|
get_tree().current_scene.add_child(exp_label)
|
||||||
exp_label.global_position = exp_pos + Vector2(0, -20)
|
exp_label.global_position = exp_pos + Vector2(0, -20)
|
||||||
@@ -371,10 +390,10 @@ func _on_activation_area_body_shape_entered(_body_rid: RID, body: Node2D, _body_
|
|||||||
return
|
return
|
||||||
|
|
||||||
if is_disarmed or is_active:
|
if is_disarmed or is_active:
|
||||||
return # Can't trigger if disarmed or already active
|
return # Can't trigger if disarmed or already active
|
||||||
|
|
||||||
if has_cooldown and cooldown_timer > 0:
|
if has_cooldown and cooldown_timer > 0:
|
||||||
return # Still on cooldown
|
return # Still on cooldown
|
||||||
|
|
||||||
# Trigger trap
|
# Trigger trap
|
||||||
is_active = true
|
is_active = true
|
||||||
@@ -392,7 +411,7 @@ func _on_activation_area_body_shape_entered(_body_rid: RID, body: Node2D, _body_
|
|||||||
# Use GameWorld RPC with trap name instead of path
|
# Use GameWorld RPC with trap name instead of path
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
if game_world and game_world.has_method("_sync_trap_state_by_name"):
|
if game_world and game_world.has_method("_sync_trap_state_by_name"):
|
||||||
game_world._sync_trap_state_by_name.rpc(name, true, false) # detected=true, disarmed=false
|
game_world._sync_trap_state_by_name.rpc(name, true, false) # detected=true, disarmed=false
|
||||||
|
|
||||||
# Deal damage to player (with luck-based avoidance)
|
# Deal damage to player (with luck-based avoidance)
|
||||||
_deal_trap_damage(body)
|
_deal_trap_damage(body)
|
||||||
@@ -414,7 +433,7 @@ func _deal_trap_damage(player: Node) -> void:
|
|||||||
|
|
||||||
# Luck-based avoidance check
|
# Luck-based avoidance check
|
||||||
var luck_stat = player.character_stats.baseStats.lck + player.character_stats.get_pass("lck")
|
var luck_stat = player.character_stats.baseStats.lck + player.character_stats.get_pass("lck")
|
||||||
var avoid_chance = luck_stat * 0.02 # 2% per luck point (e.g., 10 luck = 20% avoid)
|
var avoid_chance = luck_stat * 0.02 # 2% per luck point (e.g., 10 luck = 20% avoid)
|
||||||
var avoid_roll = randf()
|
var avoid_roll = randf()
|
||||||
|
|
||||||
if avoid_roll < avoid_chance:
|
if avoid_roll < avoid_chance:
|
||||||
@@ -424,10 +443,16 @@ func _deal_trap_damage(player: Node) -> void:
|
|||||||
_show_floating_text("AVOIDED", Color.GREEN)
|
_show_floating_text("AVOIDED", Color.GREEN)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Apply trap damage (affected by player's defense)
|
# Apply trap damage (affected by player's defense) - must run on player's authority so take_damage and death run correctly
|
||||||
var final_damage = player.character_stats.calculate_damage(trap_damage, false, false)
|
var final_damage = player.character_stats.calculate_damage(trap_damage, false, false)
|
||||||
|
|
||||||
if player.has_method("rpc_take_damage"):
|
if player.has_method("rpc_take_damage"):
|
||||||
player.rpc_take_damage(trap_damage, global_position)
|
var pid = player.get_multiplayer_authority()
|
||||||
|
if multiplayer.get_unique_id() == pid:
|
||||||
|
player.take_damage(trap_damage, global_position)
|
||||||
|
elif multiplayer.has_multiplayer_peer() and pid != 0:
|
||||||
|
player.rpc_take_damage.rpc_id(pid, trap_damage, global_position)
|
||||||
|
else:
|
||||||
|
player.rpc_take_damage(trap_damage, global_position)
|
||||||
|
|
||||||
print(player.name, " took ", final_damage, " trap damage")
|
print(player.name, " took ", final_damage, " trap damage")
|
||||||
|
|||||||