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="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="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"]
|
||||
|
||||
[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)
|
||||
[ext_resource type="PackedScene" uid="uid://kmlhn1iaceg8" path="res://scenes/boss_spider_bat.tscn" id="6_w00nx"]
|
||||
|
||||
[node name="BossRoomTest" type="Node2D" unique_id=1788886249]
|
||||
script = ExtResource("1_qh2jl")
|
||||
@@ -51,7 +40,7 @@ position = Vector2(323, 352)
|
||||
rotation = 3.1415927
|
||||
|
||||
[node name="TorchWall4" parent="Environment" unique_id=600381662 instance=ExtResource("2_qh2jl")]
|
||||
position = Vector2(421, 351)
|
||||
position = Vector2(396, 351)
|
||||
rotation = 3.1415927
|
||||
|
||||
[node name="Entities" type="Node2D" parent="." unique_id=1341597062]
|
||||
@@ -126,11 +115,5 @@ light_mask = 1048575
|
||||
visibility_layer = 1048575
|
||||
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)
|
||||
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"]
|
||||
radius = 58.30952
|
||||
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_detect"]
|
||||
radius = 50.0
|
||||
|
||||
[node name="EnemyHand" type="CharacterBody2D" unique_id=512887809]
|
||||
collision_mask = 64
|
||||
script = ExtResource("1_hqcsv")
|
||||
@@ -103,6 +106,13 @@ libraries/ = SubResource("AnimationLibrary_ptw5w")
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=448410993]
|
||||
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]
|
||||
collision_layer = 0
|
||||
|
||||
@@ -122,6 +132,7 @@ collision_layer = 0
|
||||
shape = SubResource("CircleShape2D_ptw5w")
|
||||
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="GrabPlayerArea" to="." method="_on_grab_player_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]
|
||||
visible = false
|
||||
material = SubResource("ShaderMaterial_shield")
|
||||
position = Vector2(0, -4)
|
||||
texture = ExtResource("14_shield")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
@@ -347,6 +348,7 @@ vframes = 8
|
||||
[node name="Sprite2DShieldHolding" type="Sprite2D" parent="." unique_id=1318098286]
|
||||
visible = false
|
||||
material = SubResource("ShaderMaterial_shield")
|
||||
position = Vector2(0, -4)
|
||||
texture = ExtResource("15_shieldh")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
[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="AudioStream" uid="uid://csrrj2vrkbsqj" path="res://assets/audio/sfx/z3/LA_Enemy_Jump.wav" id="3_y5ckx"]
|
||||
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_slime"]
|
||||
radius = 6.0
|
||||
@@ -26,3 +27,8 @@ hframes = 15
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=521937817]
|
||||
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
|
||||
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]
|
||||
anchors_preset = 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://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="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"]
|
||||
radius = 4.0
|
||||
height = 12.0
|
||||
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_detect"]
|
||||
radius = 99.0
|
||||
|
||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_1u1k0"]
|
||||
playback_mode = 1
|
||||
streams_count = 7
|
||||
@@ -181,6 +186,13 @@ position = Vector2(0, 2)
|
||||
rotation = -1.5707964
|
||||
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]
|
||||
stream = SubResource("AudioStreamRandomizer_1u1k0")
|
||||
max_distance = 1187.0
|
||||
@@ -206,6 +218,18 @@ bus = &"Sfx"
|
||||
[node name="SfxOpenChest" type="AudioStreamPlayer2D" parent="." unique_id=743332693]
|
||||
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]
|
||||
stream = SubResource("AudioStreamRandomizer_2p257")
|
||||
volume_db = -2.611
|
||||
@@ -234,3 +258,4 @@ process_material = SubResource("ParticleProcessMaterial_ejwle")
|
||||
wait_time = 0.07
|
||||
|
||||
[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://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" path="res://assets/audio/sfx/z3/LA_Enemy_Fall.wav" id="10_fall"]
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_1"]
|
||||
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]
|
||||
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]
|
||||
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://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://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"]
|
||||
colors = PackedColorArray(0, 0, 0, 1, 1, 0.13732082, 0.092538536, 1)
|
||||
@@ -474,6 +476,21 @@ tracks/0/keys = {
|
||||
"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"]
|
||||
_data = {
|
||||
&"RESET": SubResource("Animation_t4otl"),
|
||||
@@ -484,7 +501,8 @@ _data = {
|
||||
&"frost_ready": SubResource("Animation_frost_rdy"),
|
||||
&"healing_charging": SubResource("Animation_heal_ch"),
|
||||
&"healing_ready": SubResource("Animation_heal_rdy"),
|
||||
&"idle": SubResource("Animation_hax0n")
|
||||
&"idle": SubResource("Animation_hax0n"),
|
||||
&"netted": SubResource("Animation_sc3ue")
|
||||
}
|
||||
|
||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_u2ulf"]
|
||||
@@ -870,26 +888,34 @@ bus = &"Sfx"
|
||||
|
||||
[node name="SfxBirdSound" type="AudioStreamPlayer2D" parent="." unique_id=1946085725]
|
||||
stream = ExtResource("47_mx1m4")
|
||||
volume_db = -13.255
|
||||
volume_db = -9.289
|
||||
attenuation = 3.2490087
|
||||
panning_strength = 1.12
|
||||
bus = &"Sfx"
|
||||
|
||||
[node name="SfxLookOut" type="AudioStreamPlayer2D" parent="." unique_id=1177750193]
|
||||
stream = ExtResource("48_6e8lb")
|
||||
volume_db = 0.881
|
||||
volume_db = 8.813
|
||||
max_distance = 1138.0
|
||||
attenuation = 7.999997
|
||||
attenuation = 5.856343
|
||||
panning_strength = 1.04
|
||||
bus = &"Sfx"
|
||||
|
||||
[node name="SfxAhaa" type="AudioStreamPlayer2D" parent="." unique_id=1556952538]
|
||||
stream = SubResource("AudioStreamRandomizer_lxlsd")
|
||||
volume_db = 8.053
|
||||
max_distance = 1496.0
|
||||
attenuation = 6.062864
|
||||
panning_strength = 1.13
|
||||
bus = &"Sfx"
|
||||
|
||||
[node name="SfxDeny2" type="AudioStreamPlayer2D" parent="." unique_id=1127340261]
|
||||
stream = ExtResource("45_g5jhy")
|
||||
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="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://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"]
|
||||
length = 0.001
|
||||
@@ -84,6 +89,14 @@ radius = 17.117243
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_62q8x"]
|
||||
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]
|
||||
script = ExtResource("1_62q8x")
|
||||
|
||||
@@ -128,6 +141,17 @@ attenuation = 1.9318731
|
||||
bus = &"Sfx"
|
||||
|
||||
[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"
|
||||
|
||||
[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 player_owner: Node = null
|
||||
var hit_targets: Dictionary = {}
|
||||
var _cut_web_rpc_sent: bool = false
|
||||
|
||||
var damage_effect_axe_scene: PackedScene = preload("res://scenes/damage_effect_axe.tscn")
|
||||
|
||||
@@ -31,6 +32,9 @@ func _ready() -> void:
|
||||
if hit_area:
|
||||
if not hit_area.body_entered.is_connected(_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"):
|
||||
$AttackSwosh.play()
|
||||
|
||||
@@ -60,9 +64,20 @@ func _process(delta: float) -> void:
|
||||
elapsed_time += delta
|
||||
if player_owner and is_instance_valid(player_owner):
|
||||
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:
|
||||
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:
|
||||
if body == player_owner:
|
||||
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)
|
||||
else:
|
||||
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:
|
||||
var eid = body.get_multiplayer_authority()
|
||||
if eid != 0:
|
||||
|
||||
@@ -14,6 +14,7 @@ var punch_direction: Vector2 = Vector2.RIGHT
|
||||
var player_owner: Node = null
|
||||
var hit_targets: Dictionary = {}
|
||||
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")
|
||||
|
||||
@@ -23,8 +24,12 @@ var damage_effect_punch_scene: PackedScene = preload("res://scenes/damage_effect
|
||||
func _ready() -> void:
|
||||
if has_node("SfxSwosh"):
|
||||
$SfxSwosh.play()
|
||||
if hit_area and not hit_area.body_entered.is_connected(_on_body_entered):
|
||||
hit_area.body_entered.connect(_on_body_entered)
|
||||
if hit_area:
|
||||
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:
|
||||
sprite.frame = PUNCH_FRAMES[0]
|
||||
|
||||
@@ -46,10 +51,21 @@ func _process(delta: float) -> void:
|
||||
if sprite and PUNCH_FRAMES.size() > 0:
|
||||
var frame_idx = min(int(elapsed / FRAME_DURATION), PUNCH_FRAMES.size() - 1)
|
||||
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:
|
||||
hit_area.set_deferred("monitoring", false)
|
||||
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:
|
||||
if body == player_owner:
|
||||
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)
|
||||
else:
|
||||
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:
|
||||
var enemy_peer_id = body.get_multiplayer_authority()
|
||||
if enemy_peer_id != 0:
|
||||
@@ -92,7 +111,10 @@ func _on_body_entered(body: Node2D) -> void:
|
||||
return
|
||||
|
||||
# Interactables with health (boxes, etc.) - small damage
|
||||
# Hidden chests must not be hit until detected (no damage, no effect)
|
||||
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"):
|
||||
$SfxImpact.play()
|
||||
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 mana_changed(new_mana: float, max_mana: float)
|
||||
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 no_health
|
||||
|
||||
@@ -13,7 +13,7 @@ signal character_changed(char: CharacterStats)
|
||||
signal signal_drop_item(item: Item)
|
||||
|
||||
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 character_name: String = ""
|
||||
@export var xp: float = 0
|
||||
@@ -21,17 +21,17 @@ var race: String = "Human" # "Dwarf", "Elf", or "Human"
|
||||
@export var mp: float = 20.0
|
||||
|
||||
# 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:
|
||||
var facial_hair:String = ""
|
||||
var facial_hair_color:Color = Color.WHITE
|
||||
var hairstyle:String = ""
|
||||
var hair_color:Color = Color.WHITE
|
||||
var eyes:String = ""
|
||||
var eye_color:Color = Color.WHITE
|
||||
var eye_lashes:String = ""
|
||||
var eyelash_color:Color = Color.WHITE
|
||||
var add_on:String = ""
|
||||
var facial_hair: String = ""
|
||||
var facial_hair_color: Color = Color.WHITE
|
||||
var hairstyle: String = ""
|
||||
var hair_color: Color = Color.WHITE
|
||||
var eyes: String = ""
|
||||
var eye_color: Color = Color.WHITE
|
||||
var eye_lashes: String = ""
|
||||
var eyelash_color: Color = Color.WHITE
|
||||
var add_on: String = ""
|
||||
|
||||
var bonusmaxhp: 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"]
|
||||
|
||||
#calculated values
|
||||
var stats:Array = [
|
||||
var stats: Array = [
|
||||
5,
|
||||
4,
|
||||
5,
|
||||
@@ -62,7 +62,7 @@ var stats:Array = [
|
||||
var inventory: Array = []
|
||||
|
||||
# mainhand, offhand, headgear, body, feet, accessory (6 total)
|
||||
var equipment:Dictionary = {
|
||||
var equipment: Dictionary = {
|
||||
"mainhand": null,
|
||||
"offhand": null,
|
||||
"headgear": null,
|
||||
@@ -79,7 +79,7 @@ var equipment:Dictionary = {
|
||||
"wis": 10,
|
||||
"cha": 10,
|
||||
"lck": 10,
|
||||
"per": 10 # Perception - affects trap detection
|
||||
"per": 10 # Perception - affects trap detection
|
||||
}
|
||||
|
||||
@export var def: int = 0
|
||||
@@ -137,7 +137,7 @@ func getCalculatedStats():
|
||||
|
||||
pass
|
||||
|
||||
func get_pass(iStr:String):
|
||||
func get_pass(iStr: String):
|
||||
var cnt = 0
|
||||
if equipment["mainhand"] != null:
|
||||
for key in equipment["mainhand"].modifiers.keys():
|
||||
@@ -217,11 +217,17 @@ var crit_chance: float:
|
||||
get:
|
||||
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:
|
||||
get:
|
||||
# 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)
|
||||
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:
|
||||
get:
|
||||
@@ -230,6 +236,21 @@ var hit_chance: float:
|
||||
# Formula: 95% base + (DEX * 0.3%)
|
||||
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:
|
||||
get:
|
||||
# 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:
|
||||
hp = 0.0
|
||||
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:
|
||||
mp = clamp(mp + amount, 0, 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:
|
||||
# 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:
|
||||
# Physical damage: defense provides percentage reduction (like D&D armor)
|
||||
# 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
|
||||
if is_critical:
|
||||
# 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)
|
||||
# Check if dead (use epsilon to handle floating point precision)
|
||||
if hp <= 0.001:
|
||||
hp = 0.0 # Ensure exactly 0
|
||||
hp = 0.0 # Ensure exactly 0
|
||||
no_health.emit() # Emit when health reaches 0
|
||||
character_changed.emit(self)
|
||||
return actual_damage
|
||||
@@ -346,7 +369,7 @@ func restore_mana(amount: float) -> void:
|
||||
|
||||
func saveInventory() -> Array:
|
||||
var inventorySave = []
|
||||
for it:Item in inventory:
|
||||
for it: Item in inventory:
|
||||
inventorySave.push_back(it.save())
|
||||
return inventorySave
|
||||
|
||||
@@ -365,7 +388,7 @@ func loadInventory(iArr: Array):
|
||||
inventory.clear() # remove previous content
|
||||
for iDic in iArr:
|
||||
if iDic != null:
|
||||
inventory.push_back( Item.new(iDic) )
|
||||
inventory.push_back(Item.new(iDic))
|
||||
pass
|
||||
|
||||
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["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["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
|
||||
pass
|
||||
|
||||
@@ -383,7 +406,7 @@ func save() -> Dictionary:
|
||||
"character_type": character_type,
|
||||
"character_name": character_name,
|
||||
|
||||
"race": race, # Save race
|
||||
"race": race, # Save race
|
||||
"baseStats": baseStats,
|
||||
|
||||
"hp": hp,
|
||||
@@ -408,8 +431,8 @@ func save() -> Dictionary:
|
||||
|
||||
"facial_hair_color": facial_hair_color.to_html(true),
|
||||
"hair_color": hair_color.to_html(true),
|
||||
"eye_color": eye_color.to_html(true), # Save eye color
|
||||
"eyelash_color": eyelash_color.to_html(true), # Save eyelash color
|
||||
"eye_color": eye_color.to_html(true), # Save eye color
|
||||
"eyelash_color": eyelash_color.to_html(true), # Save eyelash color
|
||||
|
||||
"inventory": saveInventory(),
|
||||
"equipment": saveEquipment()
|
||||
@@ -471,7 +494,7 @@ func load(iDic: Dictionary) -> void:
|
||||
if iDic.has("eyes"):
|
||||
eyes = iDic.get("eyes")
|
||||
if iDic.has("eye_lashes"):
|
||||
eye_lashes = iDic.get("eye_lashes")
|
||||
eye_lashes = iDic.get("eye_lashes")
|
||||
if iDic.has("add_on"):
|
||||
add_on = iDic.get("add_on")
|
||||
if iDic.has("facial_hair_color"):
|
||||
@@ -504,29 +527,29 @@ func calculateStats():
|
||||
|
||||
pass'
|
||||
|
||||
func add_coin(iAmount:int):
|
||||
func add_coin(iAmount: int):
|
||||
coin += iAmount
|
||||
emit_signal("character_changed", self)
|
||||
pass
|
||||
|
||||
func drop_item(iItem:Item):
|
||||
func drop_item(iItem: Item):
|
||||
var index = 0
|
||||
for item in inventory:
|
||||
if item == iItem:
|
||||
break
|
||||
index+=1
|
||||
index += 1
|
||||
inventory.remove_at(index)
|
||||
emit_signal("signal_drop_item", iItem)
|
||||
emit_signal("character_changed", self)
|
||||
pass
|
||||
|
||||
func drop_equipment(iItem:Item):
|
||||
func drop_equipment(iItem: Item):
|
||||
unequip_item(iItem, false)
|
||||
# directly remove the item from the inventory
|
||||
drop_item(iItem)
|
||||
pass
|
||||
|
||||
func add_item(iItem:Item):
|
||||
func add_item(iItem: Item):
|
||||
# Try to stack with existing items if possible
|
||||
if iItem.can_have_multiple_of:
|
||||
for existing_item in inventory:
|
||||
@@ -570,7 +593,7 @@ func add_item(iItem:Item):
|
||||
emit_signal("character_changed", self)
|
||||
pass
|
||||
|
||||
func unequip_item(iItem:Item, updateChar:bool = true):
|
||||
func unequip_item(iItem: Item, updateChar: bool = true):
|
||||
if iItem.equipment_type == Item.EquipmentType.NONE:
|
||||
return
|
||||
|
||||
@@ -612,7 +635,7 @@ func forceUpdate():
|
||||
emit_signal("character_changed", self)
|
||||
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
|
||||
if iItem.equipment_type == Item.EquipmentType.NONE:
|
||||
return
|
||||
@@ -692,14 +715,14 @@ func equip_item(iItem:Item, insert_index: int = -1):
|
||||
emit_signal("character_changed", self)
|
||||
pass
|
||||
|
||||
func setSkin(iValue:int):
|
||||
func setSkin(iValue: int):
|
||||
if iValue < 0 or iValue > 6:
|
||||
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)
|
||||
pass
|
||||
|
||||
func setFacialHair(iType:int):
|
||||
func setFacialHair(iType: int):
|
||||
if iType < 0 or iType > 3:
|
||||
return
|
||||
|
||||
@@ -722,7 +745,7 @@ func setFacialHair(iType:int):
|
||||
pass
|
||||
|
||||
|
||||
func setHair(iType:int):
|
||||
func setHair(iType: int):
|
||||
if iType < 0 or iType > 12:
|
||||
return
|
||||
if iType == 0:
|
||||
@@ -731,7 +754,7 @@ func setHair(iType:int):
|
||||
return
|
||||
hairstyle = "res://assets/gfx/Puny-Characters/Layer 4 - Hairstyle/Hairstyles/FHairstyle" + str(iType)
|
||||
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"
|
||||
emit_signal("character_changed", self)
|
||||
pass
|
||||
@@ -837,7 +860,7 @@ func setFacialHairColor(iColor: Color):
|
||||
facial_hair_color = iColor
|
||||
emit_signal("character_changed", self)
|
||||
pass
|
||||
func setHairColor(iColor:Color):
|
||||
func setHairColor(iColor: Color):
|
||||
hair_color = iColor
|
||||
emit_signal("character_changed", self)
|
||||
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
|
||||
@@ -6,34 +6,34 @@ extends StaticBody2D
|
||||
#KeyDoors should always be closed at start
|
||||
#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
|
||||
@export var is_closed: bool = true
|
||||
var is_closing:bool = false
|
||||
var is_opening:bool = false
|
||||
var time_to_move:float = 0.2
|
||||
var move_timer:float = 0.0
|
||||
var animation_start_position: Vector2 = Vector2.ZERO # Position when animation started
|
||||
@export var is_closed: bool = true
|
||||
var is_closing: bool = false
|
||||
var is_opening: bool = false
|
||||
var time_to_move: float = 0.2
|
||||
var move_timer: float = 0.0
|
||||
var animation_start_position: Vector2 = Vector2.ZERO # Position when animation started
|
||||
|
||||
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 closed_position: Vector2 = Vector2.ZERO # Position when door is closed (local)
|
||||
var open_offset: Vector2 = Vector2.ZERO # Offset from closed to open position (local)
|
||||
|
||||
# Room and puzzle state
|
||||
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 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 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 enemies_defeated: bool = false # True when all enemies in room are defeated
|
||||
var switches_activated: bool = false # True when required switches are activated
|
||||
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 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 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 enemies_defeated: bool = false # True when all enemies in room are defeated
|
||||
var switches_activated: bool = false # True when required switches are activated
|
||||
|
||||
# Key door state
|
||||
var key_used: bool = false # True when key has been used
|
||||
var key_indicator: Sprite2D = null # Visual indicator showing key above door
|
||||
var key_used: bool = false # True when key has been used
|
||||
var key_indicator: Sprite2D = null # Visual indicator showing key above door
|
||||
|
||||
# Floor switches this door is connected to
|
||||
var connected_switches: Array = [] # Array of floor switch nodes
|
||||
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 connected_switches: Array = [] # Array of floor switch nodes
|
||||
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
|
||||
|
||||
# Smoke puff scene for StoneDoor effects
|
||||
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)
|
||||
if direction == "Left":
|
||||
self.rotate(-PI/2)
|
||||
self.rotate(-PI / 2)
|
||||
elif direction == "Right":
|
||||
self.rotate(PI/2)
|
||||
self.rotate(PI / 2)
|
||||
elif direction == "Down":
|
||||
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)
|
||||
# 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)
|
||||
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":
|
||||
# Door on bottom wall:
|
||||
# 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)
|
||||
# So open is 16px DOWN from closed, open_offset = (0, 16)
|
||||
# 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":
|
||||
# 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)
|
||||
# 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)
|
||||
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":
|
||||
# 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)
|
||||
# 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)
|
||||
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
|
||||
# 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 target_pos = closed_position + open_offset
|
||||
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
|
||||
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)
|
||||
|
||||
# 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
|
||||
var start_pos = animation_start_position if animation_start_position != Vector2.ZERO else (closed_position + open_offset)
|
||||
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
|
||||
if type == "StoneDoor" or type == "GateDoor":
|
||||
@@ -220,7 +220,7 @@ func _process(delta: float) -> void:
|
||||
# Move door to open position (away from closed position)
|
||||
var open_position = closed_position + open_offset
|
||||
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
|
||||
set_collision_layer_value(7, false)
|
||||
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_closing = false
|
||||
move_timer = 0.0
|
||||
animation_start_position = Vector2.ZERO # Reset animation start position
|
||||
animation_start_position = Vector2.ZERO # Reset animation start position
|
||||
else:
|
||||
# Closing animation complete
|
||||
is_closed = true
|
||||
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
|
||||
set_collision_layer_value(7, true)
|
||||
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_closing = false
|
||||
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)
|
||||
if type == "StoneDoor" or type == "GateDoor":
|
||||
_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
|
||||
# Position and is_closed should only be changed by explicit _open()/_close() calls or animation
|
||||
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
|
||||
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:
|
||||
# Door is at closed position - collision should be ENABLED
|
||||
@@ -330,7 +330,7 @@ func _update_collision_based_on_position():
|
||||
func _open():
|
||||
# Only open on server/authority in multiplayer, then sync to clients
|
||||
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
|
||||
return # Clients wait for RPC
|
||||
return # Clients wait for RPC
|
||||
|
||||
$TeleporterIntoClosedRoom.is_enabled = false
|
||||
# CRITICAL: For KeyDoors, ensure they start from closed position before opening
|
||||
@@ -343,7 +343,7 @@ func _open():
|
||||
position = closed_position
|
||||
global_position = closed_position
|
||||
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)
|
||||
else:
|
||||
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
|
||||
is_closed = 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
|
||||
if closed_position != Vector2.ZERO:
|
||||
@@ -409,7 +409,7 @@ func _open():
|
||||
func _close():
|
||||
# Only close on server/authority in multiplayer, then sync to clients
|
||||
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)
|
||||
if type == "KeyDoor":
|
||||
@@ -424,23 +424,23 @@ func _close():
|
||||
|
||||
# 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 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)
|
||||
|
||||
# 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:
|
||||
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
|
||||
if is_actually_at_closed and not is_closed:
|
||||
# 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)
|
||||
position = closed_position # Ensure exact position
|
||||
position = closed_position # Ensure exact position
|
||||
is_closed = 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
|
||||
# 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
|
||||
# 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)
|
||||
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)
|
||||
|
||||
@@ -508,7 +508,7 @@ func _ready_after_setup():
|
||||
var closed_offset = Vector2.ZERO
|
||||
match direction:
|
||||
"Up":
|
||||
closed_offset = Vector2(0, 16) # Closed is 16px DOWN from open
|
||||
closed_offset = Vector2(0, 16) # Closed is 16px DOWN from open
|
||||
"Down":
|
||||
# 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)
|
||||
@@ -517,20 +517,20 @@ func _ready_after_setup():
|
||||
if type == "KeyDoor":
|
||||
# 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
|
||||
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:
|
||||
# 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":
|
||||
closed_offset = Vector2(16, 0) # Closed is 16px RIGHT from open
|
||||
closed_offset = Vector2(16, 0) # Closed is 16px RIGHT from open
|
||||
"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
|
||||
|
||||
# Update open_offset for animation logic (offset from closed to open)
|
||||
# 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)
|
||||
|
||||
@@ -541,38 +541,38 @@ func _ready_after_setup():
|
||||
if type == "KeyDoor":
|
||||
# For KeyDoors, the position from game_world is the 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
|
||||
var keydoor_open_offset = Vector2.ZERO
|
||||
match direction:
|
||||
"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":
|
||||
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":
|
||||
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":
|
||||
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
|
||||
closed_position = keydoor_closed_position # Closed is where game_world placed it
|
||||
open_offset = keydoor_open_offset # Offset to move from closed to open
|
||||
closed_position = keydoor_closed_position # Closed is where game_world placed it
|
||||
open_offset = keydoor_open_offset # Offset to move from closed to open
|
||||
|
||||
# KeyDoor starts CLOSED
|
||||
is_closed = true
|
||||
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)
|
||||
# Create key indicator sprite for KeyDoor
|
||||
_create_key_indicator()
|
||||
return # Exit early for KeyDoors
|
||||
return # Exit early for KeyDoors
|
||||
elif is_closed:
|
||||
# StoneDoor/GateDoor starting closed (shouldn't happen for blocking doors, but handle it)
|
||||
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)
|
||||
LogManager.log("Door: Starting CLOSED at position " + str(position) + " (type: " + str(type) + ", direction: " + str(direction) + ") - collision ENABLED", LogManager.CATEGORY_DOOR)
|
||||
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)
|
||||
position = open_position
|
||||
|
||||
global_position = position # Ensure global_position matches 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
|
||||
global_position = position # Ensure global_position matches 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
|
||||
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
|
||||
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:
|
||||
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
|
||||
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)
|
||||
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
|
||||
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:
|
||||
LogManager.log_error("Door: ERROR - Door is at open position but is_closed is true! Fixing state...", LogManager.CATEGORY_DOOR)
|
||||
is_closed = false
|
||||
@@ -624,7 +624,7 @@ func _ready_after_setup():
|
||||
func _create_key_indicator():
|
||||
# Create visual indicator for key above door
|
||||
if key_indicator:
|
||||
return # Already created
|
||||
return # Already created
|
||||
|
||||
key_indicator = Sprite2D.new()
|
||||
# Load key texture from loot system
|
||||
@@ -633,9 +633,9 @@ func _create_key_indicator():
|
||||
key_indicator.texture = key_texture
|
||||
key_indicator.hframes = 20
|
||||
key_indicator.vframes = 14
|
||||
key_indicator.frame = (13 * 20) + 10 # Key frame from loot system
|
||||
key_indicator.position = Vector2(0, -24) # Above door
|
||||
key_indicator.visible = false # Hidden until key is used
|
||||
key_indicator.frame = (13 * 20) + 10 # Key frame from loot system
|
||||
key_indicator.position = Vector2(0, -24) # Above door
|
||||
key_indicator.visible = false # Hidden until key is used
|
||||
add_child(key_indicator)
|
||||
|
||||
func _on_room_entered(body):
|
||||
@@ -646,7 +646,7 @@ func _on_room_entered(body):
|
||||
|
||||
# Verify this door is in the room we just entered
|
||||
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 door_room1 = room1 if room1 else {}
|
||||
@@ -675,7 +675,7 @@ func _on_room_entered(body):
|
||||
if not puzzle_solved:
|
||||
# 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 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)
|
||||
|
||||
@@ -699,7 +699,7 @@ func _on_room_entered(body):
|
||||
_close()
|
||||
# Don't check puzzle state immediately - wait for door to finish closing
|
||||
# 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:
|
||||
# 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)
|
||||
@@ -714,7 +714,7 @@ func _on_room_entered(body):
|
||||
position = closed_position
|
||||
global_position = closed_position
|
||||
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)
|
||||
# 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
|
||||
@@ -732,7 +732,7 @@ func _on_room_exited(body):
|
||||
func _check_puzzle_state():
|
||||
# Only check puzzle state on server/authority in multiplayer
|
||||
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)
|
||||
# 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)
|
||||
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_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 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 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),
|
||||
# allow switch to trigger again to open the door
|
||||
@@ -804,7 +804,35 @@ func _check_puzzle_state():
|
||||
switches_activated = 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:
|
||||
# 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
|
||||
# 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
|
||||
@@ -829,7 +857,7 @@ func _are_all_enemies_defeated() -> bool:
|
||||
if child.is_in_group("enemy"):
|
||||
# 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"):
|
||||
continue # Skip pre-spawned enemies
|
||||
continue # Skip pre-spawned enemies
|
||||
|
||||
# Check if enemy is in this room (use position-based check, more reliable)
|
||||
var enemy_in_room = false
|
||||
@@ -849,7 +877,7 @@ func _are_all_enemies_defeated() -> bool:
|
||||
var spawner_name = child.get_meta("spawner_name")
|
||||
# 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:
|
||||
enemy_in_room = true # Confirmed by spawner name
|
||||
enemy_in_room = true # Confirmed by spawner name
|
||||
|
||||
if enemy_in_room:
|
||||
room_spawned_enemies.append(child)
|
||||
@@ -874,12 +902,12 @@ func _are_all_enemies_defeated() -> bool:
|
||||
|
||||
if not enemy_is_dead:
|
||||
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 room_spawned_enemies.size() > 0:
|
||||
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
|
||||
# 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)
|
||||
|
||||
if not enemy_in_room:
|
||||
continue # Skip enemies not in this room
|
||||
continue # Skip enemies not in this room
|
||||
|
||||
# Check if enemy is alive
|
||||
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
|
||||
# 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)
|
||||
|
||||
var spawners_in_room = []
|
||||
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)
|
||||
|
||||
if not enemy_in_room:
|
||||
continue # Skip enemies not in this room
|
||||
continue # Skip enemies not in this room
|
||||
|
||||
# Check if enemy is dead
|
||||
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
|
||||
if spawners_in_room.size() == 0:
|
||||
for spawner_name in unique_spawner_names_that_spawned.keys():
|
||||
spawners_in_room.append(null) # Placeholder for destroyed spawner
|
||||
spawners_that_have_spawned.append(null) # Count as spawned
|
||||
spawners_in_room.append(null) # Placeholder for destroyed spawner
|
||||
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)
|
||||
else:
|
||||
# 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: 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():
|
||||
# Spawn 1-3 smoke puffs when StoneDoor finishes closing
|
||||
if not smoke_puff_scene:
|
||||
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):
|
||||
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_y = randf_range(-8, 8)
|
||||
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
|
||||
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:
|
||||
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):
|
||||
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_y = randf_range(-8, 8)
|
||||
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
|
||||
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)
|
||||
return false
|
||||
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!
|
||||
# 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)
|
||||
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):
|
||||
# Player entered key interaction area
|
||||
@@ -1228,7 +1255,7 @@ func _show_key_indicator():
|
||||
key_indicator.visible = true
|
||||
# Make sure it's on top (higher z-index or add to front)
|
||||
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:
|
||||
# Create key indicator if it doesn't exist yet
|
||||
_create_key_indicator()
|
||||
@@ -1244,13 +1271,13 @@ func teleportPlayer(body: Node2D):
|
||||
var keydoor_open_offset = Vector2.ZERO
|
||||
match direction:
|
||||
"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":
|
||||
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":
|
||||
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":
|
||||
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
|
||||
|
||||
@@ -1284,6 +1311,8 @@ func _sync_door_open():
|
||||
var is_actually_open = distance_to_closed > 5.0
|
||||
|
||||
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
|
||||
if closed_position != Vector2.ZERO:
|
||||
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")
|
||||
|
||||
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_character_stats()
|
||||
|
||||
current_health = max_health
|
||||
# If spawn/scene set a higher max_health (e.g. boss with 1200), apply it to character_stats so take_damage uses it
|
||||
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")
|
||||
|
||||
# Setup shadow (if it exists - some enemies like humanoids don't have a Shadow node)
|
||||
@@ -72,6 +81,19 @@ func _ready():
|
||||
# This allows enemies to collide with interactable objects so they can path around them
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
# Override in subclasses to set specific baseStats
|
||||
@@ -127,70 +149,9 @@ func _physics_process(delta):
|
||||
burn_sprite.set_meta("burn_animation_timer", anim_timer)
|
||||
return
|
||||
|
||||
# Fallout: humanoid sinks like player (FALL anim) then dies; slime rotates 45 + scale then dies
|
||||
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
|
||||
scale = Vector2.ONE * max(0.0, fallout_scale_progress)
|
||||
if has_method("_set_animation"):
|
||||
_set_animation("FALL")
|
||||
move_and_slide()
|
||||
# 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 _check_and_handle_fallout(delta):
|
||||
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
|
||||
if attack_timer > 0:
|
||||
@@ -211,8 +172,9 @@ func _physics_process(delta):
|
||||
if not is_knocked_back:
|
||||
_ai_behavior(delta)
|
||||
|
||||
# Slime, rat, humanoid: try to avoid stepping onto fallout (position_z <= 0 = ground enemies only; bat has position_z 1)
|
||||
if position_z <= 0.0 and not fallout_state and not fallout_defeat_started and velocity.length_squared() > 1.0:
|
||||
# All ground enemies: avoid stepping onto fallout when moving under their own control (not when knocked back)
|
||||
# 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")
|
||||
if game_world and game_world.has_method("_is_position_on_fallout_tile"):
|
||||
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")
|
||||
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])
|
||||
# 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)
|
||||
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
|
||||
_remove_burn_debuff()
|
||||
|
||||
# Sync position and animation to clients (only server sends)
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
|
||||
# Get state value if enemy has a state variable (for bats/slimes)
|
||||
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
|
||||
# This avoids node path resolution issues when clients haven't spawned yet
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if game_world and game_world.has_method("_sync_enemy_position"):
|
||||
# Send via game_world using enemy name/index and position for identification
|
||||
game_world._rpc_to_ready_peers("_sync_enemy_position", [enemy_name, enemy_index, position, velocity, position_z, current_direction, anim_frame, "", 0, state_val])
|
||||
# Removed fallback rpc() call - it causes node path resolution errors
|
||||
# If game_world is not available, skip sync (will sync next frame)
|
||||
# Sync position and animation to clients (only server sends). Also call from subclasses that override _physics_process (e.g. spider).
|
||||
_send_position_sync_to_clients()
|
||||
|
||||
# Call this from base _physics_process or from subclasses that override _physics_process (e.g. enemy_spider) so position sync is always sent.
|
||||
func _send_position_sync_to_clients() -> void:
|
||||
if not multiplayer.has_multiplayer_peer() or not is_multiplayer_authority():
|
||||
return
|
||||
var state_val = -1
|
||||
if "state" in self:
|
||||
state_val = get("state") as int
|
||||
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):
|
||||
# Override in subclasses
|
||||
@@ -528,6 +561,12 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
|
||||
else:
|
||||
# Fallback: try direct RPC (may fail if node path doesn't match)
|
||||
_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:
|
||||
# Prevent multiple death triggers
|
||||
@@ -582,7 +621,7 @@ func _show_damage_number(amount: float, from_position: Vector2, is_critical: boo
|
||||
else:
|
||||
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.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)
|
||||
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 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 random_move_dir: Vector2 = Vector2.ZERO
|
||||
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 TILE_SIZE: int = 16
|
||||
const TILE_STRIDE: int = 17 # 16 + separation 1
|
||||
@onready var detection_area: Area2D = $DetectionArea
|
||||
@onready var emerge_area: Area2D = $EmergeArea
|
||||
@onready var grab_area: Area2D = $GrabPlayerArea
|
||||
@onready var interest_area: Area2D = $PlayerInterestArea
|
||||
@@ -42,7 +46,7 @@ func _ready() -> void:
|
||||
# Start hidden
|
||||
modulate.a = 0.0
|
||||
# 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:
|
||||
area.set_collision_mask_value(1, true)
|
||||
area.monitoring = true
|
||||
@@ -247,6 +251,59 @@ func _ai_behavior(delta: float) -> void:
|
||||
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:
|
||||
if state != HandState.HIDDEN:
|
||||
return
|
||||
@@ -257,6 +314,8 @@ func _on_emerge_area_body_entered(body: Node2D) -> void:
|
||||
if not is_multiplayer_authority():
|
||||
return
|
||||
|
||||
# Reveal effect is removed only when emerge animation finishes (_on_animation_finished)
|
||||
|
||||
state = HandState.EMERGING
|
||||
modulate.a = 1.0
|
||||
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:
|
||||
if anim_name == "emerge":
|
||||
_remove_detected_effect() # Remove reveal effect only once hand has fully emerged
|
||||
state = HandState.IDLE
|
||||
if anim_player and anim_player.has_animation("idle"):
|
||||
anim_player.play("idle")
|
||||
@@ -330,7 +390,7 @@ func _sync_hand_emerged():
|
||||
# Sync hand emergence visibility to clients
|
||||
if is_multiplayer_authority():
|
||||
return # Authority already handled it locally
|
||||
|
||||
# Effect removed when emerge animation finishes on each client (_on_animation_finished)
|
||||
if state == HandState.HIDDEN:
|
||||
state = HandState.EMERGING
|
||||
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 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_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 spell_cooldown_timer: float = 0.0
|
||||
var bomb_cooldown_timer: float = 0.0
|
||||
@@ -766,7 +767,7 @@ func _load_random_headgear():
|
||||
"SoldierIronHelmBlue.png", "SoldierSteelHelmBlue.png"
|
||||
],
|
||||
"Basic Range": [
|
||||
"ArcherHatCyan.png", "HunterHatRed.png", "RangerHatGreen.png", "RogueHatGreen.png"
|
||||
"ArcherHatCyan.png", "HunterHatRed.png", "RogueHatGreen.png"
|
||||
],
|
||||
"French": ["MusketeerHatPurple.png"],
|
||||
"Japanese": [
|
||||
@@ -978,10 +979,10 @@ func _assign_loadout():
|
||||
elif spell_roll < 0.20:
|
||||
spell_type = "healing"
|
||||
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:
|
||||
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
|
||||
if appearance_rng.randf() < 0.12:
|
||||
can_lift_throw = true
|
||||
@@ -1016,32 +1017,10 @@ func _get_nearby_liftable() -> Node:
|
||||
return best
|
||||
|
||||
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)
|
||||
|
||||
# Handle dead state (from parent)
|
||||
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
|
||||
# Humanoid-specific timers (base updates attack_timer; run before base AI)
|
||||
if spell_cooldown_timer > 0:
|
||||
spell_cooldown_timer -= delta
|
||||
if bomb_cooldown_timer > 0:
|
||||
@@ -1055,47 +1034,28 @@ func _physics_process(delta):
|
||||
if shield_block_timer <= 0:
|
||||
is_blocking = false
|
||||
_update_shield_visibility()
|
||||
|
||||
# Update bow charge pulse timer when charging bow
|
||||
if ai_state == AIState.BOW_CHARGING and is_charging_attack:
|
||||
bow_charge_tint_pulse_time += delta * bow_charge_tint_pulse_speed_charged
|
||||
_apply_bow_charge_tint()
|
||||
elif ai_state != AIState.BOW_CHARGING:
|
||||
# Clear bow charge tint when not charging
|
||||
_clear_bow_charge_tint()
|
||||
|
||||
# Handle knockback
|
||||
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)
|
||||
# Base handles: dead, not authority, fallout (drag/sink), knockback, attack_timer, _ai_behavior, avoidance, move_and_slide, collisions, burn
|
||||
super._physics_process(delta)
|
||||
|
||||
# Enemy AI - only if not knocked back
|
||||
if not is_knocked_back:
|
||||
_ai_behavior(delta)
|
||||
# Reset attack flags when cooldown expired (base decrements attack_timer only)
|
||||
if attack_timer <= 0:
|
||||
can_attack = true
|
||||
is_attacking = false
|
||||
|
||||
# Move
|
||||
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)
|
||||
# Sync position and animation to clients (only server sends; humanoid uses game_world RPC)
|
||||
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")
|
||||
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_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])
|
||||
else:
|
||||
# Fallback: try direct call to _sync_position (not RPC)
|
||||
_sync_position(position, velocity, position_z, current_direction, 0, current_animation, current_frame)
|
||||
|
||||
func _ai_behavior(delta):
|
||||
@@ -1249,28 +1209,19 @@ func _chasing_behavior(delta_arg):
|
||||
|
||||
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:
|
||||
# Check if player is attacking (recently attacked or in melee range)
|
||||
var player_is_attacking = false
|
||||
if dist < 60.0: # Close enough that player might attack
|
||||
# Check if player is facing us and might be attacking
|
||||
if "is_attacking" in target_player and target_player.is_attacking:
|
||||
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:
|
||||
# Only consider blocking when player is actually attacking (sword up), not just "close"
|
||||
var player_is_attacking = (dist < 70.0 and "is_attacking" in target_player and target_player.is_attacking)
|
||||
# Raise shield only when player is attacking (not when merely close)
|
||||
if player_is_attacking and not is_blocking and randf() < 0.65:
|
||||
# 65% chance to block when player is attacking (not 100%), so we attack sometimes
|
||||
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()
|
||||
# Play shield activation sound
|
||||
if sfx_activate_shield:
|
||||
sfx_activate_shield.play()
|
||||
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
|
||||
_update_shield_visibility()
|
||||
|
||||
@@ -1320,6 +1271,9 @@ func _chasing_behavior(delta_arg):
|
||||
|
||||
# --- Melee: close enough to 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)
|
||||
if is_blocking:
|
||||
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:
|
||||
var to_attacker = (from_position - global_position).normalized()
|
||||
var facing = _get_attack_direction_vector()
|
||||
# Check if attack is coming from the direction we're facing (blocking direction)
|
||||
if to_attacker.dot(facing) > 0.5 and randf() < shield_block_chance:
|
||||
# Successfully blocked - reduce damage
|
||||
amount = amount * 0.5
|
||||
# Raise shield if not already blocking
|
||||
var attack_from_front = to_attacker.dot(facing) > 0.5
|
||||
# Full block when already blocking and attack from front, or reactive block roll
|
||||
var did_block = (is_blocking and attack_from_front) or (attack_from_front and randf() < shield_block_chance)
|
||||
if did_block:
|
||||
# Full block: no damage, show BLOCKED to attacker, small knockback on attacker, chance to lower our shield
|
||||
if not is_blocking:
|
||||
is_blocking = true
|
||||
shield_block_timer = shield_block_duration
|
||||
shield_block_timer = max(shield_block_duration, SHIELD_BLOCK_MIN_HOLD)
|
||||
_update_shield_visibility()
|
||||
if sfx_activate_shield:
|
||||
sfx_activate_shield.play()
|
||||
# Play block sound
|
||||
else:
|
||||
shield_block_timer = max(shield_block_timer, SHIELD_BLOCK_MIN_HOLD)
|
||||
if sfx_block_with_shield:
|
||||
sfx_block_with_shield.play()
|
||||
# Face the attacker
|
||||
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:
|
||||
# Attack not blocked, but raise shield anyway if we have one (defensive reaction)
|
||||
if not is_blocking:
|
||||
# Attack not blocked; brief defensive raise (shorter so they attack again sooner)
|
||||
if not is_blocking and randf() < 0.5:
|
||||
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()
|
||||
if sfx_activate_shield:
|
||||
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_boss: Label = 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_player_count: 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_boss = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerBoss/LabelBoss")
|
||||
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_player_count = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelPlayerCount")
|
||||
label_room_code = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelRoomCode")
|
||||
@@ -86,11 +105,13 @@ func _ready():
|
||||
if not game_world:
|
||||
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:
|
||||
texture_progress_bar_boss_hp.visible = false
|
||||
if label_boss:
|
||||
label_boss.visible = false
|
||||
if center_bottom_boss:
|
||||
center_bottom_boss.visible = false
|
||||
|
||||
# Update host info display
|
||||
_update_host_info()
|
||||
@@ -387,32 +408,52 @@ func start_timer():
|
||||
timer_running = true
|
||||
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():
|
||||
# Find boss enemy (if any)
|
||||
# Find boss enemy that is ACTIVATED (bar only shows when boss is activated)
|
||||
var boss_enemy = null
|
||||
var enemies = get_tree().get_nodes_in_group("enemy")
|
||||
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"):
|
||||
if enemy.has_meta("is_boss") and enemy.get_meta("is_boss") and enemy.has_meta("boss_activated") and enemy.get_meta("boss_activated"):
|
||||
boss_enemy = enemy
|
||||
break
|
||||
|
||||
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:
|
||||
texture_progress_bar_boss_hp.visible = true
|
||||
texture_progress_bar_boss_hp.visible = false
|
||||
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 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:
|
||||
texture_progress_bar_boss_hp.max_value = max_health
|
||||
texture_progress_bar_boss_hp.value = health
|
||||
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:
|
||||
texture_progress_bar_boss_hp.visible = false
|
||||
if label_boss:
|
||||
|
||||
@@ -40,6 +40,13 @@ var chest_closed_frame: int = -1
|
||||
var chest_opened_frame: int = -1
|
||||
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
|
||||
var sync_timer: float = 0.0
|
||||
var sync_interval: float = 0.05 # Sync 20 times per second
|
||||
@@ -72,6 +79,94 @@ func _ready():
|
||||
if shadow:
|
||||
shadow.modulate = Color(0, 0, 0, 0.5)
|
||||
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):
|
||||
# All clients simulate physics locally for smooth visuals
|
||||
@@ -81,13 +176,15 @@ func _physics_process(delta):
|
||||
return
|
||||
|
||||
if not is_frozen:
|
||||
# Fallout: sink and disappear when on ground (not held, not airborne)
|
||||
if not is_airborne and position_z <= 0.0:
|
||||
# Fallout: sink and disappear when on ground (not held, not airborne). Pillars must never sink.
|
||||
if not is_airborne and position_z <= 0.0 and object_type != "Pillar":
|
||||
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 not falling_into_fallout:
|
||||
falling_into_fallout = true
|
||||
fallout_sink_progress = 1.0
|
||||
if has_node("SfxFall"):
|
||||
$SfxFall.play()
|
||||
fallout_sink_progress -= delta / FALLOUT_SINK_DURATION
|
||||
if fallout_sink_progress <= 0.0:
|
||||
queue_free()
|
||||
@@ -388,6 +485,9 @@ func _is_wall_collider(collider) -> bool:
|
||||
return false
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
"""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:
|
||||
return
|
||||
health -= amount
|
||||
@@ -471,6 +574,9 @@ func take_damage(amount: float, _from_position: Vector2) -> void:
|
||||
_break_into_pieces()
|
||||
|
||||
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
|
||||
if object_type == "Chest" and not is_chest_opened:
|
||||
# In multiplayer, send RPC to server if client is opening
|
||||
@@ -807,6 +913,13 @@ func _open_chest(by_player: Node = null):
|
||||
return
|
||||
$SfxOpenChest.play()
|
||||
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
|
||||
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:
|
||||
candidates.append(item_id)
|
||||
|
||||
# Select random item from candidates using deterministic RNG
|
||||
var random_item_id = candidates[chest_rng.randi() % candidates.size()] if not candidates.is_empty() else null
|
||||
# Chance for empty chest (no item, no coin, no loot)
|
||||
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
|
||||
|
||||
# CRITICAL: Instantly give item to player instead of spawning loot object
|
||||
if by_player and is_instance_valid(by_player) and by_player.is_in_group("player") and chest_item:
|
||||
# Add item to player inventory
|
||||
if by_player.character_stats:
|
||||
var was_encumbered = by_player.character_stats.is_over_encumbered()
|
||||
by_player.character_stats.add_item(chest_item)
|
||||
if not was_encumbered and by_player.character_stats.is_over_encumbered():
|
||||
if by_player.has_method("show_floating_status"):
|
||||
by_player.show_floating_status("Encumbered!", Color(1.0, 0.5, 0.2))
|
||||
|
||||
# Show pickup notification
|
||||
var items_texture = load(chest_item.spritePath) if chest_item.spritePath != "" else null
|
||||
var display_text = chest_item.item_name.to_upper()
|
||||
var item_color = Color.WHITE
|
||||
|
||||
# Determine color based on item type/rarity
|
||||
if chest_item.item_type == Item.ItemType.Restoration:
|
||||
item_color = Color.GREEN
|
||||
elif chest_item.item_type == Item.ItemType.Equippable:
|
||||
item_color = Color.CYAN # Cyan for equipment (matches loot pickup color)
|
||||
else:
|
||||
item_color = Color.WHITE
|
||||
|
||||
# Show notification with item sprite (pass chest_item for ItemSprite colorization)
|
||||
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)
|
||||
else:
|
||||
_show_item_pickup_notification(by_player, display_text, item_color, null, 0, 0, 0, chest_item)
|
||||
|
||||
# Play chest open sound
|
||||
if has_node("SfxChestOpen"):
|
||||
$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)
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
var player_peer_id = by_player.get_multiplayer_authority() if by_player else 0
|
||||
# Reuse game_world from earlier in the function
|
||||
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")
|
||||
# Sync chest open visual with item_data so clients can show the floating text
|
||||
var item_data = chest_item.save() if chest_item else {}
|
||||
game_world._rpc_to_ready_peers("_sync_chest_open_by_name", [chest_name, "item", player_peer_id, item_data])
|
||||
# Sync inventory+equipment to joiner (server added item; joiner's client must apply)
|
||||
if multiplayer.is_server():
|
||||
var owner_id = by_player.get_multiplayer_authority()
|
||||
if owner_id != 1 and owner_id != multiplayer.get_unique_id():
|
||||
var inv_data: Array = []
|
||||
for inv_item in by_player.character_stats.inventory:
|
||||
inv_data.append(inv_item.save() if inv_item else null)
|
||||
var equip_data: Dictionary = {}
|
||||
for slot_name in by_player.character_stats.equipment.keys():
|
||||
var eq = by_player.character_stats.equipment[slot_name]
|
||||
equip_data[slot_name] = eq.save() if eq else null
|
||||
if by_player.has_method("_apply_inventory_and_equipment_from_server"):
|
||||
by_player._apply_inventory_and_equipment_from_server.rpc_id(owner_id, inv_data, equip_data)
|
||||
# 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"):
|
||||
if chest_item:
|
||||
# Add item to player inventory
|
||||
if by_player.character_stats:
|
||||
var was_encumbered = by_player.character_stats.is_over_encumbered()
|
||||
by_player.character_stats.add_item(chest_item)
|
||||
if not was_encumbered and by_player.character_stats.is_over_encumbered():
|
||||
if by_player.has_method("show_floating_status"):
|
||||
by_player.show_floating_status("Encumbered!", Color(1.0, 0.5, 0.2))
|
||||
|
||||
# Show pickup notification
|
||||
var items_texture = load(chest_item.spritePath) if chest_item.spritePath != "" else null
|
||||
var display_text = chest_item.item_name.to_upper()
|
||||
var item_color = Color.WHITE
|
||||
|
||||
# Determine color based on item type/rarity
|
||||
if chest_item.item_type == Item.ItemType.Restoration:
|
||||
item_color = Color.GREEN
|
||||
elif chest_item.item_type == Item.ItemType.Equippable:
|
||||
item_color = Color.CYAN # Cyan for equipment (matches loot pickup color)
|
||||
else:
|
||||
item_color = Color.WHITE
|
||||
|
||||
# Show notification with item sprite (pass chest_item for ItemSprite colorization)
|
||||
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)
|
||||
else:
|
||||
_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
|
||||
if has_node("SfxGetItemFromChest"):
|
||||
$SfxGetItemFromChest.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)
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
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")
|
||||
var item_data = chest_item.save() if chest_item else {}
|
||||
game_world._rpc_to_ready_peers("_sync_chest_open_by_name", [chest_name, "item", player_peer_id, item_data])
|
||||
if multiplayer.is_server():
|
||||
var owner_id = by_player.get_multiplayer_authority()
|
||||
if owner_id != 1 and owner_id != multiplayer.get_unique_id():
|
||||
var inv_data: Array = []
|
||||
for inv_item in by_player.character_stats.inventory:
|
||||
inv_data.append(inv_item.save() if inv_item else null)
|
||||
var equip_data: Dictionary = {}
|
||||
for slot_name in by_player.character_stats.equipment.keys():
|
||||
var eq = by_player.character_stats.equipment[slot_name]
|
||||
equip_data[slot_name] = eq.save() if eq else null
|
||||
if by_player.has_method("_apply_inventory_and_equipment_from_server"):
|
||||
by_player._apply_inventory_and_equipment_from_server.rpc_id(owner_id, inv_data, equip_data)
|
||||
else:
|
||||
# Empty chest: show "EMPTY CHEST" floating text
|
||||
_show_item_pickup_notification(by_player, "EMPTY CHEST", Color(0.5, 0.5, 0.5))
|
||||
print(name, " opened by ", by_player.name, " - empty chest")
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
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:
|
||||
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)
|
||||
else:
|
||||
_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:
|
||||
# Fallback to old loot type system (for backwards compatibility)
|
||||
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)
|
||||
const _BAR_WIDTH: int = 100
|
||||
const _BAR_VALUE_MIN_WIDTH: int = 44 # "999/999"
|
||||
const _BAR_LABEL_MIN_WIDTH: int = 52 # "Weight:", "Exp:", "HP:", "MP:", "Coin:"
|
||||
const _BAR_VALUE_MIN_WIDTH: int = 44 # "999/999"
|
||||
const _BAR_LABEL_MIN_WIDTH: int = 52 # "Weight:", "Exp:", "HP:", "MP:", "Coin:"
|
||||
|
||||
# Weight UI elements (created programmatically)
|
||||
var weight_container: HBoxContainer = null
|
||||
@@ -257,18 +257,19 @@ func _update_stats():
|
||||
str(char_stats.baseStats.lck) + "\n" + \
|
||||
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:
|
||||
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:
|
||||
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.defense,
|
||||
char_stats.move_speed,
|
||||
char_stats.attack_speed,
|
||||
char_stats.sight,
|
||||
char_stats.spell_amp,
|
||||
char_stats.crit_chance
|
||||
char_stats.crit_chance,
|
||||
char_stats.dodge_chance * 100.0
|
||||
]
|
||||
|
||||
# HP bar
|
||||
@@ -1042,7 +1043,7 @@ func _update_selection_from_navigation():
|
||||
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)
|
||||
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
|
||||
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():
|
||||
@@ -1368,7 +1369,7 @@ func _on_inventory_item_pressed(item: Item):
|
||||
|
||||
_update_selection_highlight()
|
||||
_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):
|
||||
# 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"):
|
||||
var mana_amount = item.modifiers["mp"]
|
||||
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)
|
||||
if index >= 0:
|
||||
@@ -1845,3 +1849,6 @@ func _lock_player_controls(lock: bool):
|
||||
var local_players = player_manager.get_local_players()
|
||||
for player in local_players:
|
||||
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.sight) + "\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():
|
||||
# Equipment slot order: mainhand, offhand, headgear, armour, boots, accessory
|
||||
|
||||
@@ -966,10 +966,7 @@ static func _load_all_items():
|
||||
"description": "A standard longsword",
|
||||
"item_type": Item.ItemType.Equippable,
|
||||
"equipment_type": Item.EquipmentType.MAINHAND,
|
||||
"weapon_type": Item.WeaponType.SWORD,
|
||||
"spriteFrame": 3 * 20 + 10, # 10,3
|
||||
"modifiers": {"dmg": 5},
|
||||
"buy_cost": 100,
|
||||
"sell_worth": 30,
|
||||
"rarity": ItemRarity.COMMON,
|
||||
"weight": 3.5
|
||||
|
||||
@@ -58,7 +58,7 @@ var item: Item = null # Item instance (for LootType.ITEM)
|
||||
# Quantity badge for items with quantity > 1
|
||||
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 fallout_sink_progress: float = 1.0
|
||||
const FALLOUT_SINK_DURATION: float = 0.4
|
||||
@@ -234,6 +234,8 @@ func _physics_process(delta):
|
||||
if not falling_into_fallout:
|
||||
falling_into_fallout = true
|
||||
fallout_sink_progress = 1.0
|
||||
if has_node("SfxFall"):
|
||||
$SfxFall.play()
|
||||
# Lock to center of fallout tile and stop all x/y movement
|
||||
if gw.has_method("_get_tile_center_at"):
|
||||
global_position = gw._get_tile_center_at(global_position)
|
||||
@@ -244,13 +246,26 @@ func _physics_process(delta):
|
||||
pickup_area.monitorable = false
|
||||
fallout_sink_progress -= delta / FALLOUT_SINK_DURATION
|
||||
if fallout_sink_progress <= 0.0:
|
||||
# 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
|
||||
# Keys respawn on a safe tile (like player); other loot is removed
|
||||
if loot_type == LootType.KEY:
|
||||
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()
|
||||
if game_world and game_world.has_method("_get_nearest_safe_tile_center"):
|
||||
var safe_pos = game_world._get_nearest_safe_tile_center(global_position)
|
||||
_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
|
||||
scale = Vector2.ONE * max(0.0, fallout_sink_progress)
|
||||
move_and_slide()
|
||||
@@ -447,6 +462,20 @@ func _animate_coin(delta):
|
||||
var frame = int(coin_anim_time) % 6
|
||||
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):
|
||||
if falling_into_fallout:
|
||||
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_FLOOR: Color = Color(0.38, 0.38, 0.44)
|
||||
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)
|
||||
|
||||
var _map_size: Vector2i = Vector2i.ZERO
|
||||
var _explored_map: PackedInt32Array = PackedInt32Array()
|
||||
var _grid: Array = [] # 2D grid [x][y]: 0=wall, 1=floor, 2=door, 3=corridor
|
||||
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_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
|
||||
_map_size = map_size
|
||||
_grid = grid
|
||||
_player_tile = player_tile
|
||||
_other_player_tiles = other_player_tiles
|
||||
_exit_tile = exit_tile
|
||||
_exit_discovered = exit_discovered
|
||||
queue_redraw()
|
||||
@@ -73,6 +76,12 @@ func _draw() -> void:
|
||||
var py := float(_player_tile.y) * th + th * 0.5
|
||||
var r := maxf(2.0, minf(tw, th) * 0.4)
|
||||
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:
|
||||
var ex := float(_exit_tile.x) * tw + tw * 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
|
||||
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
|
||||
func log_print(message: String):
|
||||
LogManager.log(message, LogManager.CATEGORY_NETWORK)
|
||||
@@ -69,6 +73,23 @@ func _ready():
|
||||
add_child(room_registry)
|
||||
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):
|
||||
# 0 = ENet, 1 = WebRTC, 2 = WebSocket
|
||||
# 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
|
||||
func _on_peer_connected(id: int):
|
||||
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)
|
||||
if not players_info.has(id):
|
||||
# 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
|
||||
"player_names": _generate_player_names(1, id)
|
||||
}
|
||||
|
||||
# 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
|
||||
log_print("NetworkManager: Joiner - host connected via multiplayer.peer_connected, emitting connection_succeeded")
|
||||
connection_succeeded.emit()
|
||||
|
||||
# Emit player connected signal (peer is now available for RPCs)
|
||||
player_connected.emit(id, players_info[id])
|
||||
|
||||
# Update room registry if hosting (player count changed)
|
||||
if is_hosting and room_registry and not room_id.is_empty():
|
||||
var player_count = get_all_player_ids().size()
|
||||
@@ -373,20 +399,22 @@ func _on_peer_connected(id: int):
|
||||
# Called when a peer disconnects
|
||||
func _on_peer_disconnected(id: int):
|
||||
log_print("Peer disconnected: " + str(id))
|
||||
# Get player_info before erasing it
|
||||
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 joiner lost host, trigger server disconnection immediately (no grace)
|
||||
if not is_hosting and id == 1:
|
||||
log_print("NetworkManager: Host (peer ID 1) disconnected, triggering reconnection...")
|
||||
_on_server_disconnected()
|
||||
return
|
||||
|
||||
# Update room registry if hosting (player count changed)
|
||||
# Host: when a joiner disconnects, use grace period so brief drops don't "recreate" them as new
|
||||
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():
|
||||
var player_count = get_all_player_ids().size()
|
||||
var _level = 1
|
||||
@@ -422,6 +450,9 @@ func _on_connection_failed():
|
||||
# Called on client when disconnected from server
|
||||
func _on_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)
|
||||
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,
|
||||
# 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 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)
|
||||
if has_meta("connection_succeeded_timer_set"):
|
||||
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")
|
||||
current_tree.change_scene_to_packed(game_world_scene)
|
||||
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
|
||||
@@ -747,7 +787,17 @@ func get_room_id() -> String:
|
||||
return room_id
|
||||
|
||||
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:
|
||||
reconnection_timer -= delta
|
||||
if reconnection_timer <= 0.0:
|
||||
@@ -827,6 +877,43 @@ func _generate_room_id() -> String:
|
||||
code += chars[randi() % chars.length()]
|
||||
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:
|
||||
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_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
|
||||
var can_attack: bool = true
|
||||
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_look_out = $SfxLookOut
|
||||
@onready var sfx_ahaa = $SfxAhaa
|
||||
@onready var sfx_secret_found = $SfxSecretFound
|
||||
|
||||
# Alert indicator (exclamation mark)
|
||||
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)
|
||||
|
||||
# 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()
|
||||
params.shape = placed_shape
|
||||
params.transform = placed_shape_transform
|
||||
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
|
||||
# The object might still be in the scene tree with collision disabled, so we need to exclude it
|
||||
var exclude_list = [self]
|
||||
if placed_obj and is_instance_valid(placed_obj):
|
||||
exclude_list.append(placed_obj)
|
||||
# CRITICAL: Exclude using RIDs so the physics engine actually excludes them (Node refs may not work)
|
||||
var exclude_list: Array[RID] = [get_rid()]
|
||||
if placed_obj and is_instance_valid(placed_obj) and placed_obj is CollisionObject2D:
|
||||
exclude_list.append(placed_obj.get_rid())
|
||||
params.exclude = exclude_list
|
||||
|
||||
# Test the actual collision shape at the placement position
|
||||
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
|
||||
return hits.size() == 0
|
||||
|
||||
@@ -1964,6 +1960,8 @@ func _physics_process(delta):
|
||||
const MANA_REGEN_RATE = 2.0 # mana per second
|
||||
if character_stats.mp < character_stats.maxmp:
|
||||
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
|
||||
if is_charging_spell:
|
||||
@@ -2496,8 +2494,8 @@ func _handle_input():
|
||||
if new_direction != current_direction:
|
||||
current_direction = new_direction
|
||||
_update_cone_light_rotation()
|
||||
elif is_pushing:
|
||||
# Keep locked direction when pushing
|
||||
elif is_pushing or (held_object and not is_lifting):
|
||||
# Keep direction from when grab started (don't turn to face the object)
|
||||
if push_direction_locked != current_direction:
|
||||
current_direction = push_direction_locked as Direction
|
||||
_update_cone_light_rotation()
|
||||
@@ -2539,9 +2537,10 @@ func _handle_input():
|
||||
elif is_lifting:
|
||||
if current_animation != "LIFT" and current_animation != "IDLE_HOLD":
|
||||
_set_animation("IDLE_HOLD")
|
||||
elif is_pushing:
|
||||
_set_animation("IDLE_PUSH")
|
||||
# Keep locked direction when pushing
|
||||
elif is_pushing or (held_object and not is_lifting):
|
||||
if is_pushing:
|
||||
_set_animation("IDLE_PUSH")
|
||||
# Keep direction from when grab started
|
||||
if push_direction_locked != current_direction:
|
||||
current_direction = push_direction_locked as Direction
|
||||
_update_cone_light_rotation()
|
||||
@@ -2598,8 +2597,8 @@ func _handle_input():
|
||||
if character_stats and character_stats.is_over_encumbered():
|
||||
current_speed = base_speed * 0.25
|
||||
|
||||
# Lock movement if movement_lock_timer is active or reviving a corpse
|
||||
if movement_lock_timer > 0.0 or is_reviving:
|
||||
# Lock movement if movement_lock_timer is active, reviving a corpse, or netted by web
|
||||
if movement_lock_timer > 0.0 or is_reviving or netted_by_web:
|
||||
velocity = Vector2.ZERO
|
||||
else:
|
||||
velocity = input_vector * current_speed
|
||||
@@ -2623,6 +2622,32 @@ func _handle_walking_sfx():
|
||||
if sfx_walk and sfx_walk.playing:
|
||||
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():
|
||||
var grab_button_down = false
|
||||
var grab_just_pressed = false
|
||||
@@ -3192,7 +3217,7 @@ func _handle_interactions():
|
||||
|
||||
# Handle bow charging
|
||||
if has_bow_and_arrows and not is_lifting and not is_pushing:
|
||||
if attack_just_pressed and can_attack and not is_charging_bow 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:
|
||||
$SfxBuckleBow.play()
|
||||
# Start charging bow
|
||||
@@ -3260,7 +3285,7 @@ func _handle_interactions():
|
||||
|
||||
# Normal attack (non-bow or no arrows)
|
||||
# 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:
|
||||
# Attack while lifting -> throw immediately in facing direction
|
||||
_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)
|
||||
grab_distance = global_position.distance_to(closest_body.global_position)
|
||||
|
||||
# Calculate push axis from grab direction (but don't move the object yet)
|
||||
var grab_direction = grab_offset.normalized()
|
||||
# Use player's current facing when grab started (do not turn to face the object)
|
||||
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:
|
||||
grab_direction = last_movement_direction
|
||||
grab_direction = Vector2.DOWN
|
||||
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
|
||||
# But keep collision with walls (layer 7) enabled for pushing
|
||||
@@ -3503,15 +3529,11 @@ func _start_pushing():
|
||||
is_pushing = true
|
||||
is_lifting = false
|
||||
|
||||
# Lock to the direction we're facing when we start pushing
|
||||
var initial_direction = grab_offset.normalized()
|
||||
# Keep the direction we had when we started the grab (do not face the object)
|
||||
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:
|
||||
initial_direction = last_movement_direction.normalized()
|
||||
|
||||
# Snap to one of 8 directions
|
||||
initial_direction = Vector2.DOWN
|
||||
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)
|
||||
facing_direction_vector = push_axis.normalized()
|
||||
|
||||
@@ -4045,7 +4067,7 @@ func _place_down_object():
|
||||
print("Placed down ", placed_obj.name, " at ", place_pos)
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
if fallout_state:
|
||||
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
|
||||
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():
|
||||
_rpc_to_ready_peers("_sync_damage", [actual_damage, attacker_position])
|
||||
|
||||
# Check if dead - but wait for damage animation to play first
|
||||
# Use small epsilon to handle floating point precision issues (HP might be 0.0000001 instead of exactly 0.0)
|
||||
# Check if dead - below 1 HP must always trigger death (trap, etc.)
|
||||
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:
|
||||
character_stats.hp = 0.0 # Clamp to exactly 0
|
||||
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
|
||||
# Wait a bit for damage animation and knockback to show
|
||||
await get_tree().create_timer(0.3).timeout
|
||||
_die()
|
||||
if is_instance_valid(self) and is_dead:
|
||||
_die()
|
||||
|
||||
func _die():
|
||||
# Already processing death - prevent multiple concurrent death sequences
|
||||
@@ -7575,6 +7604,10 @@ func _on_level_up_stats(stats_increased: Array):
|
||||
if not character_stats:
|
||||
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
|
||||
var stat_display_names = {
|
||||
"str": "STR",
|
||||
@@ -7651,6 +7684,17 @@ func rpc_apply_corpse_knockback(dir_x: float, dir_y: float, force: float):
|
||||
is_knocked_back = true
|
||||
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")
|
||||
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
|
||||
@@ -7796,6 +7840,26 @@ func _sync_trap_detected_alert():
|
||||
if sfx_look_out:
|
||||
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")
|
||||
func _sync_exit_found_alert():
|
||||
# Sync exit found alert to all clients
|
||||
|
||||
@@ -15,15 +15,19 @@ var elapsed_time: float = 0.0
|
||||
var distance_traveled: float = 0.0
|
||||
var player_owner: Node = null
|
||||
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 hit_area = $Area2D
|
||||
|
||||
func _ready():
|
||||
$SfxSwosh.play()
|
||||
# Connect area signals (only if not already connected)
|
||||
if hit_area and not hit_area.body_entered.is_connected(_on_body_entered):
|
||||
hit_area.body_entered.connect(_on_body_entered)
|
||||
if hit_area:
|
||||
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
|
||||
|
||||
func setup(direction: Vector2, owner_player: Node, damage_value: float = 20.0):
|
||||
travel_direction = direction.normalized()
|
||||
@@ -51,6 +55,13 @@ func _physics_process(delta):
|
||||
# Decelerate quickly
|
||||
current_speed -= deceleration * delta
|
||||
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
|
||||
var movement = travel_direction * current_speed * delta
|
||||
@@ -62,6 +73,11 @@ func _physics_process(delta):
|
||||
if sprite:
|
||||
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):
|
||||
# Don't hit the owner
|
||||
if body == player_owner:
|
||||
@@ -134,6 +150,9 @@ func _on_body_entered(body):
|
||||
else:
|
||||
# Client sends RPC to server
|
||||
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:
|
||||
# Fallback: try direct call (may fail if node path doesn't match)
|
||||
var enemy_peer_id = body.get_multiplayer_authority()
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -7,11 +7,11 @@ extends Node2D
|
||||
@onready var detection_area = $DetectionArea
|
||||
|
||||
# Trap state
|
||||
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_active: bool = false # True when trap is currently triggering
|
||||
var has_cooldown: bool = false # Some traps can reset
|
||||
var cooldown_time: float = 5.0 # Time until trap can re-activate
|
||||
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_active: bool = false # True when trap is currently triggering
|
||||
var has_cooldown: bool = false # Some traps can reset
|
||||
var cooldown_time: float = 5.0 # Time until trap can re-activate
|
||||
var cooldown_timer: float = 0.0
|
||||
|
||||
# Trap properties
|
||||
@@ -44,7 +44,7 @@ func _ready() -> void:
|
||||
if index == 0:
|
||||
sprite.texture = load("res://assets/gfx/traps/Floor_Lance.png")
|
||||
trap_type = "Floor_Lance"
|
||||
has_cooldown = true # Lance traps can reset
|
||||
has_cooldown = true # Lance traps can reset
|
||||
|
||||
# Start hidden (invisible until detected)
|
||||
sprite.modulate.a = 0.0
|
||||
@@ -91,14 +91,14 @@ func _on_detection_area_body_entered(body: Node) -> void:
|
||||
return
|
||||
|
||||
if is_detected or is_disarmed:
|
||||
return # Already detected or disarmed
|
||||
return # Already detected or disarmed
|
||||
|
||||
# Get player peer ID
|
||||
var peer_id = body.get_multiplayer_authority()
|
||||
|
||||
# Check if this player has already attempted detection
|
||||
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
|
||||
player_detection_attempts[peer_id] = true
|
||||
@@ -108,7 +108,7 @@ func _on_detection_area_body_entered(body: Node) -> void:
|
||||
_roll_perception_check(body)
|
||||
|
||||
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:
|
||||
# Roll perception check for player
|
||||
@@ -119,8 +119,8 @@ func _roll_perception_check(player: Node) -> void:
|
||||
|
||||
# Perception roll: d20 + PER modifier
|
||||
# Target DC: 15 (medium difficulty)
|
||||
var roll = randi() % 20 + 1 # 1d20
|
||||
var total = roll + int(per_stat / 2) - 5 # PER modifier: (PER - 10) / 2
|
||||
var roll = randi() % 20 + 1 # 1d20
|
||||
var total = roll + int(per_stat / 2) - 5 # PER modifier: (PER - 10) / 2
|
||||
var dc = 15
|
||||
|
||||
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"):
|
||||
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)
|
||||
# CRITICAL: Validate trap is still valid before sending RPC
|
||||
# Use GameWorld RPC to avoid node path issues
|
||||
if multiplayer.has_multiplayer_peer() and is_inside_tree() and is_instance_valid(self):
|
||||
if multiplayer.is_server():
|
||||
# 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"):
|
||||
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)
|
||||
|
||||
@@ -200,7 +205,7 @@ func _on_disarm_area_body_exited(body: Node) -> void:
|
||||
func _show_disarm_text(_player: Node) -> void:
|
||||
# Create "DISARM" label above trap
|
||||
if disarm_label:
|
||||
return # Already showing
|
||||
return # Already showing
|
||||
|
||||
disarm_label = Label.new()
|
||||
disarm_label.text = "DISARM"
|
||||
@@ -212,6 +217,13 @@ func _show_disarm_text(_player: Node) -> void:
|
||||
disarm_label.z_index = 100
|
||||
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:
|
||||
if disarm_label:
|
||||
disarm_label.queue_free()
|
||||
@@ -246,6 +258,12 @@ func _complete_disarm() -> void:
|
||||
if $SfxDisarming.playing:
|
||||
$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(null)
|
||||
|
||||
@@ -267,7 +285,7 @@ func _complete_disarm() -> void:
|
||||
# Grant EXP to all players for disarming trap (only on server)
|
||||
# CRITICAL: Only server should grant EXP to avoid duplicates
|
||||
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 valid_players = []
|
||||
for player in all_players:
|
||||
@@ -303,7 +321,7 @@ func _complete_disarm() -> void:
|
||||
if multiplayer.is_server():
|
||||
# Host disarmed: broadcast to clients
|
||||
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:
|
||||
# Joiner disarmed: request host to apply locally and broadcast to all
|
||||
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():
|
||||
return
|
||||
is_disarmed = true
|
||||
_remove_detected_effect()
|
||||
if sprite:
|
||||
sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
|
||||
if activation_area:
|
||||
@@ -335,7 +354,7 @@ func _show_exp_number(amount: float, exp_pos: Vector2):
|
||||
|
||||
# Set text and color for EXP (green)
|
||||
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
|
||||
|
||||
# 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")
|
||||
if entities_node:
|
||||
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:
|
||||
get_tree().current_scene.add_child(exp_label)
|
||||
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
|
||||
|
||||
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:
|
||||
return # Still on cooldown
|
||||
return # Still on cooldown
|
||||
|
||||
# Trigger trap
|
||||
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
|
||||
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"):
|
||||
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_trap_damage(body)
|
||||
@@ -414,7 +433,7 @@ func _deal_trap_damage(player: Node) -> void:
|
||||
|
||||
# Luck-based avoidance check
|
||||
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()
|
||||
|
||||
if avoid_roll < avoid_chance:
|
||||
@@ -424,10 +443,16 @@ func _deal_trap_damage(player: Node) -> void:
|
||||
_show_floating_text("AVOIDED", Color.GREEN)
|
||||
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)
|
||||
|
||||
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")
|
||||
|
||||