started working on fog darkness
This commit is contained in:
BIN
src/assets/audio/sfx/enemies/bat/bat_chirp.mp3
Normal file
BIN
src/assets/audio/sfx/enemies/bat/bat_chirp.mp3
Normal file
Binary file not shown.
19
src/assets/audio/sfx/enemies/bat/bat_chirp.mp3.import
Normal file
19
src/assets/audio/sfx/enemies/bat/bat_chirp.mp3.import
Normal file
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="mp3"
|
||||
type="AudioStreamMP3"
|
||||
uid="uid://bessb1ga6hwy4"
|
||||
path="res://.godot/imported/bat_chirp.mp3-0252020fc7122a4f730bfae189f18695.mp3str"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/sfx/enemies/bat/bat_chirp.mp3"
|
||||
dest_files=["res://.godot/imported/bat_chirp.mp3-0252020fc7122a4f730bfae189f18695.mp3str"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
BIN
src/assets/audio/sfx/enemies/bat/bat_flap1.mp3
Normal file
BIN
src/assets/audio/sfx/enemies/bat/bat_flap1.mp3
Normal file
Binary file not shown.
19
src/assets/audio/sfx/enemies/bat/bat_flap1.mp3.import
Normal file
19
src/assets/audio/sfx/enemies/bat/bat_flap1.mp3.import
Normal file
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="mp3"
|
||||
type="AudioStreamMP3"
|
||||
uid="uid://dcn14oarhvvlk"
|
||||
path="res://.godot/imported/bat_flap1.mp3-021fbc685ca206c0a27fd81b7f631f85.mp3str"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/sfx/enemies/bat/bat_flap1.mp3"
|
||||
dest_files=["res://.godot/imported/bat_flap1.mp3-021fbc685ca206c0a27fd81b7f631f85.mp3str"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
BIN
src/assets/audio/sfx/enemies/bat/bat_flap2.mp3
Normal file
BIN
src/assets/audio/sfx/enemies/bat/bat_flap2.mp3
Normal file
Binary file not shown.
19
src/assets/audio/sfx/enemies/bat/bat_flap2.mp3.import
Normal file
19
src/assets/audio/sfx/enemies/bat/bat_flap2.mp3.import
Normal file
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="mp3"
|
||||
type="AudioStreamMP3"
|
||||
uid="uid://chm1mvjrrj3vj"
|
||||
path="res://.godot/imported/bat_flap2.mp3-927285e69a6bfa85963ed2e3cdd7cd52.mp3str"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/sfx/enemies/bat/bat_flap2.mp3"
|
||||
dest_files=["res://.godot/imported/bat_flap2.mp3-927285e69a6bfa85963ed2e3cdd7cd52.mp3str"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
@@ -2,30 +2,145 @@
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://c4ee36hr5f766" path="res://assets/gfx/RPG DUNGEON VOL 3.png" id="1_e3020"]
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_w8s50"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_x8b6b"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_t46y5"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_hu0mk"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_7y1f8"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_okmkx"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_tiog7"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_1w2p2"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_hmgok"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_cmw36"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_bqa6v"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_c2t7l"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_br5gx"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_w7dhj"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_1hffl"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_w3suo"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_ugmkx"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_04rhq"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_lskmx"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_d50pg"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_7irfv"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_sjxac"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_c22jm"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_rdtaq"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_gdg35"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_xa7bm"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_1nbxl"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_wsd2f"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_8usvn"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_y41hv"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_1exwc"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_w72ja"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_whcyq"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_n2gtd"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_b0pj5"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="OccluderPolygon2D" id="OccluderPolygon2D_4bwa3"]
|
||||
polygon = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
|
||||
[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_1bvp3"]
|
||||
texture = ExtResource("1_e3020")
|
||||
separation = Vector2i(1, 1)
|
||||
0:0/0 = 0
|
||||
0:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
1:0/0 = 0
|
||||
1:0/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_tiog7")
|
||||
1:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
2:0/0 = 0
|
||||
2:0/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_bqa6v")
|
||||
2:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
3:0/0 = 0
|
||||
3:0/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_04rhq")
|
||||
3:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
4:0/0 = 0
|
||||
4:0/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_rdtaq")
|
||||
4:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
5:0/0 = 0
|
||||
6:0/0 = 0
|
||||
7:0/0 = 0
|
||||
7:0/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_whcyq")
|
||||
7:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
8:0/0 = 0
|
||||
9:0/0 = 0
|
||||
9:0/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_b0pj5")
|
||||
9:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
10:0/0 = 0
|
||||
11:0/0 = 0
|
||||
11:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
0:1/0 = 0
|
||||
0:1/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_w8s50")
|
||||
0:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
1:1/0 = 0
|
||||
1:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
@@ -34,6 +149,7 @@ separation = Vector2i(1, 1)
|
||||
3:1/0 = 0
|
||||
3:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
4:1/0 = 0
|
||||
4:1/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_gdg35")
|
||||
4:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
5:1/0 = 0
|
||||
6:1/0 = 0
|
||||
@@ -46,15 +162,19 @@ separation = Vector2i(1, 1)
|
||||
11:1/0 = 0
|
||||
12:1/0 = 0
|
||||
0:2/0 = 0
|
||||
0:2/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_x8b6b")
|
||||
0:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
1:2/0 = 0
|
||||
1:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
2:2/0 = 0
|
||||
2:2/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_c2t7l")
|
||||
3:2/0 = 0
|
||||
3:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
4:2/0 = 0
|
||||
4:2/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_xa7bm")
|
||||
4:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
5:2/0 = 0
|
||||
5:2/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_1exwc")
|
||||
5:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
6:2/0 = 0
|
||||
6:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
@@ -64,9 +184,11 @@ separation = Vector2i(1, 1)
|
||||
10:2/0 = 0
|
||||
10:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
11:2/0 = 0
|
||||
11:2/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_7y1f8")
|
||||
11:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
12:2/0 = 0
|
||||
0:3/0 = 0
|
||||
0:3/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_t46y5")
|
||||
0:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
1:3/0 = 0
|
||||
1:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
@@ -75,6 +197,7 @@ separation = Vector2i(1, 1)
|
||||
3:3/0 = 0
|
||||
3:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
4:3/0 = 0
|
||||
4:3/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_1nbxl")
|
||||
4:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
5:3/0 = 0
|
||||
6:3/0 = 0
|
||||
@@ -83,16 +206,22 @@ separation = Vector2i(1, 1)
|
||||
11:3/0 = 0
|
||||
12:3/0 = 0
|
||||
0:4/0 = 0
|
||||
0:4/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_hu0mk")
|
||||
0:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
1:4/0 = 0
|
||||
1:4/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_1w2p2")
|
||||
1:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
2:4/0 = 0
|
||||
2:4/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_br5gx")
|
||||
2:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
3:4/0 = 0
|
||||
3:4/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_lskmx")
|
||||
3:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
4:4/0 = 0
|
||||
4:4/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_wsd2f")
|
||||
4:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
5:4/0 = 0
|
||||
5:4/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_w72ja")
|
||||
5:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
6:4/0 = 0
|
||||
6:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
@@ -100,6 +229,7 @@ separation = Vector2i(1, 1)
|
||||
10:4/0 = 0
|
||||
10:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
11:4/0 = 0
|
||||
11:4/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_okmkx")
|
||||
11:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
12:4/0 = 0
|
||||
0:5/0 = 0
|
||||
@@ -124,8 +254,10 @@ separation = Vector2i(1, 1)
|
||||
1:6/0 = 0
|
||||
1:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
2:6/0 = 0
|
||||
2:6/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_w7dhj")
|
||||
2:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
3:6/0 = 0
|
||||
3:6/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_d50pg")
|
||||
3:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
4:6/0 = 0
|
||||
4:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
@@ -133,21 +265,27 @@ separation = Vector2i(1, 1)
|
||||
5:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
6:6/0 = 0
|
||||
7:6/0 = 0
|
||||
7:6/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_n2gtd")
|
||||
7:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
8:6/0 = 0
|
||||
9:6/0 = 0
|
||||
9:6/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_4bwa3")
|
||||
9:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
10:6/0 = 0
|
||||
11:6/0 = 0
|
||||
12:6/0 = 0
|
||||
0:7/0 = 0
|
||||
1:7/0 = 0
|
||||
1:7/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_hmgok")
|
||||
1:7/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
2:7/0 = 0
|
||||
2:7/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_1hffl")
|
||||
2:7/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
3:7/0 = 0
|
||||
3:7/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_7irfv")
|
||||
3:7/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
4:7/0 = 0
|
||||
4:7/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_8usvn")
|
||||
4:7/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
5:7/0 = 0
|
||||
6:7/0 = 0
|
||||
@@ -165,12 +303,16 @@ separation = Vector2i(1, 1)
|
||||
18:7/0 = 0
|
||||
19:7/0 = 0
|
||||
1:8/0 = 0
|
||||
1:8/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_cmw36")
|
||||
1:8/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
2:8/0 = 0
|
||||
2:8/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_w3suo")
|
||||
2:8/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
3:8/0 = 0
|
||||
3:8/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_sjxac")
|
||||
3:8/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
4:8/0 = 0
|
||||
4:8/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_y41hv")
|
||||
4:8/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
5:8/0 = 0
|
||||
8:8/0 = 0
|
||||
@@ -190,8 +332,10 @@ separation = Vector2i(1, 1)
|
||||
1:9/0 = 0
|
||||
1:9/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
2:9/0 = 0
|
||||
2:9/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_ugmkx")
|
||||
2:9/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
3:9/0 = 0
|
||||
3:9/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_c22jm")
|
||||
3:9/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
4:9/0 = 0
|
||||
4:9/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||
@@ -351,6 +495,7 @@ separation = Vector2i(1, 1)
|
||||
13:15/0 = 0
|
||||
|
||||
[resource]
|
||||
occlusion_layer_0/light_mask = 1
|
||||
physics_layer_0/collision_layer = 64
|
||||
physics_layer_0/collision_mask = 0
|
||||
custom_data_layer_0/name = "terrain"
|
||||
|
||||
@@ -17,12 +17,19 @@ run/max_fps=60
|
||||
boot_splash/bg_color=Color(0.09375, 0.09375, 0.09375, 1)
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[audio]
|
||||
|
||||
buses/default_bus_layout="uid://psistrevppd1"
|
||||
|
||||
[autoload]
|
||||
|
||||
NetworkManager="*res://scripts/network_manager.gd"
|
||||
|
||||
[display]
|
||||
|
||||
window/size/viewport_width=1280
|
||||
window/size/viewport_height=720
|
||||
window/stretch/mode="canvas_items"
|
||||
window/stretch/scale_mode="integer"
|
||||
|
||||
[input]
|
||||
|
||||
@@ -2,10 +2,25 @@
|
||||
|
||||
[ext_resource type="Script" uid="uid://c0wywibyp77c" path="res://scripts/enemy_bat.gd" id="1"]
|
||||
[ext_resource type="Texture2D" uid="uid://bipt58n2ggxu5" path="res://assets/gfx/enemies/Bat.png" id="2"]
|
||||
[ext_resource type="AudioStream" uid="uid://dcn14oarhvvlk" path="res://assets/audio/sfx/enemies/bat/bat_flap1.mp3" id="3_xbmos"]
|
||||
[ext_resource type="AudioStream" uid="uid://chm1mvjrrj3vj" path="res://assets/audio/sfx/enemies/bat/bat_flap2.mp3" id="4_veom1"]
|
||||
[ext_resource type="AudioStream" uid="uid://bessb1ga6hwy4" path="res://assets/audio/sfx/enemies/bat/bat_chirp.mp3" id="5_yoqnl"]
|
||||
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_bat"]
|
||||
radius = 6.0
|
||||
|
||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_o7h1p"]
|
||||
playback_mode = 1
|
||||
random_pitch = 1.0178324
|
||||
streams_count = 2
|
||||
stream_0/stream = ExtResource("3_xbmos")
|
||||
stream_1/stream = ExtResource("4_veom1")
|
||||
|
||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_6m63e"]
|
||||
random_pitch = 1.0118532
|
||||
streams_count = 1
|
||||
stream_0/stream = ExtResource("5_yoqnl")
|
||||
|
||||
[node name="EnemyBat" type="CharacterBody2D" unique_id=909833829]
|
||||
collision_layer = 2
|
||||
script = ExtResource("1")
|
||||
@@ -29,3 +44,15 @@ frame = 2
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=897277405]
|
||||
shape = SubResource("CircleShape2D_bat")
|
||||
|
||||
[node name="BatFlapSfx" type="AudioStreamPlayer2D" parent="." unique_id=2095836633]
|
||||
stream = SubResource("AudioStreamRandomizer_o7h1p")
|
||||
max_distance = 1160.0
|
||||
attenuation = 7.999991
|
||||
panning_strength = 1.09
|
||||
|
||||
[node name="BatChirpSfx" type="AudioStreamPlayer2D" parent="." unique_id=288445950]
|
||||
stream = SubResource("AudioStreamRandomizer_6m63e")
|
||||
max_distance = 1107.0
|
||||
attenuation = 9.18958
|
||||
panning_strength = 1.05
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[ext_resource type="PackedScene" uid="uid://cxfvw8y7jqn2p" path="res://scenes/player.tscn" id="2"]
|
||||
[ext_resource type="Script" uid="uid://db58xcyo4cjk" path="res://scripts/game_world.gd" id="4"]
|
||||
[ext_resource type="Script" uid="uid://wff5063ctp7g" path="res://scripts/debug_overlay.gd" id="5"]
|
||||
[ext_resource type="Script" path="res://scripts/room_lighting_system.gd" id="6"]
|
||||
[ext_resource type="AudioStream" uid="uid://dthr2w8x0cj6v" path="res://assets/audio/sfx/ambience/wind-castle-loop.wav.mp3" id="6_6c6v5"]
|
||||
[ext_resource type="TileSet" uid="uid://dqem5tbvooxrg" path="res://assets/gfx/RPG DUNGEON VOL 3.tres" id="9"]
|
||||
|
||||
@@ -37,7 +38,10 @@ script = ExtResource("5")
|
||||
[node name="CanvasModulate" type="CanvasModulate" parent="." unique_id=948490815]
|
||||
light_mask = 1048575
|
||||
visibility_layer = 1048575
|
||||
color = Color(0.671875, 0.671875, 0.671875, 1)
|
||||
color = Color(0.4140625, 0.4140625, 0.4140625, 1)
|
||||
|
||||
[node name="RoomLightingSystem" type="Node2D" parent="." unique_id=1234567893]
|
||||
script = ExtResource("6")
|
||||
|
||||
[node name="AudioStreamPlayer2D" type="AudioStreamPlayer2D" parent="." unique_id=1141138343]
|
||||
stream = ExtResource("6_6c6v5")
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
[gd_scene format=3 uid="uid://cxs0ybxk2blth"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://vm6intetgl40" path="res://scripts/inventory_ui.gd" id="1_inventory_ui"]
|
||||
[ext_resource type="FontFile" uid="uid://bajcvmidrnc33" path="res://assets/fonts/standard_font.png" id="2_ylkvr"]
|
||||
[ext_resource type="AudioStream" uid="uid://b5xbv7s85sy5o" path="res://assets/audio/sfx/pickups/potion.mp3" id="3_eicjl"]
|
||||
[ext_resource type="AudioStream" uid="uid://cnb376ah43nqi" path="res://assets/audio/sfx/pickups/bite-food-01.mp3" id="4_uwj4j"]
|
||||
[ext_resource type="AudioStream" uid="uid://bbnby1sso3f4v" path="res://assets/audio/sfx/pickups/bite-food-02.mp3" id="5_1dxi5"]
|
||||
[ext_resource type="AudioStream" uid="uid://umoxmryvbm01" path="res://assets/audio/sfx/cloth/leather_cloth_01.wav.mp3" id="6_dqfnd"]
|
||||
[ext_resource type="AudioStream" uid="uid://djw6c5rb4mm60" path="res://assets/audio/sfx/cloth/leather_cloth_02.wav.mp3" id="7_ngbl7"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_selection"]
|
||||
bg_color = Color(0, 0, 0, 0)
|
||||
@@ -10,11 +16,24 @@ border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(1, 1, 0, 1)
|
||||
|
||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_8wyaw"]
|
||||
playback_mode = 1
|
||||
random_pitch = 1.0059091
|
||||
streams_count = 2
|
||||
stream_0/stream = ExtResource("4_uwj4j")
|
||||
stream_1/stream = ExtResource("5_1dxi5")
|
||||
|
||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_cwdri"]
|
||||
streams_count = 2
|
||||
stream_0/stream = ExtResource("6_dqfnd")
|
||||
stream_1/stream = ExtResource("7_ngbl7")
|
||||
|
||||
[node name="InventoryUI" type="CanvasLayer" unique_id=-1294967296]
|
||||
layer = 150
|
||||
script = ExtResource("1_inventory_ui")
|
||||
|
||||
[node name="InventoryContainer" type="Control" parent="." unique_id=-294967296]
|
||||
visible = false
|
||||
layout_mode = 3
|
||||
anchors_preset = 3
|
||||
anchor_left = 1.0
|
||||
@@ -44,29 +63,39 @@ size_flags_vertical = 3
|
||||
mouse_filter = 1
|
||||
color = Color(0.1, 0.1, 0.1, 0.85)
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="InventoryContainer/MarginContainer" unique_id=1015792177]
|
||||
[node name="MarginContainer" type="MarginContainer" parent="InventoryContainer/MarginContainer" unique_id=828124619]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 16
|
||||
theme_override_constants/margin_top = 16
|
||||
theme_override_constants/margin_right = 16
|
||||
theme_override_constants/margin_bottom = 16
|
||||
|
||||
[node name="HBox" type="HBoxContainer" parent="InventoryContainer/MarginContainer/VBoxContainer" unique_id=1705032704]
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="InventoryContainer/MarginContainer/MarginContainer" unique_id=1015792177]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="HBox" type="HBoxContainer" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer" unique_id=1705032704]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="StatsPanel" type="VBoxContainer" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox" unique_id=-1589934592]
|
||||
[node name="StatsPanel" type="VBoxContainer" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox" unique_id=-1589934592]
|
||||
custom_minimum_size = Vector2(200, 0)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 5
|
||||
|
||||
[node name="StatsLabel" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel" unique_id=-589934592]
|
||||
[node name="StatsLabel" type="Label" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel" unique_id=-589934592]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 14
|
||||
theme_override_fonts/font = ExtResource("2_ylkvr")
|
||||
theme_override_font_sizes/font_size = 16
|
||||
text = "Stats"
|
||||
|
||||
[node name="StatsHBox" type="HBoxContainer" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel" unique_id=410065408]
|
||||
[node name="StatsHBox" type="HBoxContainer" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel" unique_id=410065408]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 5
|
||||
|
||||
[node name="LabelBaseStats" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox" unique_id=1410065408]
|
||||
[node name="LabelBaseStats" type="Label" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox" unique_id=1410065408]
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("2_ylkvr")
|
||||
theme_override_font_sizes/font_size = 10
|
||||
text = "Level
|
||||
|
||||
@@ -80,8 +109,9 @@ INT
|
||||
WIS
|
||||
LCK"
|
||||
|
||||
[node name="LabelBaseStatsValue" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox" unique_id=-1884901888]
|
||||
[node name="LabelBaseStatsValue" type="Label" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox" unique_id=-1884901888]
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("2_ylkvr")
|
||||
theme_override_font_sizes/font_size = 10
|
||||
text = "1
|
||||
|
||||
@@ -96,8 +126,9 @@ text = "1
|
||||
10"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="LabelDerivedStats" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox" unique_id=-884901888]
|
||||
[node name="LabelDerivedStats" type="Label" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox" unique_id=-884901888]
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("2_ylkvr")
|
||||
theme_override_font_sizes/font_size = 10
|
||||
text = "XP
|
||||
Coin
|
||||
@@ -112,8 +143,9 @@ Sight
|
||||
SpellAmp
|
||||
Crit%"
|
||||
|
||||
[node name="LabelDerivedStatsValue" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox" unique_id=115098112]
|
||||
[node name="LabelDerivedStatsValue" type="Label" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox" unique_id=115098112]
|
||||
layout_mode = 2
|
||||
theme_override_fonts/font = ExtResource("2_ylkvr")
|
||||
theme_override_font_sizes/font_size = 10
|
||||
text = "0/100
|
||||
0
|
||||
@@ -129,60 +161,73 @@ text = "0/100
|
||||
12.0%"
|
||||
horizontal_alignment = 2
|
||||
|
||||
[node name="InventoryPanel" type="VBoxContainer" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox" unique_id=1115098112]
|
||||
[node name="InventoryPanel" type="VBoxContainer" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox" unique_id=1115098112]
|
||||
custom_minimum_size = Vector2(400, 0)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 5
|
||||
|
||||
[node name="EquipmentLabel" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=2115098112]
|
||||
[node name="EquipmentLabel" type="Label" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=2115098112]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 14
|
||||
theme_override_fonts/font = ExtResource("2_ylkvr")
|
||||
theme_override_font_sizes/font_size = 16
|
||||
text = "Equipment"
|
||||
|
||||
[node name="EquipmentSpacer" type="Control" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=3000000001]
|
||||
[node name="EquipmentSpacer" type="Control" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=-1294967295]
|
||||
custom_minimum_size = Vector2(0, 8)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="EquipmentPanel" type="GridContainer" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=-1179869184]
|
||||
[node name="EquipmentPanel" type="GridContainer" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=-1179869184]
|
||||
layout_mode = 2
|
||||
theme_override_constants/h_separation = 15
|
||||
theme_override_constants/v_separation = 15
|
||||
columns = 3
|
||||
|
||||
[node name="EquipmentBottomSpacer" type="Control" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=3000000002]
|
||||
[node name="EquipmentBottomSpacer" type="Control" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=-1294967294]
|
||||
custom_minimum_size = Vector2(0, 8)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="InventoryLabel" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=-179869184]
|
||||
[node name="InventoryLabel" type="Label" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=-179869184]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 14
|
||||
theme_override_fonts/font = ExtResource("2_ylkvr")
|
||||
theme_override_font_sizes/font_size = 16
|
||||
text = "Inventory"
|
||||
|
||||
[node name="InventoryScroll" type="ScrollContainer" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=820130816]
|
||||
[node name="InventoryScroll" type="ScrollContainer" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel" unique_id=820130816]
|
||||
custom_minimum_size = Vector2(380, 120)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="InventoryVBox" type="VBoxContainer" parent="InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll" unique_id=1820130816]
|
||||
[node name="InventoryVBox" type="VBoxContainer" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll" unique_id=1820130816]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/separation = -4
|
||||
|
||||
[node name="SelectionRectangle" type="Panel" parent="InventoryContainer/MarginContainer/VBoxContainer" unique_id=-1474836480]
|
||||
[node name="SelectionRectangle" type="Panel" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer" unique_id=-1474836480]
|
||||
z_index = 100
|
||||
custom_minimum_size = Vector2(38, 38)
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_selection")
|
||||
|
||||
[node name="InfoPanel" type="VBoxContainer" parent="InventoryContainer/MarginContainer/VBoxContainer" unique_id=-474836480]
|
||||
[node name="InfoPanel" type="VBoxContainer" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer" unique_id=-474836480]
|
||||
custom_minimum_size = Vector2(0, 80)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="InfoLabel" type="Label" parent="InventoryContainer/MarginContainer/VBoxContainer/InfoPanel" unique_id=525163520]
|
||||
[node name="InfoLabel" type="Label" parent="InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/InfoPanel" unique_id=525163520]
|
||||
custom_minimum_size = Vector2(300, 64)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 10
|
||||
theme_override_fonts/font = ExtResource("2_ylkvr")
|
||||
theme_override_font_sizes/font_size = 8
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 3
|
||||
|
||||
[node name="SfxPotion" type="AudioStreamPlayer2D" parent="." unique_id=370835589]
|
||||
stream = ExtResource("3_eicjl")
|
||||
volume_db = 9.724
|
||||
|
||||
[node name="SfxFood" type="AudioStreamPlayer2D" parent="." unique_id=1396668527]
|
||||
stream = SubResource("AudioStreamRandomizer_8wyaw")
|
||||
|
||||
[node name="SfxArmour" type="AudioStreamPlayer2D" parent="." unique_id=1756569602]
|
||||
stream = SubResource("AudioStreamRandomizer_cwdri")
|
||||
|
||||
@@ -29,15 +29,27 @@
|
||||
[ext_resource type="AudioStream" uid="uid://4vulahdsj4i2" path="res://assets/audio/sfx/swoosh/throw_01.wav.mp3" id="27_31cv2"]
|
||||
[ext_resource type="AudioStream" uid="uid://w6yon88kjfml" path="res://assets/audio/sfx/nickes/lift_sfx.ogg" id="28_pf23h"]
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_wqfne"]
|
||||
colors = PackedColorArray(0, 0, 0, 1, 1, 0.13732082, 0.092538536, 1)
|
||||
|
||||
[sub_resource type="GradientTexture2D" id="GradientTexture2D_wnwbv"]
|
||||
gradient = SubResource("Gradient_wqfne")
|
||||
fill_from = Vector2(0.46153846, 0.87606835)
|
||||
fill_to = Vector2(0.46153846, 0.11965812)
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_jej6c"]
|
||||
offsets = PackedFloat32Array(0.7710843, 0.77710843)
|
||||
colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 0)
|
||||
|
||||
[sub_resource type="GradientTexture2D" id="GradientTexture2D_f1ej7"]
|
||||
gradient = SubResource("Gradient_jej6c")
|
||||
use_hdr = true
|
||||
fill = 1
|
||||
fill_from = Vector2(0.51304346, 0.51304346)
|
||||
fill_to = Vector2(0.9391304, 0.08260869)
|
||||
fill_to = Vector2(0.8974359, 0.08547009)
|
||||
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_pf23h"]
|
||||
radius = 32.0
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_3v2ag"]
|
||||
colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0)
|
||||
@@ -84,10 +96,29 @@ collision_mask = 67
|
||||
motion_mode = 1
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="PointLight2D" type="PointLight2D" parent="." unique_id=1250823818]
|
||||
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=720799975]
|
||||
visible = false
|
||||
position = Vector2(1.499999, -2.0000038)
|
||||
scale = Vector2(1.984375, 2.0937502)
|
||||
texture = SubResource("GradientTexture2D_wnwbv")
|
||||
|
||||
[node name="ConeLight" type="PointLight2D" parent="." unique_id=120780131]
|
||||
blend_mode = 2
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="PointLight2D" type="PointLight2D" parent="." unique_id=1250823818]
|
||||
position = Vector2(-1, 0)
|
||||
blend_mode = 2
|
||||
shadow_enabled = true
|
||||
texture = SubResource("GradientTexture2D_f1ej7")
|
||||
|
||||
[node name="LightCollision" type="Area2D" parent="PointLight2D" unique_id=502090625]
|
||||
collision_layer = 0
|
||||
collision_mask = 16384
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="PointLight2D/LightCollision" unique_id=1350075834]
|
||||
shape = SubResource("CircleShape2D_pf23h")
|
||||
|
||||
[node name="Shadow" type="Sprite2D" parent="." unique_id=937683521]
|
||||
z_index = -1
|
||||
position = Vector2(0, 7)
|
||||
@@ -216,3 +247,11 @@ stream = ExtResource("28_pf23h")
|
||||
max_distance = 1246.0
|
||||
attenuation = 6.964403
|
||||
panning_strength = 1.11
|
||||
|
||||
[node name="DirectionalLight2D" type="DirectionalLight2D" parent="." unique_id=1013099358]
|
||||
visible = false
|
||||
rotation = 3.1869712
|
||||
energy = 0.13
|
||||
blend_mode = 2
|
||||
shadow_enabled = true
|
||||
max_distance = 100.0
|
||||
|
||||
@@ -7,6 +7,8 @@ var is_stuck = false
|
||||
var stick_timer = 0.0
|
||||
|
||||
var initiated_by: Node2D = null
|
||||
var player_owner: Node = null # Like sword_projectile
|
||||
var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup)
|
||||
|
||||
@onready var arrow_area = $ArrowArea # Assuming you have an Area2D node named ArrowArea
|
||||
@onready var shadow = $Shadow # Assuming you have a Shadow node under the CharacterBody2D
|
||||
@@ -14,7 +16,9 @@ var initiated_by: Node2D = null
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
arrow_area.set_deferred("monitoring", true)
|
||||
#arrow_area.body_entered.connect(_on_body_entered)
|
||||
# Connect area signals
|
||||
if arrow_area:
|
||||
arrow_area.body_entered.connect(_on_arrow_area_body_entered)
|
||||
$SfxArrowFire.play()
|
||||
call_deferred("_initialize_arrow")
|
||||
|
||||
@@ -49,10 +53,11 @@ func _initialize_arrow() -> void:
|
||||
# Apply the scaling to the shadow
|
||||
shadow.rotation = -(angle - PI / 2)
|
||||
|
||||
func shoot(shoot_direction: Vector2, start_pos: Vector2) -> void:
|
||||
func shoot(shoot_direction: Vector2, start_pos: Vector2, owner_player: Node = null) -> void:
|
||||
direction = shoot_direction.normalized()
|
||||
global_position = start_pos
|
||||
#position = start_pos
|
||||
player_owner = owner_player
|
||||
initiated_by = owner_player
|
||||
|
||||
# Called every frame. 'delta' is the e lapsed time since the previous frame.
|
||||
func _process(delta: float) -> void:
|
||||
@@ -76,94 +81,99 @@ func _physics_process(_delta: float) -> void:
|
||||
func play_impact():
|
||||
$SfxImpactSound.play()
|
||||
|
||||
# Called when the arrow hits a wall or another object
|
||||
func _on_body_entered(body: Node) -> void:
|
||||
if not is_stuck:
|
||||
if body == initiated_by:
|
||||
return
|
||||
if body is CharacterBody2D and body.stats.is_invulnerable == false and body.stats.hp > 0: # hit an enemy
|
||||
#if body is CharacterBody2D and body.collision_layer & (1 << 8) and body.taking_damage_timer <= 0 and body.stats.hp > 0: # Check if body is enemy (layer 9)
|
||||
|
||||
# Stop the arrow
|
||||
velocity = Vector2.ZERO
|
||||
is_stuck = true
|
||||
stick_timer = 0.0
|
||||
arrow_area.set_deferred("monitoring", false)
|
||||
# Calculate the collision point - move arrow slightly back from its direction
|
||||
var collision_normal = -direction # Opposite of arrow's direction
|
||||
var offset_distance = 8 # Adjust this value based on your collision shape sizes
|
||||
var stick_position = global_position + (collision_normal * offset_distance)
|
||||
|
||||
# Make arrow a child of the enemy to stick to it
|
||||
var global_rot = global_rotation
|
||||
get_parent().call_deferred("remove_child", self)
|
||||
body.call_deferred("add_child", self)
|
||||
self.set_deferred("global_position", stick_position)
|
||||
self.set_deferred("global_rotation", global_rot)
|
||||
#global_rotation = global_rot
|
||||
body.call_deferred("take_damage", self, initiated_by)
|
||||
self.call_deferred("play_impact") # need to play the sound on the next frame, because else it cuts it.
|
||||
|
||||
else:
|
||||
$SfxImpactWall.play()
|
||||
# Stop the arrow
|
||||
velocity = Vector2.ZERO
|
||||
is_stuck = true
|
||||
stick_timer = 0.0
|
||||
arrow_area.set_deferred("monitoring", false)
|
||||
# You can optionally stick the arrow at the collision point if you want:
|
||||
# position = body.position # Uncomment this if you want to "stick" it at the collision point
|
||||
# Additional logic for handling interaction with walls or other objects
|
||||
|
||||
|
||||
func _on_arrow_area_area_entered(area: Area2D) -> void:
|
||||
if not is_stuck:
|
||||
if area.get_parent() == initiated_by:
|
||||
return
|
||||
if area.get_parent() is CharacterBody2D and area.get_parent().stats.is_invulnerable == false and area.get_parent().stats.hp > 0: # hit an enemy
|
||||
#if body is CharacterBody2D and body.collision_layer & (1 << 8) and body.taking_damage_timer <= 0 and body.stats.hp > 0: # Check if body is enemy (layer 9)
|
||||
|
||||
# Stop the arrow
|
||||
velocity = Vector2.ZERO
|
||||
is_stuck = true
|
||||
stick_timer = 0.0
|
||||
arrow_area.set_deferred("monitoring", false)
|
||||
# Calculate the collision point - move arrow slightly back from its direction
|
||||
var collision_normal = -direction # Opposite of arrow's direction
|
||||
var offset_distance = 8 # Adjust this value based on your collision shape sizes
|
||||
var stick_position = global_position + (collision_normal * offset_distance)
|
||||
|
||||
# Make arrow a child of the enemy to stick to it
|
||||
var global_rot = global_rotation
|
||||
get_parent().call_deferred("remove_child", self)
|
||||
area.get_parent().call_deferred("add_child", self)
|
||||
self.set_deferred("global_position", stick_position)
|
||||
self.set_deferred("global_rotation", global_rot)
|
||||
#global_rotation = global_rot
|
||||
area.get_parent().call_deferred("take_damage", self, initiated_by)
|
||||
self.call_deferred("play_impact") # need to play the sound on the next frame, because else it cuts it.
|
||||
|
||||
else:
|
||||
$SfxImpactWall.play()
|
||||
# Stop the arrow
|
||||
velocity = Vector2.ZERO
|
||||
is_stuck = true
|
||||
stick_timer = 0.0
|
||||
arrow_area.set_deferred("monitoring", false)
|
||||
# You can optionally stick the arrow at the collision point if you want:
|
||||
# position = body.position # Uncomment this if you want to "stick" it at the collision point
|
||||
# Additional logic for handling interaction with walls or other objects
|
||||
pass # Replace with function body.
|
||||
|
||||
|
||||
# Called when the arrow hits a wall or another object (like sword_projectile)
|
||||
func _on_arrow_area_body_entered(body: Node2D) -> void:
|
||||
if not is_stuck:
|
||||
if body == initiated_by:
|
||||
if is_stuck:
|
||||
return
|
||||
|
||||
# Don't hit the owner
|
||||
if body == player_owner or body == initiated_by:
|
||||
return
|
||||
|
||||
# Don't hit the same target twice
|
||||
if body in hit_targets:
|
||||
return
|
||||
|
||||
# CRITICAL: Only the projectile owner (authority) should deal damage
|
||||
if player_owner and not player_owner.is_multiplayer_authority():
|
||||
return # Only the authority (creator) of the projectile can deal damage
|
||||
|
||||
# Add to hit_targets IMMEDIATELY to prevent multiple hits
|
||||
hit_targets[body] = true
|
||||
|
||||
# Deal damage to players
|
||||
if body.is_in_group("player") and body.has_method("rpc_take_damage"):
|
||||
play_impact()
|
||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
||||
var player_peer_id = body.get_multiplayer_authority()
|
||||
if player_peer_id != 0:
|
||||
if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id():
|
||||
body.rpc_take_damage(20.0, attacker_pos) # TODO: Get actual damage from player
|
||||
else:
|
||||
body.rpc_take_damage.rpc_id(player_peer_id, 20.0, attacker_pos)
|
||||
else:
|
||||
body.rpc_take_damage.rpc(20.0, attacker_pos)
|
||||
_stick_to_target(body)
|
||||
return
|
||||
|
||||
# Deal damage to enemies
|
||||
if body.is_in_group("enemy") and body.has_method("rpc_take_damage"):
|
||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
||||
var damage = 20.0 # TODO: Get actual damage from player
|
||||
if player_owner and player_owner.character_stats:
|
||||
damage = player_owner.character_stats.damage
|
||||
|
||||
# Check hit chance (based on player's DEX stat)
|
||||
var hit_roll = randf()
|
||||
var hit_chance = 0.95
|
||||
if player_owner and player_owner.character_stats:
|
||||
hit_chance = player_owner.character_stats.hit_chance
|
||||
var is_miss = hit_roll >= hit_chance
|
||||
|
||||
if is_miss:
|
||||
if body.has_method("_show_damage_number"):
|
||||
body._show_damage_number(0.0, attacker_pos, false, true, false) # is_miss = true
|
||||
_stick_to_target(body)
|
||||
return
|
||||
|
||||
play_impact()
|
||||
var enemy_peer_id = body.get_multiplayer_authority()
|
||||
if enemy_peer_id != 0:
|
||||
if multiplayer.is_server() and enemy_peer_id == multiplayer.get_unique_id():
|
||||
body.rpc_take_damage(damage, attacker_pos, false)
|
||||
else:
|
||||
body.rpc_take_damage.rpc_id(enemy_peer_id, damage, attacker_pos, false)
|
||||
else:
|
||||
body.rpc_take_damage.rpc(damage, attacker_pos, false)
|
||||
_stick_to_target(body)
|
||||
return
|
||||
|
||||
# Hit wall or other object
|
||||
$SfxImpactWall.play()
|
||||
_stick_to_wall()
|
||||
|
||||
func _stick_to_target(target: Node2D):
|
||||
# Stop the arrow
|
||||
velocity = Vector2.ZERO
|
||||
is_stuck = true
|
||||
stick_timer = 0.0
|
||||
arrow_area.set_deferred("monitoring", false)
|
||||
|
||||
# Calculate the collision point - move arrow slightly back from its direction
|
||||
var collision_normal = -direction
|
||||
var offset_distance = 8
|
||||
var stick_position = global_position + (collision_normal * offset_distance)
|
||||
|
||||
# Make arrow a child of the target to stick to it
|
||||
var global_rot = global_rotation
|
||||
get_parent().call_deferred("remove_child", self)
|
||||
target.call_deferred("add_child", self)
|
||||
self.set_deferred("global_position", stick_position)
|
||||
self.set_deferred("global_rotation", global_rot)
|
||||
|
||||
func _stick_to_wall():
|
||||
# Stop the arrow
|
||||
velocity = Vector2.ZERO
|
||||
is_stuck = true
|
||||
stick_timer = 0.0
|
||||
arrow_area.set_deferred("monitoring", false)
|
||||
pass # Replace with function body.
|
||||
|
||||
@@ -85,6 +85,28 @@ var equipment:Dictionary = {
|
||||
"dark": 0
|
||||
}
|
||||
|
||||
# Calculate total inventory weight (including equipped items)
|
||||
func get_total_weight() -> float:
|
||||
var total = 0.0
|
||||
# Count inventory items (stacked items count their quantity)
|
||||
for item in inventory:
|
||||
if item:
|
||||
total += item.weight * item.quantity
|
||||
# Count equipped items
|
||||
for slot in equipment.values():
|
||||
if slot:
|
||||
total += slot.weight
|
||||
return total
|
||||
|
||||
# Calculate carrying capacity based on STR
|
||||
func get_carrying_capacity() -> float:
|
||||
# Base capacity: 20 + (STR * 5)
|
||||
return 20.0 + (baseStats.str * 5.0)
|
||||
|
||||
# Check if over-encumbered
|
||||
func is_over_encumbered() -> bool:
|
||||
return get_total_weight() > get_carrying_capacity()
|
||||
|
||||
func getCalculatedStats():
|
||||
var _res = {
|
||||
"str": self.str,
|
||||
@@ -158,8 +180,9 @@ var damage: float:
|
||||
|
||||
var defense: float:
|
||||
get:
|
||||
# Reduced DEF scaling: 0.2 per END point (was 0.3) to make it less overpowered for low-level enemies
|
||||
return ((baseStats.end + get_pass("end")) * 0.2) + get_pass("def")
|
||||
# Further reduced DEF scaling: 0.15 per END point (was 0.2) - makes defense weaker overall
|
||||
# In D&D/Baldur's Gate, AC affects hit chance, not damage. Defense here provides minimal flat reduction.
|
||||
return ((baseStats.end + get_pass("end")) * 0.15) + get_pass("def")
|
||||
|
||||
var spell_amp: float:
|
||||
get:
|
||||
@@ -259,17 +282,22 @@ func modify_mana(amount: float) -> void:
|
||||
character_changed.emit(self)
|
||||
|
||||
func calculate_damage(base_damage: float, is_magical: bool = false, is_critical: bool = false) -> float:
|
||||
# Apply defense reduction (DEF reduces damage by a flat amount, not percentage)
|
||||
# Defense formula: flat reduction based on END and equipment DEF
|
||||
# Critical hits pierce 80% of DEF (only 20% of DEF applies to crits)
|
||||
# Apply defense reduction - more like D&D where defense provides minimal protection
|
||||
# Defense now provides percentage reduction (like armor) rather than flat reduction
|
||||
# This prevents complete damage negation while still providing meaningful protection
|
||||
var final_damage = base_damage
|
||||
if not is_magical:
|
||||
# Physical damage: reduce by defense value (flat reduction)
|
||||
var effective_defense = defense
|
||||
# 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 effective_defense = defense_percentage
|
||||
if is_critical:
|
||||
# Critical hits pierce 80% of DEF (only 20% applies)
|
||||
effective_defense = defense * 0.2
|
||||
final_damage = max(0.0, base_damage - effective_defense)
|
||||
effective_defense = defense_percentage * 0.2
|
||||
# Apply percentage reduction, but ensure at least 10% of damage gets through
|
||||
final_damage = base_damage * (1.0 - effective_defense)
|
||||
# Ensure minimum damage (at least 10% of base damage, or 1.0, whichever is higher)
|
||||
final_damage = max(1.0, max(base_damage * 0.1, final_damage))
|
||||
else:
|
||||
# Magical damage: reduce by magic resistance percentage
|
||||
final_damage = base_damage * (1 - (resistances.magic / 100.0))
|
||||
@@ -464,10 +492,27 @@ func drop_equipment(iItem:Item):
|
||||
pass
|
||||
|
||||
func add_item(iItem:Item):
|
||||
# Try to stack with existing items if possible
|
||||
if iItem.can_have_multiple_of:
|
||||
for existing_item in inventory:
|
||||
# Check if items are the same (same name and properties)
|
||||
if existing_item and existing_item.item_name == iItem.item_name and existing_item.spriteFrame == iItem.spriteFrame:
|
||||
# Stack the items
|
||||
existing_item.quantity += iItem.quantity
|
||||
emit_signal("character_changed", self)
|
||||
return
|
||||
|
||||
# If not stackable or no matching item found, add as new item
|
||||
self.inventory.push_back(iItem)
|
||||
|
||||
# Auto-equip if slot is empty (only for equippable items)
|
||||
# BUT: Do NOT auto-equip BOW weapons (they require arrows in off-hand)
|
||||
if iItem.item_type == Item.ItemType.Equippable and iItem.equipment_type != Item.EquipmentType.NONE:
|
||||
# Skip auto-equip for BOW weapons
|
||||
if iItem.equipment_type == Item.EquipmentType.MAINHAND and iItem.weapon_type == Item.WeaponType.BOW:
|
||||
emit_signal("character_changed", self)
|
||||
return
|
||||
|
||||
var slot_key = ""
|
||||
match iItem.equipment_type:
|
||||
Item.EquipmentType.MAINHAND:
|
||||
1
src/scripts/character_stats.gd.uid
Normal file
1
src/scripts/character_stats.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d7jp3ffh28hr
|
||||
@@ -309,13 +309,13 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
|
||||
print(name, " DODGED the attack! (DEX: ", character_stats.baseStats.dex + character_stats.get_pass("dex"), ", dodge chance: ", dodge_chance * 100.0, "%)")
|
||||
# Show "DODGED" text
|
||||
_show_damage_number(0.0, from_position, false, false, true) # is_dodged = true
|
||||
# Sync dodge visual to clients
|
||||
# Sync dodge visual to clients (pass damage_amount=0.0 and is_dodged=true to indicate dodge)
|
||||
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
||||
var enemy_name = name
|
||||
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if game_world and game_world.has_method("_sync_enemy_damage_visual"):
|
||||
game_world._sync_enemy_damage_visual.rpc(enemy_name, enemy_index)
|
||||
game_world._sync_enemy_damage_visual.rpc(enemy_name, enemy_index, 0.0, from_position, false, true)
|
||||
return # No damage taken, exit early
|
||||
|
||||
# If not dodged, apply damage with DEF reduction
|
||||
@@ -357,10 +357,10 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
|
||||
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if game_world and game_world.has_method("_sync_enemy_damage_visual"):
|
||||
game_world._sync_enemy_damage_visual.rpc(enemy_name, enemy_index)
|
||||
game_world._sync_enemy_damage_visual.rpc(enemy_name, enemy_index, actual_damage, from_position, is_critical)
|
||||
else:
|
||||
# Fallback: try direct RPC (may fail if node path doesn't match)
|
||||
_sync_damage_visual.rpc()
|
||||
_sync_damage_visual.rpc(actual_damage, from_position, is_critical)
|
||||
|
||||
if current_health <= 0:
|
||||
# Prevent multiple death triggers
|
||||
@@ -655,7 +655,7 @@ func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, dir: int = 0
|
||||
_update_client_visuals()
|
||||
|
||||
# This function can be called directly (not just via RPC) when game_world routes the update
|
||||
func _sync_damage_visual():
|
||||
func _sync_damage_visual(damage_amount: float = 0.0, attacker_position: Vector2 = Vector2.ZERO, is_critical: bool = false, is_dodged: bool = false):
|
||||
# Clients receive damage visual sync
|
||||
# Only process if we're not the authority (i.e., we're a client)
|
||||
if is_multiplayer_authority():
|
||||
@@ -663,6 +663,10 @@ func _sync_damage_visual():
|
||||
|
||||
_flash_damage()
|
||||
|
||||
# Show damage number on client (even if damage_amount is 0 for dodges/misses)
|
||||
if attacker_position != Vector2.ZERO:
|
||||
_show_damage_number(damage_amount, attacker_position, is_critical, false, is_dodged)
|
||||
|
||||
# This function can be called directly (not just via RPC) when game_world routes the update
|
||||
func _sync_death():
|
||||
# Clients receive death sync and play death animation locally
|
||||
|
||||
@@ -12,6 +12,11 @@ var detection_range: float = 80.0 # Range to detect players (much smaller)
|
||||
|
||||
var fly_height: float = 8.0 # Z position when flying
|
||||
|
||||
# Audio
|
||||
@onready var bat_flap_sfx: AudioStreamPlayer2D = $BatFlapSfx
|
||||
@onready var bat_chirp_sfx: AudioStreamPlayer2D = $BatChirpSfx
|
||||
var has_played_chase_sound: bool = false # Track if we've played sound when starting to chase
|
||||
|
||||
func _ready():
|
||||
super._ready()
|
||||
|
||||
@@ -80,9 +85,15 @@ func _idle_behavior(_delta):
|
||||
if target_player:
|
||||
var dist = global_position.distance_to(target_player.global_position)
|
||||
if dist < detection_range:
|
||||
# Start flying
|
||||
# Start flying (chasing player)
|
||||
var was_idle = (state == BatState.IDLE)
|
||||
state = BatState.FLYING
|
||||
state_timer = fly_duration
|
||||
|
||||
# Play sound very seldom when starting to chase (only once per bat, with low chance)
|
||||
if was_idle and not has_played_chase_sound and randf() < 0.15: # 15% chance
|
||||
_play_chase_sound()
|
||||
has_played_chase_sound = true
|
||||
return
|
||||
|
||||
# Switch to flying after idle duration
|
||||
@@ -177,3 +188,16 @@ func _play_death_animation():
|
||||
|
||||
await fade_tween.finished
|
||||
queue_free()
|
||||
|
||||
func _play_chase_sound():
|
||||
# Play a random bat sound when starting to chase (very seldom)
|
||||
if not bat_flap_sfx and not bat_chirp_sfx:
|
||||
return
|
||||
|
||||
# Randomly choose between flap or chirp
|
||||
if randf() < 0.5:
|
||||
if bat_flap_sfx and bat_flap_sfx.stream:
|
||||
bat_flap_sfx.play()
|
||||
else:
|
||||
if bat_chirp_sfx and bat_chirp_sfx.stream:
|
||||
bat_chirp_sfx.play()
|
||||
|
||||
@@ -366,7 +366,7 @@ func _sync_enemy_death(enemy_name: String, enemy_index: int):
|
||||
print("GameWorld: Could not find enemy for death sync: name=", enemy_name, " index=", enemy_index)
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func _sync_enemy_damage_visual(enemy_name: String, enemy_index: int):
|
||||
func _sync_enemy_damage_visual(enemy_name: String, enemy_index: int, damage_amount: float = 0.0, attacker_position: Vector2 = Vector2.ZERO, is_critical: bool = false, is_dodged: bool = false):
|
||||
# Clients receive enemy damage visual sync from server
|
||||
# Find the enemy by name or index
|
||||
if multiplayer.is_server():
|
||||
@@ -389,12 +389,63 @@ func _sync_enemy_damage_visual(enemy_name: String, enemy_index: int):
|
||||
|
||||
if enemy and enemy.has_method("_sync_damage_visual"):
|
||||
# Call the enemy's _sync_damage_visual method directly (not via RPC)
|
||||
enemy._sync_damage_visual()
|
||||
enemy._sync_damage_visual(damage_amount, attacker_position, is_critical, is_dodged)
|
||||
else:
|
||||
# Enemy not found - might already be freed or never spawned
|
||||
# This is okay, just log it
|
||||
print("GameWorld: Could not find enemy for damage visual sync: name=", enemy_name, " index=", enemy_index)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _request_item_drop(item_data: Dictionary, drop_position: Vector2, player_peer_id: int):
|
||||
# Server receives item drop request from client
|
||||
# Remove item from player's inventory and spawn as loot on server (syncs to all clients)
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
var entities_node = get_node_or_null("Entities")
|
||||
if not entities_node:
|
||||
return
|
||||
|
||||
# Find the player by peer_id
|
||||
var player = null
|
||||
var players = get_tree().get_nodes_in_group("player")
|
||||
for p in players:
|
||||
if p.has_method("get_multiplayer_authority") and p.get_multiplayer_authority() == player_peer_id:
|
||||
player = p
|
||||
break
|
||||
|
||||
# Create item from data
|
||||
var item = Item.new(item_data)
|
||||
if not item:
|
||||
print("GameWorld: Could not create item from data for drop request")
|
||||
return
|
||||
|
||||
# Remove item from player's inventory on server (if player found)
|
||||
if player and player.character_stats:
|
||||
var item_index = -1
|
||||
for i in range(player.character_stats.inventory.size()):
|
||||
var inv_item = player.character_stats.inventory[i]
|
||||
if inv_item and inv_item.item_name == item.item_name and inv_item.spriteFrame == item.spriteFrame:
|
||||
# Found matching item - remove it
|
||||
item_index = i
|
||||
break
|
||||
|
||||
if item_index >= 0:
|
||||
player.character_stats.inventory.remove_at(item_index)
|
||||
# Emit character_changed to sync inventory update
|
||||
player.character_stats.character_changed.emit(player.character_stats)
|
||||
print("GameWorld: Removed item from player inventory on server: ", item.item_name)
|
||||
else:
|
||||
print("GameWorld: WARNING: Item not found in player inventory on server: ", item.item_name)
|
||||
|
||||
# Spawn loot on server (this will sync to all clients)
|
||||
var loot = ItemLootHelper.spawn_item_loot(item, drop_position, entities_node, self)
|
||||
if loot:
|
||||
# Set metadata for pickup cooldown
|
||||
loot.set_meta("dropped_by_peer_id", player_peer_id)
|
||||
loot.set_meta("drop_time", Time.get_ticks_msec())
|
||||
print("GameWorld: Server spawned item loot from client drop request: ", item.item_name)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _request_loot_pickup(loot_id: int, loot_position: Vector2, player_peer_id: int):
|
||||
# Server receives loot pickup request from client
|
||||
@@ -589,6 +640,9 @@ func _generate_dungeon():
|
||||
var generator = load("res://scripts/dungeon_generator.gd").new()
|
||||
var map_size = Vector2i(72, 72) # 72x72 tiles
|
||||
|
||||
# Hide all players and remove collision before generating new level
|
||||
_hide_all_players()
|
||||
|
||||
# Generate dungeon (pass current level for scaling)
|
||||
dungeon_data = generator.generate_dungeon(map_size, dungeon_seed, current_level)
|
||||
|
||||
@@ -631,6 +685,14 @@ func _generate_dungeon():
|
||||
# Move any already-spawned players to the correct spawn points
|
||||
_move_all_players_to_start_room()
|
||||
|
||||
# Restore players (make visible and restore collision)
|
||||
_restore_all_players()
|
||||
|
||||
# Reinitialize room lighting system for new level
|
||||
var room_lighting = get_node_or_null("RoomLightingSystem")
|
||||
if room_lighting and room_lighting.has_method("reinitialize"):
|
||||
room_lighting.reinitialize()
|
||||
|
||||
# Update camera immediately to ensure it's looking at the players
|
||||
await get_tree().process_frame # Wait a frame for players to be fully in scene tree
|
||||
_update_camera()
|
||||
@@ -1079,6 +1141,11 @@ func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, h
|
||||
# Spawn room triggers on client
|
||||
_spawn_room_triggers()
|
||||
|
||||
# Reinitialize room lighting system for new level (client)
|
||||
var room_lighting = get_node_or_null("RoomLightingSystem")
|
||||
if room_lighting and room_lighting.has_method("reinitialize"):
|
||||
room_lighting.reinitialize()
|
||||
|
||||
# Wait a frame to ensure all enemies and objects are properly added to scene tree and initialized
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame # Wait an extra frame to ensure enemies are fully ready
|
||||
@@ -1585,6 +1652,36 @@ func _clear_level():
|
||||
|
||||
print("GameWorld: Previous level cleared")
|
||||
|
||||
func _hide_all_players():
|
||||
# Hide all players and remove collision before generating new level
|
||||
var players = get_tree().get_nodes_in_group("player")
|
||||
for player in players:
|
||||
if player:
|
||||
# Store original collision layer
|
||||
if not player.has_meta("original_collision_layer"):
|
||||
player.set_meta("original_collision_layer", player.collision_layer)
|
||||
# Remove collision layer
|
||||
player.collision_layer = 0
|
||||
# Hide player
|
||||
player.visible = false
|
||||
print("GameWorld: Hid player ", player.name, " and removed collision")
|
||||
|
||||
func _restore_all_players():
|
||||
# Restore all players (make visible and restore collision) after placement
|
||||
var players = get_tree().get_nodes_in_group("player")
|
||||
for player in players:
|
||||
if player:
|
||||
# Restore collision layer
|
||||
if player.has_meta("original_collision_layer"):
|
||||
player.collision_layer = player.get_meta("original_collision_layer")
|
||||
player.remove_meta("original_collision_layer")
|
||||
else:
|
||||
# Default to layer 1 (players)
|
||||
player.collision_layer = 1
|
||||
# Make player visible
|
||||
player.visible = true
|
||||
print("GameWorld: Restored player ", player.name, " (visible and collision restored)")
|
||||
|
||||
func _move_all_players_to_start_room():
|
||||
# Move all players to the start room of the new level
|
||||
if dungeon_data.is_empty() or not dungeon_data.has("start_room"):
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
uid://dvcubtup4odug
|
||||
@@ -1 +0,0 @@
|
||||
uid://d1nl6a63n5wtr
|
||||
@@ -7,11 +7,13 @@ extends CanvasLayer
|
||||
|
||||
var is_open: bool = false
|
||||
var local_player: Node = null
|
||||
var is_updating_ui: bool = false # Prevent recursive UI updates
|
||||
|
||||
# Selection tracking
|
||||
var selected_item: Item = null # Selected inventory item
|
||||
var selected_slot: String = "" # Selected equipment slot name
|
||||
var selected_type: String = "" # "item" or "equipment"
|
||||
var is_first_open: bool = true # Track if this is the first time opening
|
||||
|
||||
# Navigation tracking (for keyboard navigation)
|
||||
var inventory_selection_row: int = 0 # Current inventory row (0-based)
|
||||
@@ -20,17 +22,20 @@ var equipment_selection_index: int = 0 # Current equipment slot index (0-5: mai
|
||||
|
||||
# UI Nodes (from scene)
|
||||
@onready var container: Control = $InventoryContainer
|
||||
@onready var stats_panel: Control = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel
|
||||
@onready var equipment_panel: GridContainer = $InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/EquipmentPanel
|
||||
@onready var scroll_container: ScrollContainer = $InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll
|
||||
@onready var inventory_grid: VBoxContainer = $InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll/InventoryVBox
|
||||
@onready var selection_rectangle: Panel = $InventoryContainer/MarginContainer/VBoxContainer/SelectionRectangle
|
||||
@onready var info_panel: Control = $InventoryContainer/MarginContainer/VBoxContainer/InfoPanel
|
||||
@onready var info_label: Label = $InventoryContainer/MarginContainer/VBoxContainer/InfoPanel/InfoLabel
|
||||
@onready var label_base_stats: Label = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelBaseStats
|
||||
@onready var label_base_stats_value: Label = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelBaseStatsValue
|
||||
@onready var label_derived_stats: Label = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelDerivedStats
|
||||
@onready var label_derived_stats_value: Label = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelDerivedStatsValue
|
||||
@onready var stats_panel: Control = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel
|
||||
@onready var equipment_panel: GridContainer = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/EquipmentPanel
|
||||
@onready var scroll_container: ScrollContainer = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll
|
||||
@onready var inventory_grid: VBoxContainer = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll/InventoryVBox
|
||||
@onready var selection_rectangle: Panel = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/SelectionRectangle
|
||||
@onready var info_panel: Control = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/InfoPanel
|
||||
@onready var info_label: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/InfoPanel/InfoLabel
|
||||
@onready var label_base_stats: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelBaseStats
|
||||
@onready var label_base_stats_value: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelBaseStatsValue
|
||||
@onready var label_derived_stats: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelDerivedStats
|
||||
@onready var label_derived_stats_value: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelDerivedStatsValue
|
||||
@onready var sfx_potion: AudioStreamPlayer2D = $SfxPotion
|
||||
@onready var sfx_food: AudioStreamPlayer2D = $SfxFood
|
||||
@onready var sfx_armour: AudioStreamPlayer2D = $SfxArmour
|
||||
|
||||
# Store button/item mappings for selection highlighting
|
||||
var inventory_buttons: Dictionary = {} # item -> button
|
||||
@@ -66,9 +71,6 @@ func _ready():
|
||||
# Load styleboxes for inventory slots (like inspiration system)
|
||||
_setup_styleboxes()
|
||||
|
||||
# Setup fonts for labels
|
||||
_setup_fonts()
|
||||
|
||||
# Create equipment slot buttons (dynamically)
|
||||
_create_equipment_slots()
|
||||
|
||||
@@ -79,23 +81,40 @@ func _ready():
|
||||
call_deferred("_find_local_player")
|
||||
|
||||
func _setup_styleboxes():
|
||||
# Create styleboxes similar to inspiration inventory system
|
||||
var slot_texture = preload("res://assets/gfx/ui/inventory_slot_kenny_white.png")
|
||||
if not slot_texture:
|
||||
# Fallback if texture doesn't exist
|
||||
slot_texture = null
|
||||
# Create styleboxes exactly like inspiration inventory system
|
||||
var selected_tex = preload("res://assets/gfx/ui/inventory_slot_kenny_white.png")
|
||||
|
||||
style_box_empty = StyleBoxEmpty.new()
|
||||
|
||||
if slot_texture:
|
||||
if selected_tex:
|
||||
# Scale factor for the slot background (1.5x to match larger item sprites)
|
||||
# Since StyleBoxTexture doesn't support texture_scale, we use expand_margin
|
||||
# to make the texture fill more space
|
||||
# For 1.5x scale on a 36px button (which was 24px originally), we need to expand
|
||||
# The button is now 36px, so to make a 24px texture appear 1.5x (36px), we use negative margins
|
||||
# Actually, let's use smaller positive margins to avoid clipping
|
||||
var margin_scale = 3.0 # Smaller margin to avoid clipping in upper left corner
|
||||
|
||||
style_box_hover = StyleBoxTexture.new()
|
||||
style_box_hover.texture = slot_texture
|
||||
style_box_hover.texture = selected_tex
|
||||
style_box_hover.expand_margin_left = margin_scale
|
||||
style_box_hover.expand_margin_top = margin_scale
|
||||
style_box_hover.expand_margin_right = margin_scale
|
||||
style_box_hover.expand_margin_bottom = margin_scale
|
||||
|
||||
style_box_focused = StyleBoxTexture.new()
|
||||
style_box_focused.texture = slot_texture
|
||||
style_box_focused.texture = selected_tex
|
||||
style_box_focused.expand_margin_left = margin_scale
|
||||
style_box_focused.expand_margin_top = margin_scale
|
||||
style_box_focused.expand_margin_right = margin_scale
|
||||
style_box_focused.expand_margin_bottom = margin_scale
|
||||
|
||||
style_box_pressed = StyleBoxTexture.new()
|
||||
style_box_pressed.texture = slot_texture
|
||||
style_box_pressed.texture = selected_tex
|
||||
style_box_pressed.expand_margin_left = margin_scale
|
||||
style_box_pressed.expand_margin_top = margin_scale
|
||||
style_box_pressed.expand_margin_right = margin_scale
|
||||
style_box_pressed.expand_margin_bottom = margin_scale
|
||||
else:
|
||||
# Fallback to empty styleboxes if texture not found
|
||||
style_box_hover = StyleBoxEmpty.new()
|
||||
@@ -106,41 +125,17 @@ func _setup_styleboxes():
|
||||
if ResourceLoader.exists("res://assets/fonts/dmg_numbers.png"):
|
||||
quantity_font = load("res://assets/fonts/dmg_numbers.png")
|
||||
|
||||
func _setup_fonts():
|
||||
# Setup fonts for labels (standard_font.png)
|
||||
var standard_font_resource = null
|
||||
if ResourceLoader.exists("res://assets/fonts/standard_font.png"):
|
||||
standard_font_resource = load("res://assets/fonts/standard_font.png")
|
||||
if standard_font_resource:
|
||||
# Stats panel labels
|
||||
if label_base_stats:
|
||||
label_base_stats.add_theme_font_override("font", standard_font_resource)
|
||||
if label_base_stats_value:
|
||||
label_base_stats_value.add_theme_font_override("font", standard_font_resource)
|
||||
if label_derived_stats:
|
||||
label_derived_stats.add_theme_font_override("font", standard_font_resource)
|
||||
if label_derived_stats_value:
|
||||
label_derived_stats_value.add_theme_font_override("font", standard_font_resource)
|
||||
|
||||
# Info label
|
||||
if info_label:
|
||||
info_label.add_theme_font_override("font", standard_font_resource)
|
||||
|
||||
# Equipment and Inventory labels
|
||||
var eq_label = container.get_node_or_null("MarginContainer/VBoxContainer/HBox/InventoryPanel/EquipmentLabel")
|
||||
if eq_label:
|
||||
eq_label.add_theme_font_override("font", standard_font_resource)
|
||||
var inv_label = container.get_node_or_null("MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryLabel")
|
||||
if inv_label:
|
||||
inv_label.add_theme_font_override("font", standard_font_resource)
|
||||
var stats_label = container.get_node_or_null("MarginContainer/VBoxContainer/HBox/StatsPanel/StatsLabel")
|
||||
if stats_label:
|
||||
stats_label.add_theme_font_override("font", standard_font_resource)
|
||||
|
||||
func _setup_selection_rectangle():
|
||||
# Selection rectangle is already in scene, just ensure it's configured correctly
|
||||
if selection_rectangle:
|
||||
selection_rectangle.visible = false
|
||||
# Ensure it's on top and visible
|
||||
selection_rectangle.z_index = 100
|
||||
selection_rectangle.z_as_relative = false
|
||||
selection_rectangle.mouse_filter = Control.MOUSE_FILTER_IGNORE # Don't block mouse input
|
||||
# Ensure it's on top
|
||||
selection_rectangle.z_index = 100
|
||||
selection_rectangle.z_as_relative = false
|
||||
|
||||
func _find_local_player():
|
||||
# Find the local player
|
||||
@@ -207,7 +202,6 @@ func _create_equipment_slots():
|
||||
label.text = slot_label
|
||||
label.add_theme_font_size_override("font_size", 10)
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
var standard_font_resource = null
|
||||
if ResourceLoader.exists("res://assets/fonts/standard_font.png"):
|
||||
var font_resource = load("res://assets/fonts/standard_font.png")
|
||||
if font_resource:
|
||||
@@ -217,8 +211,9 @@ func _create_equipment_slots():
|
||||
# Button (use styleboxes like inspiration system)
|
||||
var button = Button.new()
|
||||
button.name = slot_name + "_btn"
|
||||
button.custom_minimum_size = Vector2(24, 24) # Smaller like inspiration (24x24 instead of 60x60)
|
||||
button.size = Vector2(24, 24)
|
||||
# Button size increased to accommodate 1.5x scaled texture (24 * 1.5 = 36)
|
||||
button.custom_minimum_size = Vector2(36, 36) # Increased to fit 1.5x scaled texture
|
||||
button.size = Vector2(36, 36)
|
||||
if style_box_empty:
|
||||
button.add_theme_stylebox_override("normal", style_box_empty)
|
||||
if style_box_hover:
|
||||
@@ -228,7 +223,15 @@ func _create_equipment_slots():
|
||||
if style_box_pressed:
|
||||
button.add_theme_stylebox_override("pressed", style_box_pressed)
|
||||
button.flat = false # Use styleboxes instead of flat
|
||||
button.focus_mode = Control.FOCUS_ALL # Allow button to receive focus
|
||||
button.size_flags_horizontal = 0
|
||||
button.size_flags_vertical = 0
|
||||
button.connect("pressed", _on_equipment_slot_pressed.bind(slot_name))
|
||||
# Connect focus_entered like inspiration system (for keyboard navigation)
|
||||
if local_player and local_player.character_stats:
|
||||
var equipped_item = local_player.character_stats.equipment[slot_name]
|
||||
if equipped_item:
|
||||
button.connect("focus_entered", _on_equipment_slot_pressed.bind(slot_name))
|
||||
slot_container.add_child(button)
|
||||
|
||||
equipment_slots[slot_name] = button
|
||||
@@ -259,6 +262,10 @@ func _on_equipment_slot_pressed(slot_name: String):
|
||||
if not local_player or not local_player.character_stats:
|
||||
return
|
||||
|
||||
# Prevent updates during UI refresh (prevents infinite loops from focus_entered)
|
||||
if is_updating_ui:
|
||||
return
|
||||
|
||||
# Only select if there's an item equipped
|
||||
if not _has_equipment_in_slot(slot_name):
|
||||
return
|
||||
@@ -276,67 +283,69 @@ func _on_equipment_slot_pressed(slot_name: String):
|
||||
_update_selection_rectangle()
|
||||
|
||||
func _update_selection_highlight():
|
||||
# Reset all button styles
|
||||
for button in equipment_buttons.values():
|
||||
if button:
|
||||
var highlight = button.get_node_or_null("Highlight")
|
||||
if highlight:
|
||||
highlight.queue_free()
|
||||
# This function is kept for compatibility but now uses _update_selection_rectangle()
|
||||
_update_selection_rectangle()
|
||||
|
||||
for button in inventory_buttons.values():
|
||||
if button:
|
||||
var highlight = button.get_node_or_null("Highlight")
|
||||
if highlight:
|
||||
highlight.queue_free()
|
||||
# Removed _clear_button_highlight and _apply_button_highlight - using focus system instead
|
||||
|
||||
func _update_selection_rectangle():
|
||||
# Update visual selection rectangle position and visibility
|
||||
if not selection_rectangle:
|
||||
return
|
||||
|
||||
var target_button: Button = null
|
||||
var target_position: Vector2 = Vector2.ZERO
|
||||
var should_show: bool = false
|
||||
|
||||
# Get the parent of selection_rectangle (VBoxContainer) to calculate relative positions
|
||||
var selection_parent = selection_rectangle.get_parent()
|
||||
if not selection_parent:
|
||||
# Update visual selection indicator - use button focus like inspiration system
|
||||
# Hide the old selection rectangle
|
||||
if selection_rectangle:
|
||||
selection_rectangle.visible = false
|
||||
return
|
||||
|
||||
# Find and focus the selected button (like inspiration system uses grab_focus())
|
||||
var target_button: Button = null
|
||||
|
||||
if selected_type == "equipment" and selected_slot != "":
|
||||
# Show rectangle on equipment slot (only if it has an item)
|
||||
# Focus equipment slot (only if it has an item)
|
||||
if _has_equipment_in_slot(selected_slot):
|
||||
target_button = equipment_buttons.get(selected_slot)
|
||||
if target_button and target_button.is_inside_tree():
|
||||
# Get button position relative to selection_rectangle's parent (VBoxContainer)
|
||||
var button_global_pos = target_button.global_position
|
||||
var parent_global_pos = selection_parent.global_position
|
||||
target_position = button_global_pos - parent_global_pos
|
||||
should_show = true
|
||||
elif selected_type == "item" and inventory_selection_row >= 0 and inventory_selection_row < inventory_rows_list.size():
|
||||
# Show rectangle on inventory item
|
||||
# Focus inventory item
|
||||
var row = inventory_rows_list[inventory_selection_row]
|
||||
if row and inventory_selection_col >= 0 and inventory_selection_col < row.get_child_count():
|
||||
target_button = row.get_child(inventory_selection_col) as Button
|
||||
if target_button and target_button.is_inside_tree():
|
||||
# Get button position relative to selection_rectangle's parent (VBoxContainer)
|
||||
var button_global_pos = target_button.global_position
|
||||
var parent_global_pos = selection_parent.global_position
|
||||
target_position = button_global_pos - parent_global_pos
|
||||
should_show = true
|
||||
|
||||
# Only show and position if we have a valid target
|
||||
if should_show and target_button:
|
||||
selection_rectangle.visible = true
|
||||
selection_rectangle.position = target_position
|
||||
selection_rectangle.size = Vector2(38, 38)
|
||||
# Grab focus on selected button (this will automatically show the focus stylebox)
|
||||
if target_button:
|
||||
if target_button.is_inside_tree():
|
||||
# Don't grab focus if button already has focus (prevents infinite loops)
|
||||
if target_button.has_focus():
|
||||
return
|
||||
|
||||
# Ensure button is visible and ready
|
||||
if not target_button.visible:
|
||||
target_button.visible = true
|
||||
# Wait a frame to ensure button is fully ready for focus
|
||||
await get_tree().process_frame
|
||||
# Check again if already focused (might have changed during await)
|
||||
if target_button.has_focus():
|
||||
return
|
||||
# Ensure button can receive focus
|
||||
if target_button.focus_mode == Control.FOCUS_NONE:
|
||||
target_button.focus_mode = Control.FOCUS_ALL
|
||||
# Try direct grab_focus (only if not already focused)
|
||||
if not target_button.has_focus():
|
||||
target_button.grab_focus()
|
||||
# Also use call_deferred as backup to ensure it's set
|
||||
target_button.call_deferred("grab_focus")
|
||||
print("InventoryUI: Focus grabbed on button - has_focus: ", target_button.has_focus())
|
||||
else:
|
||||
selection_rectangle.visible = false
|
||||
# If not in tree yet, wait a frame and try again
|
||||
await get_tree().process_frame
|
||||
if target_button.is_inside_tree():
|
||||
target_button.grab_focus()
|
||||
target_button.call_deferred("grab_focus")
|
||||
print("InventoryUI: Focus grabbed on button (after wait)")
|
||||
else:
|
||||
print("InventoryUI: Button still not in tree after wait")
|
||||
else:
|
||||
print("InventoryUI: No button to focus - selected_type: ", selected_type)
|
||||
|
||||
func _process(delta):
|
||||
if is_open and selection_rectangle and selection_rectangle.visible:
|
||||
# Animate selection rectangle border color
|
||||
if is_open:
|
||||
# Animate selection highlight border color on selected button
|
||||
selection_animation_time += delta * 2.0 # Speed of animation
|
||||
# Animate between yellow and orange
|
||||
var color1 = Color.YELLOW
|
||||
@@ -344,8 +353,18 @@ func _process(delta):
|
||||
var t = (sin(selection_animation_time) + 1.0) / 2.0 # 0 to 1
|
||||
var animated_color = color1.lerp(color2, t)
|
||||
|
||||
# Update border color
|
||||
var stylebox = selection_rectangle.get_theme_stylebox("panel") as StyleBoxFlat
|
||||
# Find the selected button and update its highlight color
|
||||
var selected_button: Button = null
|
||||
if selected_type == "equipment" and selected_slot != "":
|
||||
if _has_equipment_in_slot(selected_slot):
|
||||
selected_button = equipment_buttons.get(selected_slot)
|
||||
elif selected_type == "item" and inventory_selection_row >= 0 and inventory_selection_row < inventory_rows_list.size():
|
||||
var row = inventory_rows_list[inventory_selection_row]
|
||||
if row and inventory_selection_col >= 0 and inventory_selection_col < row.get_child_count():
|
||||
selected_button = row.get_child(inventory_selection_col) as Button
|
||||
|
||||
if selected_button and selected_button.has_meta("highlight_stylebox"):
|
||||
var stylebox = selected_button.get_meta("highlight_stylebox") as StyleBoxFlat
|
||||
if stylebox:
|
||||
stylebox.border_color = animated_color
|
||||
|
||||
@@ -353,8 +372,21 @@ func _update_ui():
|
||||
if not local_player or not local_player.character_stats:
|
||||
return
|
||||
|
||||
# Prevent recursive updates
|
||||
if is_updating_ui:
|
||||
return
|
||||
is_updating_ui = true
|
||||
|
||||
var char_stats = local_player.character_stats
|
||||
|
||||
# Ensure containers don't clip their children (allows expand_margin to show properly)
|
||||
if scroll_container:
|
||||
scroll_container.clip_contents = false # Allow buttons to extend beyond scroll bounds
|
||||
if inventory_grid:
|
||||
inventory_grid.clip_contents = false # Allow buttons to extend beyond grid bounds
|
||||
if equipment_panel:
|
||||
equipment_panel.clip_contents = false # Allow buttons to extend beyond grid bounds
|
||||
|
||||
# Debug: Print inventory contents
|
||||
print("InventoryUI: Updating UI - inventory size: ", char_stats.inventory.size())
|
||||
for i in range(char_stats.inventory.size()):
|
||||
@@ -366,6 +398,9 @@ func _update_ui():
|
||||
inventory_items_list.clear()
|
||||
inventory_rows_list.clear()
|
||||
|
||||
# Wait for old buttons to be fully freed before creating new ones
|
||||
await get_tree().process_frame
|
||||
|
||||
# Update equipment slots
|
||||
for slot_name in equipment_slots.keys():
|
||||
var button = equipment_slots[slot_name]
|
||||
@@ -394,13 +429,30 @@ func _update_ui():
|
||||
sprite.scale = Vector2(2.0, 2.0) # 2x size as requested
|
||||
button.add_child(sprite)
|
||||
|
||||
# Add quantity label if item can have multiple (like arrows)
|
||||
if equipped_item.can_have_multiple_of and equipped_item.quantity > 1:
|
||||
var quantity_label = Label.new()
|
||||
quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
quantity_label.size = Vector2(24, 24)
|
||||
quantity_label.custom_minimum_size = Vector2(0, 0)
|
||||
quantity_label.position = Vector2(10, 2)
|
||||
quantity_label.text = str(equipped_item.quantity)
|
||||
if quantity_font:
|
||||
quantity_label.add_theme_font_override("font", quantity_font)
|
||||
quantity_label.add_theme_font_size_override("font_size", 8)
|
||||
quantity_label.scale = Vector2(0.5, 0.5)
|
||||
button.add_child(quantity_label)
|
||||
|
||||
# Update inventory grid - clear existing HBoxContainers
|
||||
for child in inventory_grid.get_children():
|
||||
child.queue_free()
|
||||
|
||||
# Wait for old buttons to be fully freed before creating new ones
|
||||
await get_tree().process_frame
|
||||
|
||||
# Add inventory items using HBoxContainers (like inspiration system)
|
||||
var current_hbox: HBoxContainer = null
|
||||
var items_per_row = 10 # Items per row (like inspiration system - they use 10 per HBox)
|
||||
var items_per_row = 8 # Items per row (3 rows = 24 total items max)
|
||||
var items_in_current_row = 0
|
||||
|
||||
for item in char_stats.inventory:
|
||||
@@ -408,14 +460,17 @@ func _update_ui():
|
||||
if current_hbox == null or items_in_current_row >= items_per_row:
|
||||
current_hbox = HBoxContainer.new()
|
||||
current_hbox.add_theme_constant_override("separation", 0) # No separation like inspiration
|
||||
# Ensure HBoxContainer doesn't clip child buttons
|
||||
current_hbox.clip_contents = false
|
||||
inventory_grid.add_child(current_hbox)
|
||||
inventory_rows_list.append(current_hbox)
|
||||
items_in_current_row = 0
|
||||
|
||||
# Create button with styleboxes (like inspiration system)
|
||||
var button = Button.new()
|
||||
button.custom_minimum_size = Vector2(24, 24) # Smaller like inspiration (24x24)
|
||||
button.size = Vector2(24, 24)
|
||||
# Button size increased to accommodate 1.5x scaled texture (24 * 1.5 = 36)
|
||||
button.custom_minimum_size = Vector2(36, 36) # Increased to fit 1.5x scaled texture
|
||||
button.size = Vector2(36, 36)
|
||||
if style_box_empty:
|
||||
button.add_theme_stylebox_override("normal", style_box_empty)
|
||||
if style_box_hover:
|
||||
@@ -425,7 +480,12 @@ func _update_ui():
|
||||
if style_box_pressed:
|
||||
button.add_theme_stylebox_override("pressed", style_box_pressed)
|
||||
button.flat = false # Use styleboxes
|
||||
button.focus_mode = Control.FOCUS_ALL # Allow button to receive focus
|
||||
button.connect("pressed", _on_inventory_item_pressed.bind(item))
|
||||
# Connect focus_entered like inspiration system (for keyboard navigation)
|
||||
# Note: focus_entered will trigger when we call grab_focus(), but _on_inventory_item_pressed
|
||||
# just updates selection state, so it should be safe
|
||||
button.connect("focus_entered", _on_inventory_item_pressed.bind(item))
|
||||
current_hbox.add_child(button)
|
||||
|
||||
# Add item sprite (like inspiration system - positioned at 4,4 with centered=false)
|
||||
@@ -442,18 +502,23 @@ func _update_ui():
|
||||
sprite.scale = Vector2(2.0, 2.0) # 2x size as requested
|
||||
button.add_child(sprite)
|
||||
|
||||
# Add quantity label if item can have multiple (like inspiration system)
|
||||
if item.can_have_multiple_of and item.quantity > 1:
|
||||
# Add quantity label if item quantity > 1 (show for all stacked items)
|
||||
if item.quantity > 1:
|
||||
var quantity_label = Label.new()
|
||||
quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
quantity_label.size = Vector2(24, 24)
|
||||
quantity_label.custom_minimum_size = Vector2(0, 0)
|
||||
quantity_label.position = Vector2(10, 2)
|
||||
quantity_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||
quantity_label.size = Vector2(36, 36)
|
||||
quantity_label.custom_minimum_size = Vector2(36, 36)
|
||||
quantity_label.position = Vector2(0, 0)
|
||||
quantity_label.text = str(item.quantity)
|
||||
if quantity_font:
|
||||
quantity_label.add_theme_font_override("font", quantity_font)
|
||||
quantity_label.add_theme_font_size_override("font_size", 8)
|
||||
quantity_label.scale = Vector2(0.5, 0.5)
|
||||
# Use dmg_numbers.png font (same as damage_number.gd)
|
||||
var dmg_font_resource = load("res://assets/fonts/dmg_numbers.png")
|
||||
if dmg_font_resource:
|
||||
var font_file = FontFile.new()
|
||||
font_file.font_data = dmg_font_resource
|
||||
quantity_label.add_theme_font_override("font", font_file)
|
||||
quantity_label.add_theme_font_size_override("font_size", 16)
|
||||
quantity_label.z_index = 100 # High z-index to show above item sprite
|
||||
button.add_child(quantity_label)
|
||||
|
||||
inventory_buttons[item] = button
|
||||
@@ -468,19 +533,74 @@ func _update_ui():
|
||||
if inventory_selection_col >= row.get_child_count():
|
||||
inventory_selection_col = max(0, row.get_child_count() - 1)
|
||||
|
||||
# Update selection
|
||||
# Update selection only if selected_type is already set (don't auto-update during initialization)
|
||||
if selected_type != "":
|
||||
_update_selection_from_navigation()
|
||||
_update_selection_rectangle()
|
||||
_update_info_panel()
|
||||
|
||||
_set_selection()
|
||||
|
||||
# Reset update flag
|
||||
is_updating_ui = false
|
||||
|
||||
func _set_selection():
|
||||
# NOW check for items AFTER UI is updated
|
||||
# Initialize selection - prefer inventory, but if empty, check equipment
|
||||
# Check if we have inventory items
|
||||
if inventory_rows_list.size() > 0 and inventory_items_list.size() > 0:
|
||||
selected_type = "item"
|
||||
inventory_selection_row = 0
|
||||
inventory_selection_col = 0
|
||||
# Ensure selection is set correctly
|
||||
_update_selection_from_navigation()
|
||||
# Debug: Print selection state
|
||||
print("InventoryUI: Initial selection - type: ", selected_type, " row: ", inventory_selection_row, " col: ", inventory_selection_col, " item: ", selected_item)
|
||||
# Now set focus - buttons should be ready
|
||||
await _update_selection_rectangle() # Await to ensure focus is set
|
||||
_update_info_panel()
|
||||
else:
|
||||
# No inventory items, try equipment
|
||||
var first_filled_slot = _find_next_filled_equipment_slot(-1, 1)
|
||||
if first_filled_slot >= 0:
|
||||
selected_type = "equipment"
|
||||
equipment_selection_index = first_filled_slot
|
||||
selected_slot = equipment_slots_list[first_filled_slot]
|
||||
# Ensure selection is set correctly
|
||||
_update_selection_from_navigation()
|
||||
# Debug: Print selection state
|
||||
print("InventoryUI: Initial selection - type: ", selected_type, " slot: ", selected_slot, " item: ", selected_item)
|
||||
# Now set focus - buttons should be ready
|
||||
await _update_selection_rectangle() # Await to ensure focus is set
|
||||
_update_info_panel()
|
||||
else:
|
||||
# Nothing to select (only print this AFTER UI is updated)
|
||||
selected_type = ""
|
||||
if selection_rectangle:
|
||||
selection_rectangle.visible = false
|
||||
if info_label:
|
||||
info_label.text = ""
|
||||
print("InventoryUI: No items to select")
|
||||
pass
|
||||
|
||||
func _update_selection_from_navigation():
|
||||
# Update selected_item/selected_slot based on navigation position
|
||||
# Early return if selected_type is not set yet (prevents errors during initialization)
|
||||
if selected_type == "":
|
||||
print("InventoryUI: _update_selection_from_navigation() - selected_type is empty, skipping")
|
||||
return
|
||||
|
||||
print("InventoryUI: _update_selection_from_navigation() - selected_type: ", selected_type, " inventory_rows_list.size(): ", inventory_rows_list.size(), " inventory_items_list.size(): ", inventory_items_list.size())
|
||||
|
||||
if selected_type == "equipment" and equipment_selection_index >= 0 and equipment_selection_index < equipment_slots_list.size():
|
||||
var slot_name = equipment_slots_list[equipment_selection_index]
|
||||
if _has_equipment_in_slot(slot_name):
|
||||
selected_slot = slot_name
|
||||
if local_player and local_player.character_stats:
|
||||
selected_item = local_player.character_stats.equipment[slot_name]
|
||||
else:
|
||||
selected_item = null
|
||||
print("InventoryUI: Selected equipment slot: ", slot_name, " item: ", selected_item)
|
||||
else:
|
||||
# Empty slot - switch to inventory
|
||||
selected_type = "item"
|
||||
@@ -492,21 +612,35 @@ func _update_selection_from_navigation():
|
||||
_update_selection_from_navigation()
|
||||
elif selected_type == "item" and inventory_selection_row >= 0 and inventory_selection_row < inventory_rows_list.size():
|
||||
var row = inventory_rows_list[inventory_selection_row]
|
||||
if inventory_selection_col >= 0 and inventory_selection_col < row.get_child_count():
|
||||
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 item_index = inventory_selection_row * 10 + 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():
|
||||
selected_item = inventory_items_list[item_index]
|
||||
selected_slot = ""
|
||||
print("InventoryUI: Selected inventory item: ", selected_item.item_name if selected_item else "null")
|
||||
else:
|
||||
selected_item = null
|
||||
selected_slot = ""
|
||||
print("InventoryUI: item_index out of range!")
|
||||
else:
|
||||
selected_item = null
|
||||
selected_slot = ""
|
||||
print("InventoryUI: Row or column invalid!")
|
||||
else:
|
||||
print("InventoryUI: selected_type invalid or row out of range!")
|
||||
|
||||
func _format_item_info(item: Item) -> String:
|
||||
# Format item description, stats modifiers, and controls
|
||||
var text = ""
|
||||
|
||||
# Item name (always show)
|
||||
text += item.item_name
|
||||
|
||||
# Description
|
||||
if item.description != "":
|
||||
text += item.description
|
||||
else:
|
||||
text += item.item_name
|
||||
text += "\n" + item.description
|
||||
|
||||
text += "\n\n"
|
||||
|
||||
@@ -538,7 +672,7 @@ func _format_item_info(item: Item) -> String:
|
||||
stat_lines.append("MAXMP: +%d" % item.modifiers["maxmp"])
|
||||
|
||||
if stat_lines.size() > 0:
|
||||
text += "\n".join(stat_lines)
|
||||
text += ", ".join(stat_lines)
|
||||
text += "\n\n"
|
||||
|
||||
# Controls
|
||||
@@ -550,25 +684,28 @@ func _format_item_info(item: Item) -> String:
|
||||
elif item.item_type == Item.ItemType.Restoration:
|
||||
text += "Press F to consume"
|
||||
|
||||
# Only show "Press E to drop" for inventory items, not equipment
|
||||
if selected_type == "item":
|
||||
text += "\nPress E to drop"
|
||||
text += ", Press E to drop"
|
||||
|
||||
return text
|
||||
|
||||
func _update_info_panel():
|
||||
# Update info panel based on selected item
|
||||
if not info_label:
|
||||
print("InventoryUI: _update_info_panel() - info_label is null!")
|
||||
return
|
||||
|
||||
print("InventoryUI: _update_info_panel() - selected_item: ", selected_item, " selected_type: ", selected_type)
|
||||
if selected_item:
|
||||
info_label.text = _format_item_info(selected_item)
|
||||
print("InventoryUI: Info panel text set: ", info_label.text.substr(0, 50) if info_label.text.length() > 50 else info_label.text)
|
||||
else:
|
||||
info_label.text = ""
|
||||
print("InventoryUI: Info panel text cleared (no selected_item)")
|
||||
|
||||
func _navigate_inventory(direction: String):
|
||||
# Handle navigation within inventory
|
||||
var items_per_row = 10
|
||||
|
||||
match direction:
|
||||
"left":
|
||||
if inventory_selection_col > 0:
|
||||
@@ -625,7 +762,6 @@ func _navigate_equipment(direction: String):
|
||||
# Equipment layout: 3 columns, 2 rows
|
||||
# Row 1: mainhand(0), offhand(1), headgear(2)
|
||||
# Row 2: armour(3), boots(4), accessory(5)
|
||||
|
||||
match direction:
|
||||
"left":
|
||||
var next_index = _find_next_filled_equipment_slot(equipment_selection_index, -1)
|
||||
@@ -637,7 +773,7 @@ func _navigate_equipment(direction: String):
|
||||
equipment_selection_index = next_index
|
||||
"up":
|
||||
# Find next filled slot in row above (same column)
|
||||
var current_row = int(equipment_selection_index / 3)
|
||||
var current_row: int = floor(equipment_selection_index / 3.0)
|
||||
var current_col = equipment_selection_index % 3
|
||||
if current_row > 0:
|
||||
var target_index = (current_row - 1) * 3 + current_col
|
||||
@@ -652,7 +788,7 @@ func _navigate_equipment(direction: String):
|
||||
# Can't go up from equipment (already at top)
|
||||
"down":
|
||||
# Find next filled slot in row below (same column), or move to inventory
|
||||
var current_row = int(equipment_selection_index / 3)
|
||||
var current_row: int = floor(equipment_selection_index / 3.0)
|
||||
var current_col = equipment_selection_index % 3
|
||||
if current_row < 1:
|
||||
var target_index = (current_row + 1) * 3 + current_col
|
||||
@@ -660,37 +796,35 @@ func _navigate_equipment(direction: String):
|
||||
if _has_equipment_in_slot(target_slot):
|
||||
equipment_selection_index = target_index
|
||||
else:
|
||||
# No filled slot below, move to inventory
|
||||
# No filled slot below, move to inventory (only if inventory has items)
|
||||
if inventory_rows_list.size() > 0 and inventory_items_list.size() > 0:
|
||||
selected_type = "item"
|
||||
inventory_selection_row = 0
|
||||
inventory_selection_col = current_col
|
||||
# Clamp to valid range
|
||||
if inventory_rows_list.size() > 0:
|
||||
var inv_row = inventory_rows_list[0]
|
||||
if inventory_selection_col >= inv_row.get_child_count():
|
||||
inventory_selection_col = inv_row.get_child_count() - 1
|
||||
else:
|
||||
inventory_selection_col = 0
|
||||
_update_selection_from_navigation()
|
||||
_update_selection_rectangle()
|
||||
_update_info_panel()
|
||||
return
|
||||
# No inventory items, stay on equipment
|
||||
else:
|
||||
# Already at bottom row, move to inventory
|
||||
# Already at bottom row, move to inventory (only if inventory has items)
|
||||
if inventory_rows_list.size() > 0 and inventory_items_list.size() > 0:
|
||||
selected_type = "item"
|
||||
inventory_selection_row = 0
|
||||
inventory_selection_col = current_col
|
||||
# Clamp to valid range
|
||||
if inventory_rows_list.size() > 0:
|
||||
var inv_row = inventory_rows_list[0]
|
||||
if inventory_selection_col >= inv_row.get_child_count():
|
||||
inventory_selection_col = inv_row.get_child_count() - 1
|
||||
else:
|
||||
inventory_selection_col = 0
|
||||
_update_selection_from_navigation()
|
||||
_update_selection_rectangle()
|
||||
_update_info_panel()
|
||||
return
|
||||
# No inventory items, stay on equipment
|
||||
|
||||
_update_selection_from_navigation()
|
||||
_update_selection_rectangle()
|
||||
@@ -700,6 +834,10 @@ func _on_inventory_item_pressed(item: Item):
|
||||
if not local_player or not local_player.character_stats:
|
||||
return
|
||||
|
||||
# Prevent updates during UI refresh (prevents infinite loops from focus_entered)
|
||||
if is_updating_ui:
|
||||
return
|
||||
|
||||
selected_item = item
|
||||
selected_slot = ""
|
||||
selected_type = "item"
|
||||
@@ -707,17 +845,26 @@ func _on_inventory_item_pressed(item: Item):
|
||||
# Update navigation position
|
||||
var item_index = inventory_items_list.find(item)
|
||||
if item_index >= 0:
|
||||
var items_per_row = 10
|
||||
inventory_selection_row = int(item_index / items_per_row)
|
||||
var items_per_row: int = 8
|
||||
inventory_selection_row = floor(item_index / float(items_per_row))
|
||||
inventory_selection_col = item_index % items_per_row
|
||||
|
||||
_update_selection_highlight()
|
||||
_update_selection_rectangle()
|
||||
|
||||
func _on_character_changed(_char: CharacterStats):
|
||||
_update_ui()
|
||||
# Always update stats when character changes (even if inventory is closed)
|
||||
# Equipment changes affect max HP/MP which should be reflected everywhere
|
||||
_update_stats()
|
||||
|
||||
# Only update UI if inventory is open (prevents unnecessary updates)
|
||||
if not is_open:
|
||||
return
|
||||
# Prevent recursive updates
|
||||
if is_updating_ui:
|
||||
return
|
||||
_update_ui()
|
||||
|
||||
func _input(event):
|
||||
# Toggle with Tab key
|
||||
if event is InputEventKey and event.keycode == KEY_TAB and event.pressed and not event.echo:
|
||||
@@ -773,6 +920,9 @@ func _handle_f_key():
|
||||
var equipped_item = char_stats.equipment[selected_slot]
|
||||
if equipped_item:
|
||||
char_stats.unequip_item(equipped_item)
|
||||
# Play armour sound when unequipping
|
||||
if sfx_armour:
|
||||
sfx_armour.play()
|
||||
# After unequipping, if all equipment is empty, go to inventory
|
||||
var has_any_equipment = false
|
||||
for slot in equipment_slots_list:
|
||||
@@ -832,6 +982,10 @@ func _handle_f_key():
|
||||
|
||||
char_stats.equip_item(selected_item)
|
||||
|
||||
# Play armour sound when equipping
|
||||
if sfx_armour:
|
||||
sfx_armour.play()
|
||||
|
||||
# If this was the last item, set selection state BEFORE _update_ui()
|
||||
# so that _update_selection_from_navigation() works correctly
|
||||
if was_last_item and target_slot_name != "":
|
||||
@@ -856,6 +1010,18 @@ func _use_consumable_item(item: Item):
|
||||
|
||||
var char_stats = local_player.character_stats
|
||||
|
||||
# Determine if it's a potion or food based on item name
|
||||
var is_potion = "potion" in item.item_name.to_lower()
|
||||
|
||||
# Play appropriate sound
|
||||
if is_potion:
|
||||
if sfx_potion:
|
||||
sfx_potion.play()
|
||||
else:
|
||||
# Food item
|
||||
if sfx_food:
|
||||
sfx_food.play()
|
||||
|
||||
if item.modifiers.has("hp"):
|
||||
var hp_heal = item.modifiers["hp"]
|
||||
if local_player.has_method("heal"):
|
||||
@@ -880,6 +1046,11 @@ func _handle_e_key():
|
||||
|
||||
var char_stats = local_player.character_stats
|
||||
|
||||
# Play armour sound when dropping equipment
|
||||
if selected_item.item_type == Item.ItemType.Equippable:
|
||||
if sfx_armour:
|
||||
sfx_armour.play()
|
||||
|
||||
if not selected_item in char_stats.inventory:
|
||||
return
|
||||
|
||||
@@ -893,10 +1064,24 @@ func _handle_e_key():
|
||||
if game_world:
|
||||
entities_node = game_world.get_node_or_null("Entities")
|
||||
|
||||
# In multiplayer, clients need to request server to spawn loot
|
||||
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
|
||||
# Client: send drop request to server
|
||||
if game_world and game_world.has_method("_request_item_drop"):
|
||||
game_world._request_item_drop.rpc_id(1, selected_item.save(), drop_position, local_player.get_multiplayer_authority())
|
||||
else:
|
||||
# Fallback: try to spawn locally (won't sync)
|
||||
if entities_node:
|
||||
var loot = ItemLootHelper.spawn_item_loot(selected_item, drop_position, entities_node, game_world)
|
||||
if loot:
|
||||
if local_player.has_method("get_multiplayer_authority"):
|
||||
if loot and local_player.has_method("get_multiplayer_authority"):
|
||||
var player_peer_id = local_player.get_multiplayer_authority()
|
||||
loot.set_meta("dropped_by_peer_id", player_peer_id)
|
||||
loot.set_meta("drop_time", Time.get_ticks_msec())
|
||||
else:
|
||||
# Server or single-player: spawn directly
|
||||
if entities_node:
|
||||
var loot = ItemLootHelper.spawn_item_loot(selected_item, drop_position, entities_node, game_world)
|
||||
if loot and local_player.has_method("get_multiplayer_authority"):
|
||||
var player_peer_id = local_player.get_multiplayer_authority()
|
||||
loot.set_meta("dropped_by_peer_id", player_peer_id)
|
||||
loot.set_meta("drop_time", Time.get_ticks_msec())
|
||||
@@ -913,38 +1098,34 @@ func _open_inventory():
|
||||
if is_open:
|
||||
return
|
||||
|
||||
# Workaround: On first open, immediately close and reopen to ensure proper initialization
|
||||
if is_first_open:
|
||||
is_first_open = false
|
||||
is_open = true
|
||||
if container:
|
||||
container.visible = true
|
||||
_lock_player_controls(true)
|
||||
_update_ui()
|
||||
# Wait a frame
|
||||
await get_tree().process_frame
|
||||
# Close immediately
|
||||
_close_inventory()
|
||||
# Wait a frame
|
||||
await get_tree().process_frame
|
||||
# Now reopen properly (will continue with normal flow below)
|
||||
|
||||
is_open = true
|
||||
if container:
|
||||
container.visible = true
|
||||
|
||||
_lock_player_controls(true)
|
||||
_update_ui()
|
||||
|
||||
# Initialize selection - prefer inventory, but if empty, check equipment
|
||||
if inventory_rows_list.size() > 0:
|
||||
selected_type = "item"
|
||||
inventory_selection_row = 0
|
||||
inventory_selection_col = 0
|
||||
_update_selection_from_navigation()
|
||||
_update_selection_rectangle()
|
||||
_update_info_panel()
|
||||
else:
|
||||
# No inventory items, try equipment
|
||||
var first_filled_slot = _find_next_filled_equipment_slot(-1, 1)
|
||||
if first_filled_slot >= 0:
|
||||
selected_type = "equipment"
|
||||
equipment_selection_index = first_filled_slot
|
||||
selected_slot = equipment_slots_list[first_filled_slot]
|
||||
_update_selection_from_navigation()
|
||||
_update_selection_rectangle()
|
||||
_update_info_panel()
|
||||
else:
|
||||
# Nothing to select
|
||||
# Reset selection state BEFORE updating UI (so _update_ui doesn't try to update selection)
|
||||
selected_type = ""
|
||||
if selection_rectangle:
|
||||
selection_rectangle.visible = false
|
||||
if info_label:
|
||||
info_label.text = ""
|
||||
selected_item = null
|
||||
selected_slot = ""
|
||||
|
||||
_update_ui()
|
||||
|
||||
if not local_player:
|
||||
_find_local_player()
|
||||
|
||||
@@ -55,6 +55,7 @@ var weapon_type: WeaponType = WeaponType.NONE
|
||||
var two_handed:bool = false
|
||||
var quantity = 1
|
||||
var can_have_multiple_of:bool = false
|
||||
var weight: float = 1.0 # Item weight for encumbrance system
|
||||
|
||||
func save():
|
||||
var json = {
|
||||
@@ -72,7 +73,8 @@ func save():
|
||||
"weapon_type": weapon_type,
|
||||
"two_handed": two_handed,
|
||||
"quantity": quantity,
|
||||
"can_have_multiple_of": can_have_multiple_of
|
||||
"can_have_multiple_of": can_have_multiple_of,
|
||||
"weight": weight
|
||||
}
|
||||
return json
|
||||
|
||||
@@ -117,4 +119,6 @@ func load(iDic: Dictionary):
|
||||
quantity = iDic.get("quantity")
|
||||
if iDic.has("can_have_multiple_of"):
|
||||
can_have_multiple_of = iDic.get("can_have_multiple_of")
|
||||
if iDic.has("weight"):
|
||||
weight = iDic.get("weight")
|
||||
pass
|
||||
1
src/scripts/item.gd.uid
Normal file
1
src/scripts/item.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cx2xp5myos4s7
|
||||
@@ -1014,6 +1014,7 @@ static func _load_all_items():
|
||||
"equipment_type": Item.EquipmentType.NONE,
|
||||
"weapon_type": Item.WeaponType.NONE,
|
||||
"spriteFrame": 7 * 20 + 12, # 12,7
|
||||
"weight": 0.2, # Very light consumable
|
||||
"modifiers": {"hp": 10},
|
||||
"buy_cost": 15,
|
||||
"sell_worth": 4,
|
||||
@@ -1145,6 +1146,7 @@ static func _load_all_items():
|
||||
"equipment_type": Item.EquipmentType.NONE,
|
||||
"weapon_type": Item.WeaponType.NONE,
|
||||
"spriteFrame": 8 * 20 + 15, # 15,8
|
||||
"weight": 0.3, # Light potion
|
||||
"modifiers": {"hp": 50},
|
||||
"buy_cost": 50,
|
||||
"sell_worth": 15,
|
||||
@@ -1158,6 +1160,7 @@ static func _load_all_items():
|
||||
"equipment_type": Item.EquipmentType.NONE,
|
||||
"weapon_type": Item.WeaponType.NONE,
|
||||
"spriteFrame": 8 * 20 + 16, # 16,8
|
||||
"weight": 0.3, # Light potion
|
||||
"modifiers": {"hp": 75, "mp": 75},
|
||||
"buy_cost": 100,
|
||||
"sell_worth": 30,
|
||||
@@ -1171,6 +1174,7 @@ static func _load_all_items():
|
||||
"equipment_type": Item.EquipmentType.NONE,
|
||||
"weapon_type": Item.WeaponType.NONE,
|
||||
"spriteFrame": 8 * 20 + 17, # 17,8
|
||||
"weight": 0.3, # Light potion
|
||||
"modifiers": {"dodge_chance": 0.15}, # +15% dodge chance (temporary)
|
||||
"duration": 60.0, # 60 seconds
|
||||
"buy_cost": 80,
|
||||
@@ -1185,6 +1189,7 @@ static func _load_all_items():
|
||||
"equipment_type": Item.EquipmentType.NONE,
|
||||
"weapon_type": Item.WeaponType.NONE,
|
||||
"spriteFrame": 8 * 20 + 18, # 18,8
|
||||
"weight": 0.3, # Light potion
|
||||
"modifiers": {"mp": 50},
|
||||
"buy_cost": 40,
|
||||
"sell_worth": 12,
|
||||
@@ -1198,6 +1203,7 @@ static func _load_all_items():
|
||||
"equipment_type": Item.EquipmentType.NONE,
|
||||
"weapon_type": Item.WeaponType.NONE,
|
||||
"spriteFrame": 8 * 20 + 19, # 19,8
|
||||
"weight": 0.3, # Light potion
|
||||
"modifiers": {"res_all": 25}, # +25% to all resistances
|
||||
"duration": 120.0, # 120 seconds
|
||||
"buy_cost": 120,
|
||||
@@ -1235,6 +1241,7 @@ static func create_item(item_id: String) -> Item:
|
||||
item.quantity = item_data.get("quantity", 1)
|
||||
item.can_have_multiple_of = item_data.get("can_have_multiple_of", false)
|
||||
item.duration = item_data.get("duration", 0.0)
|
||||
item.weight = item_data.get("weight", 1.0) # Default weight 1.0
|
||||
|
||||
# spritePath defaults to items_n_shit.png in Item class, which is correct
|
||||
# spriteFrames defaults to Vector2i(20,14) in Item class, which is correct
|
||||
|
||||
@@ -544,6 +544,18 @@ func _request_pickup(player_peer_id: int):
|
||||
print("Loot: _request_pickup called on non-server, ignoring")
|
||||
return
|
||||
|
||||
# Check cooldown: prevent player from picking up their own dropped item for 5 seconds
|
||||
if has_meta("dropped_by_peer_id") and has_meta("drop_time"):
|
||||
var dropped_by_peer_id = get_meta("dropped_by_peer_id")
|
||||
var drop_time = get_meta("drop_time")
|
||||
var current_time = Time.get_ticks_msec()
|
||||
var time_since_drop = (current_time - drop_time) / 1000.0 # Convert to seconds
|
||||
|
||||
if player_peer_id == dropped_by_peer_id and time_since_drop < 5.0:
|
||||
# Player can't pick up their own dropped item for 5 seconds
|
||||
print("Loot: Player ", player_peer_id, " cannot pick up item they dropped (", time_since_drop, "s ago, need 5s cooldown)")
|
||||
return
|
||||
|
||||
# Use mutex to prevent concurrent processing (race condition protection)
|
||||
if processing_pickup:
|
||||
print("Loot: Pickup already being processed, ignoring duplicate request")
|
||||
|
||||
@@ -9,6 +9,7 @@ var appearance_rng: RandomNumberGenerator # Deterministic RNG for appearance/sta
|
||||
@export var move_speed: float = 100.0
|
||||
@export var grab_range: float = 20.0
|
||||
@export var throw_force: float = 150.0
|
||||
@export var cone_light_angle: float = 180.0 # Cone spread angle in degrees (adjustable, default wider)
|
||||
|
||||
# Network identity
|
||||
var peer_id: int = 1
|
||||
@@ -60,6 +61,7 @@ var attack_cooldown: float = 0.0 # No cooldown - instant attacks!
|
||||
var is_attacking: bool = false
|
||||
var sword_slash_scene = preload("res://scenes/sword_slash.tscn") # Old rotating version (kept for reference)
|
||||
var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn") # New projectile version
|
||||
var attack_arrow_scene = preload("res://scenes/attack_arrow.tscn")
|
||||
var blood_scene = preload("res://scenes/blood_clot.tscn")
|
||||
|
||||
# Simulated Z-axis for height (when thrown)
|
||||
@@ -92,6 +94,7 @@ var is_airborne: bool = false
|
||||
@onready var sprite_addons = $Sprite2DAddons
|
||||
@onready var sprite_headgear = $Sprite2DHeadgear
|
||||
@onready var sprite_weapon = $Sprite2DWeapon
|
||||
@onready var cone_light = $ConeLight
|
||||
|
||||
# Player stats (legacy - now using character_stats)
|
||||
var max_health: float:
|
||||
@@ -266,6 +269,12 @@ func _ready():
|
||||
if interaction_indicator:
|
||||
interaction_indicator.visible = false
|
||||
|
||||
# Set up cone light blend mode, texture, initial rotation, and spread
|
||||
if cone_light:
|
||||
_create_cone_light_texture()
|
||||
_update_cone_light_rotation()
|
||||
_update_cone_light_spread()
|
||||
|
||||
# Wait before allowing RPCs to ensure player is fully spawned on all clients
|
||||
# This prevents "Node not found" errors when RPCs try to resolve node paths
|
||||
if multiplayer.is_server():
|
||||
@@ -600,7 +609,7 @@ func _on_character_changed(_char: CharacterStats):
|
||||
# Update appearance when character stats change (e.g., equipment)
|
||||
_apply_appearance_to_sprites()
|
||||
|
||||
# Sync equipment changes to other clients
|
||||
# Sync equipment changes to other clients (when authority player changes equipment)
|
||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||
# Sync equipment to all clients
|
||||
var equipment_data = {}
|
||||
@@ -612,6 +621,32 @@ func _on_character_changed(_char: CharacterStats):
|
||||
equipment_data[slot_name] = null
|
||||
_sync_equipment.rpc(equipment_data)
|
||||
|
||||
# Sync equipment and inventory to client (when server adds/removes items for a client player)
|
||||
# This ensures joiners see items they pick up and equipment changes
|
||||
# This must be checked separately from the authority-based sync because on the server,
|
||||
# a joiner's player has authority set to their peer_id, not the server's unique_id
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and can_send_rpcs and is_inside_tree():
|
||||
var the_peer_id = get_multiplayer_authority()
|
||||
# Only sync if this is a client player (not server's own player)
|
||||
if the_peer_id != 0 and the_peer_id != multiplayer.get_unique_id():
|
||||
# Sync equipment
|
||||
var equipment_data = {}
|
||||
for slot_name in character_stats.equipment.keys():
|
||||
var item = character_stats.equipment[slot_name]
|
||||
if item:
|
||||
equipment_data[slot_name] = item.save()
|
||||
else:
|
||||
equipment_data[slot_name] = null
|
||||
_sync_equipment.rpc_id(the_peer_id, equipment_data)
|
||||
|
||||
# Sync inventory
|
||||
var inventory_data = []
|
||||
for item in character_stats.inventory:
|
||||
if item:
|
||||
inventory_data.append(item.save())
|
||||
_sync_inventory.rpc_id(the_peer_id, inventory_data)
|
||||
print(name, " syncing equipment and inventory to client peer_id=", the_peer_id, " inventory_size=", inventory_data.size())
|
||||
|
||||
func _get_player_color() -> Color:
|
||||
# Legacy function - now returns white (no color tint)
|
||||
return Color.WHITE
|
||||
@@ -700,6 +735,107 @@ func _set_animation(anim_name: String):
|
||||
current_frame = 0
|
||||
time_since_last_frame = 0.0
|
||||
|
||||
# Convert Direction enum to angle in radians for light rotation
|
||||
func _direction_to_angle(direction: int) -> float:
|
||||
match direction:
|
||||
Direction.DOWN:
|
||||
return PI / 2.0 # 90 degrees
|
||||
Direction.DOWN_RIGHT:
|
||||
return PI / 4.0 # 45 degrees
|
||||
Direction.RIGHT:
|
||||
return 0.0 # 0 degrees
|
||||
Direction.UP_RIGHT:
|
||||
return -PI / 4.0 # -45 degrees
|
||||
Direction.UP:
|
||||
return -PI / 2.0 # -90 degrees
|
||||
Direction.UP_LEFT:
|
||||
return -3.0 * PI / 4.0 # -135 degrees
|
||||
Direction.LEFT:
|
||||
return PI # 180 degrees
|
||||
Direction.DOWN_LEFT:
|
||||
return 3.0 * PI / 4.0 # 135 degrees
|
||||
_:
|
||||
return PI / 2.0 # Default to DOWN
|
||||
|
||||
# Update cone light rotation based on player's facing direction
|
||||
func _update_cone_light_rotation():
|
||||
if cone_light:
|
||||
cone_light.rotation = _direction_to_angle(current_direction)+(PI/2)
|
||||
|
||||
# Create a cone-shaped light texture programmatically
|
||||
# Creates a directional cone texture that extends forward and fades to the sides
|
||||
func _create_cone_light_texture():
|
||||
if not cone_light:
|
||||
return
|
||||
|
||||
# Create a square texture (recommended size for lights)
|
||||
var texture_size = 256
|
||||
var image = Image.create(texture_size, texture_size, false, Image.FORMAT_RGBA8)
|
||||
|
||||
var center = Vector2(texture_size / 2.0, texture_size / 2.0)
|
||||
var max_distance = texture_size / 2.0
|
||||
|
||||
# Cone parameters (these control the shape)
|
||||
var cone_angle_rad = deg_to_rad(cone_light_angle) # Convert to radians
|
||||
var half_cone = cone_angle_rad / 2.0
|
||||
var forward_dir = Vector2(0, -1) # Pointing up in texture space (will be rotated by light rotation)
|
||||
|
||||
for x in range(texture_size):
|
||||
for y in range(texture_size):
|
||||
var pos = Vector2(x, y)
|
||||
var offset = pos - center
|
||||
var distance = offset.length()
|
||||
|
||||
if distance > 0.0:
|
||||
# Normalize offset to get direction
|
||||
var dir = offset / distance
|
||||
|
||||
# Calculate angle from forward direction
|
||||
# forward_dir is (0, -1) which has angle -PI/2
|
||||
# We want to find the angle difference
|
||||
var pixel_angle = dir.angle() # Angle of pixel direction
|
||||
var forward_angle = forward_dir.angle() # Angle of forward direction (-PI/2)
|
||||
|
||||
# Calculate angle difference (wrapped to -PI to PI)
|
||||
var angle_diff = pixel_angle - forward_angle
|
||||
# Normalize to -PI to PI range
|
||||
angle_diff = fmod(angle_diff + PI, 2.0 * PI) - PI
|
||||
var abs_angle_diff = abs(angle_diff)
|
||||
|
||||
# Check if within cone angle (hard edge - no smooth falloff)
|
||||
if abs_angle_diff <= half_cone:
|
||||
# Within cone - calculate brightness
|
||||
var normalized_distance = distance / max_distance
|
||||
|
||||
# Fade based on distance (from center) - keep distance falloff
|
||||
# Hard edge for angle (pixely) - no smoothstep on angle
|
||||
var distance_factor = 1.0 - smoothstep(0.0, 1.0, normalized_distance)
|
||||
var alpha = distance_factor # Hard edge on angle, smooth fade on distance
|
||||
var color = Color(1.0, 1.0, 1.0, alpha)
|
||||
image.set_pixel(x, y, color)
|
||||
else:
|
||||
# Outside cone - transparent (hard edge)
|
||||
image.set_pixel(x, y, Color.TRANSPARENT)
|
||||
else:
|
||||
# Center point - full brightness
|
||||
image.set_pixel(x, y, Color.WHITE)
|
||||
|
||||
# Create ImageTexture from the image
|
||||
var texture = ImageTexture.create_from_image(image)
|
||||
cone_light.texture = texture
|
||||
|
||||
# Update cone light spread/angle
|
||||
# Recreates the texture with the new angle to properly show the cone shape
|
||||
func _update_cone_light_spread():
|
||||
if cone_light:
|
||||
# Recreate the texture with the new angle
|
||||
_create_cone_light_texture()
|
||||
|
||||
# Set the cone light angle (in degrees) and update the light
|
||||
func set_cone_light_angle(angle_degrees: float):
|
||||
cone_light_angle = angle_degrees
|
||||
_update_cone_light_spread()
|
||||
|
||||
# Helper function to snap direction to 8-way directions
|
||||
func _snap_to_8_directions(direction: Vector2) -> Vector2:
|
||||
if direction.length() < 0.1:
|
||||
@@ -803,10 +939,19 @@ func _physics_process(delta):
|
||||
|
||||
# Skip input if controls are disabled (e.g., when inventory is open)
|
||||
# But still allow knockback to continue (handled above)
|
||||
var skip_input = controls_disabled
|
||||
if controls_disabled:
|
||||
if not is_knocked_back:
|
||||
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0) # Slow down movement
|
||||
return
|
||||
# Immediately stop movement when controls are disabled (e.g., inventory opened)
|
||||
velocity = Vector2.ZERO
|
||||
# Reset animation to IDLE if not in a special state
|
||||
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD":
|
||||
if is_lifting:
|
||||
_set_animation("IDLE_HOLD")
|
||||
elif is_pushing:
|
||||
_set_animation("IDLE_PUSH")
|
||||
else:
|
||||
_set_animation("IDLE")
|
||||
|
||||
# Check if being held by someone
|
||||
var being_held_by_someone = false
|
||||
@@ -823,8 +968,8 @@ func _physics_process(delta):
|
||||
# During knockback, no input control - just let velocity carry the player
|
||||
# Apply friction to slow down knockback
|
||||
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
|
||||
elif not is_airborne:
|
||||
# Normal input handling
|
||||
elif not is_airborne and not skip_input:
|
||||
# Normal input handling (only if controls are not disabled)
|
||||
struggle_time = 0.0 # Reset struggle timer
|
||||
struggle_direction = Vector2.ZERO
|
||||
_handle_input()
|
||||
@@ -1006,11 +1151,17 @@ func _handle_input():
|
||||
last_movement_direction = input_vector.normalized()
|
||||
|
||||
# Update facing direction (except when pushing - locked direction)
|
||||
var new_direction = current_direction
|
||||
if not is_pushing:
|
||||
current_direction = _get_direction_from_vector(input_vector) as Direction
|
||||
new_direction = _get_direction_from_vector(input_vector) as Direction
|
||||
else:
|
||||
# Keep locked direction when pushing
|
||||
current_direction = push_direction_locked as Direction
|
||||
new_direction = push_direction_locked as Direction
|
||||
|
||||
# Update direction and cone light rotation if changed
|
||||
if new_direction != current_direction:
|
||||
current_direction = new_direction
|
||||
_update_cone_light_rotation()
|
||||
|
||||
# Set animation based on state
|
||||
if is_lifting:
|
||||
@@ -1027,7 +1178,9 @@ func _handle_input():
|
||||
elif is_pushing:
|
||||
_set_animation("IDLE_PUSH")
|
||||
# Keep locked direction when pushing
|
||||
if push_direction_locked != current_direction:
|
||||
current_direction = push_direction_locked as Direction
|
||||
_update_cone_light_rotation()
|
||||
else:
|
||||
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD":
|
||||
_set_animation("IDLE")
|
||||
@@ -1054,7 +1207,13 @@ func _handle_input():
|
||||
was_dragging_last_frame = is_dragging_now
|
||||
|
||||
# Reduce speed by half when pushing/pulling
|
||||
var current_speed = move_speed * (0.5 if is_pushing else 1.0)
|
||||
# Calculate speed with encumbrance penalty
|
||||
var base_speed = move_speed * (0.5 if is_pushing else 1.0)
|
||||
var current_speed = base_speed
|
||||
|
||||
# Apply encumbrance penalty (1/4 speed if over-encumbered)
|
||||
if character_stats and character_stats.is_over_encumbered():
|
||||
current_speed = base_speed * 0.25
|
||||
velocity = input_vector * current_speed
|
||||
|
||||
func _handle_movement(_delta):
|
||||
@@ -1611,7 +1770,19 @@ func _perform_attack():
|
||||
can_attack = false
|
||||
is_attacking = true
|
||||
|
||||
# Play attack animation
|
||||
# Check what weapon is equipped
|
||||
var equipped_weapon = null
|
||||
if character_stats and character_stats.equipment.has("mainhand"):
|
||||
equipped_weapon = character_stats.equipment["mainhand"]
|
||||
|
||||
var is_bow = false
|
||||
if equipped_weapon and equipped_weapon.weapon_type == Item.WeaponType.BOW:
|
||||
is_bow = true
|
||||
|
||||
# Play attack animation based on weapon
|
||||
if is_bow:
|
||||
_set_animation("BOW")
|
||||
else:
|
||||
_set_animation("SWORD")
|
||||
|
||||
# Calculate attack direction based on player's facing direction
|
||||
@@ -1634,7 +1805,7 @@ func _perform_attack():
|
||||
Direction.UP_RIGHT:
|
||||
attack_direction = Vector2(1, -1).normalized()
|
||||
|
||||
# Delay before spawning sword slash
|
||||
# Delay before spawning projectile
|
||||
await get_tree().create_timer(0.15).timeout
|
||||
|
||||
# Calculate damage from character_stats with randomization
|
||||
@@ -1659,7 +1830,39 @@ func _perform_attack():
|
||||
# Round to 1 decimal place
|
||||
final_damage = round(final_damage * 10.0) / 10.0
|
||||
|
||||
# Spawn sword projectile
|
||||
# Handle bow attacks - require arrows in off-hand
|
||||
if is_bow:
|
||||
# Check for arrows in off-hand
|
||||
var arrows = null
|
||||
if character_stats and character_stats.equipment.has("offhand"):
|
||||
var offhand_item = character_stats.equipment["offhand"]
|
||||
if offhand_item and offhand_item.weapon_type == Item.WeaponType.AMMUNITION:
|
||||
arrows = offhand_item
|
||||
|
||||
# Only spawn arrow if we have arrows
|
||||
if arrows and arrows.quantity > 0:
|
||||
if attack_arrow_scene:
|
||||
var arrow_projectile = attack_arrow_scene.instantiate()
|
||||
get_parent().add_child(arrow_projectile)
|
||||
arrow_projectile.shoot(attack_direction, global_position, self)
|
||||
# Consume one arrow
|
||||
arrows.quantity -= 1
|
||||
var remaining = arrows.quantity
|
||||
if arrows.quantity <= 0:
|
||||
# Remove arrows if quantity reaches 0
|
||||
character_stats.equipment["offhand"] = null
|
||||
if character_stats:
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
else:
|
||||
# Update equipment to reflect quantity change
|
||||
if character_stats:
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
print(name, " shot arrow! Arrows remaining: ", remaining)
|
||||
else:
|
||||
# Play bow animation but no projectile
|
||||
print(name, " tried to shoot but has no arrows!")
|
||||
else:
|
||||
# Spawn sword projectile for non-bow weapons
|
||||
if sword_projectile_scene:
|
||||
var projectile = sword_projectile_scene.instantiate()
|
||||
get_parent().add_child(projectile)
|
||||
@@ -2572,15 +2775,19 @@ func _sync_stats_update(kills_count: int, coins_count: int):
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_equipment(equipment_data: Dictionary):
|
||||
# Client receives equipment update from server
|
||||
# Update equipment to match other players
|
||||
# Only process if we're not the authority (remote player)
|
||||
if is_multiplayer_authority():
|
||||
return # Authority ignores this (it's the sender)
|
||||
|
||||
# Client receives equipment update from server or other clients
|
||||
# Update equipment to match server/other players
|
||||
# Server also needs to accept equipment sync from clients (joiners) to update server's copy of joiner's player
|
||||
if not character_stats:
|
||||
return
|
||||
|
||||
# On server, only accept if this is a client player (not server's own player)
|
||||
if multiplayer.is_server():
|
||||
var the_peer_id = get_multiplayer_authority()
|
||||
# If this is the server's own player, ignore (server's own changes are handled differently)
|
||||
if the_peer_id == 0 or the_peer_id == multiplayer.get_unique_id():
|
||||
return
|
||||
|
||||
# Update equipment from data
|
||||
for slot_name in equipment_data.keys():
|
||||
var item_data = equipment_data[slot_name]
|
||||
@@ -2591,7 +2798,29 @@ func _sync_equipment(equipment_data: Dictionary):
|
||||
|
||||
# Update appearance
|
||||
_apply_appearance_to_sprites()
|
||||
print(name, " equipment synced from server")
|
||||
print(name, " equipment synced: ", equipment_data.size(), " slots")
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_inventory(inventory_data: Array):
|
||||
# Client receives inventory update from server
|
||||
# Update inventory to match server's inventory
|
||||
# Unlike _sync_equipment, we WANT to receive our own inventory from the server
|
||||
# So we check if we're the server (sender) and ignore, not if we're the authority
|
||||
if multiplayer.is_server():
|
||||
return # Server ignores this (it's the sender)
|
||||
|
||||
if not character_stats:
|
||||
return
|
||||
|
||||
# Clear and rebuild inventory from server data
|
||||
character_stats.inventory.clear()
|
||||
for item_data in inventory_data:
|
||||
if item_data != null:
|
||||
character_stats.inventory.append(Item.new(item_data))
|
||||
|
||||
# Emit character_changed to update UI
|
||||
character_stats.character_changed.emit(character_stats)
|
||||
print(name, " inventory synced from server: ", character_stats.inventory.size(), " items")
|
||||
|
||||
func heal(amount: float):
|
||||
if is_dead:
|
||||
|
||||
259
src/scripts/room_lighting_system.gd
Normal file
259
src/scripts/room_lighting_system.gd
Normal file
@@ -0,0 +1,259 @@
|
||||
extends Node2D
|
||||
|
||||
# Room Lighting System
|
||||
# Manages per-room darkness and fog of war based on player lights and torch count
|
||||
|
||||
@onready var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
|
||||
# Room lighting data
|
||||
var room_lighting_data: Dictionary = {} # room_id -> {lit: bool, torch_count: int, darkness_level: float}
|
||||
var room_darkness_overlays: Dictionary = {} # room_id -> ColorRect node
|
||||
|
||||
# Constants
|
||||
const TILE_SIZE = 16 # Each tile is 16x16 pixels
|
||||
const DARKNESS_COLOR = Color(0, 0, 0, 0.95) # Almost black, slightly transparent
|
||||
const MIN_DARKNESS = 0.3 # Minimum darkness even with torches
|
||||
const MAX_DARKNESS = 0.95 # Maximum darkness (no torches, unlit)
|
||||
|
||||
# Light detection
|
||||
var light_check_timer: float = 0.0
|
||||
const LIGHT_CHECK_INTERVAL = 0.1 # Check every 0.1 seconds
|
||||
|
||||
var darkness_layer: Node2D = null
|
||||
|
||||
func _ready():
|
||||
# Create Node2D for darkness overlays (in world space, above game world)
|
||||
darkness_layer = Node2D.new()
|
||||
darkness_layer.name = "RoomDarknessLayer"
|
||||
darkness_layer.z_index = 100 # High z-index to be above game world
|
||||
add_child(darkness_layer)
|
||||
|
||||
# Wait for dungeon to be generated
|
||||
call_deferred("_initialize_room_lighting")
|
||||
|
||||
func _initialize_room_lighting():
|
||||
# Wait for dungeon data to be available
|
||||
if not game_world or game_world.dungeon_data.is_empty():
|
||||
await get_tree().process_frame
|
||||
call_deferred("_initialize_room_lighting")
|
||||
return
|
||||
|
||||
var dungeon_data = game_world.dungeon_data
|
||||
if not dungeon_data.has("rooms") or not dungeon_data.has("torches"):
|
||||
print("RoomLightingSystem: Dungeon data not ready yet")
|
||||
await get_tree().process_frame
|
||||
call_deferred("_initialize_room_lighting")
|
||||
return
|
||||
|
||||
# Clear old room lighting data and overlays when reinitializing
|
||||
room_lighting_data.clear()
|
||||
if darkness_layer:
|
||||
for child in darkness_layer.get_children():
|
||||
child.queue_free()
|
||||
room_darkness_overlays.clear()
|
||||
|
||||
# Count torches per room
|
||||
var torch_counts: Dictionary = {} # room_id -> count
|
||||
var rooms = dungeon_data.rooms
|
||||
var torches = dungeon_data.torches
|
||||
|
||||
# Initialize all rooms as unlit
|
||||
for i in range(rooms.size()):
|
||||
var room = rooms[i]
|
||||
var room_id = _get_room_id(room)
|
||||
torch_counts[room_id] = 0
|
||||
room_lighting_data[room_id] = {
|
||||
"lit": false,
|
||||
"torch_count": 0,
|
||||
"darkness_level": MAX_DARKNESS
|
||||
}
|
||||
|
||||
# Count torches in each room
|
||||
for torch_data in torches:
|
||||
var torch_pos = torch_data.position
|
||||
# Find which room this torch belongs to
|
||||
for i in range(rooms.size()):
|
||||
var room = rooms[i]
|
||||
if _is_position_in_room(torch_pos, room):
|
||||
var room_id = _get_room_id(room)
|
||||
torch_counts[room_id] = torch_counts.get(room_id, 0) + 1
|
||||
break
|
||||
|
||||
# Update room lighting data with torch counts
|
||||
for room_id in torch_counts:
|
||||
var torch_count = torch_counts[room_id]
|
||||
room_lighting_data[room_id].torch_count = torch_count
|
||||
# Calculate darkness level based on torch count (0-4 torches)
|
||||
# More torches = less darkness
|
||||
var darkness = MAX_DARKNESS - (torch_count * 0.15) # Each torch reduces darkness by 0.15
|
||||
darkness = clamp(darkness, MIN_DARKNESS, MAX_DARKNESS)
|
||||
room_lighting_data[room_id].darkness_level = darkness
|
||||
|
||||
# Create darkness overlays for all rooms (initially all dark)
|
||||
_create_darkness_overlays()
|
||||
|
||||
print("RoomLightingSystem: Initialized ", rooms.size(), " rooms with lighting data")
|
||||
|
||||
# Public method to reinitialize lighting (called when new level is generated)
|
||||
func reinitialize():
|
||||
call_deferred("_initialize_room_lighting")
|
||||
|
||||
func _get_room_id(room: Dictionary) -> String:
|
||||
# Create unique ID from room position and size
|
||||
return "%d_%d_%d_%d" % [room.x, room.y, room.w, room.h]
|
||||
|
||||
func _is_position_in_room(pos: Vector2, room: Dictionary) -> bool:
|
||||
# Convert room tile coordinates to world coordinates
|
||||
var room_world_x = room.x * TILE_SIZE
|
||||
var room_world_y = room.y * TILE_SIZE
|
||||
var room_world_w = room.w * TILE_SIZE
|
||||
var room_world_h = room.h * TILE_SIZE
|
||||
|
||||
# Check if position is within room bounds (including walls)
|
||||
return pos.x >= room_world_x and pos.x < room_world_x + room_world_w and \
|
||||
pos.y >= room_world_y and pos.y < room_world_y + room_world_h
|
||||
|
||||
func _create_darkness_overlays():
|
||||
if not darkness_layer:
|
||||
push_error("RoomLightingSystem: Darkness layer not found!")
|
||||
return
|
||||
|
||||
if not game_world or game_world.dungeon_data.is_empty():
|
||||
return
|
||||
|
||||
var rooms = game_world.dungeon_data.rooms
|
||||
|
||||
# Create darkness overlay for each room
|
||||
for i in range(rooms.size()):
|
||||
var room = rooms[i]
|
||||
var room_id = _get_room_id(room)
|
||||
|
||||
# Create ColorRect for darkness overlay
|
||||
var overlay = ColorRect.new()
|
||||
overlay.name = "Darkness_%s" % room_id
|
||||
|
||||
# Set position and size (in world coordinates)
|
||||
var room_world_x = room.x * TILE_SIZE
|
||||
var room_world_y = room.y * TILE_SIZE
|
||||
var room_world_w = room.w * TILE_SIZE
|
||||
var room_world_h = room.h * TILE_SIZE
|
||||
|
||||
overlay.position = Vector2(room_world_x, room_world_y)
|
||||
overlay.size = Vector2(room_world_w, room_world_h)
|
||||
|
||||
# Set darkness color based on torch count
|
||||
var lighting_data = room_lighting_data.get(room_id, {})
|
||||
var darkness_level = lighting_data.get("darkness_level", MAX_DARKNESS)
|
||||
var is_lit = lighting_data.get("lit", false)
|
||||
|
||||
# If room is lit, make it transparent (no darkness)
|
||||
# Otherwise, apply darkness based on torch count
|
||||
if is_lit:
|
||||
overlay.color = Color(0, 0, 0, 0) # Transparent (lit)
|
||||
else:
|
||||
overlay.color = Color(0, 0, 0, darkness_level) # Dark (unlit)
|
||||
|
||||
darkness_layer.add_child(overlay)
|
||||
room_darkness_overlays[room_id] = overlay
|
||||
|
||||
func _process(delta):
|
||||
light_check_timer += delta
|
||||
if light_check_timer >= LIGHT_CHECK_INTERVAL:
|
||||
light_check_timer = 0.0
|
||||
_check_player_lights()
|
||||
|
||||
func _check_player_lights():
|
||||
if not game_world or game_world.dungeon_data.is_empty():
|
||||
return
|
||||
|
||||
var rooms = game_world.dungeon_data.rooms
|
||||
var players = get_tree().get_nodes_in_group("player")
|
||||
|
||||
# Check each room against each player's lights
|
||||
for room in rooms:
|
||||
var room_id = _get_room_id(room)
|
||||
var was_lit = room_lighting_data[room_id].lit
|
||||
|
||||
# Check if any player's light intersects this room
|
||||
var is_lit_now = false
|
||||
for player in players:
|
||||
if _player_light_intersects_room(player, room):
|
||||
is_lit_now = true
|
||||
break
|
||||
|
||||
# Update lighting state
|
||||
if is_lit_now and not was_lit:
|
||||
# Room just became lit
|
||||
room_lighting_data[room_id].lit = true
|
||||
_update_room_darkness(room_id)
|
||||
elif not is_lit_now and was_lit:
|
||||
# Room became unlit (shouldn't happen, but handle it)
|
||||
# Actually, we keep rooms lit once they've been seen
|
||||
pass
|
||||
|
||||
func _player_light_intersects_room(player: Node2D, room: Dictionary) -> bool:
|
||||
# Check if player's cone light or point light intersects the room
|
||||
var player_pos = player.global_position
|
||||
|
||||
# Get room bounds in world coordinates
|
||||
var room_world_x = room.x * TILE_SIZE
|
||||
var room_world_y = room.y * TILE_SIZE
|
||||
var room_world_w = room.w * TILE_SIZE
|
||||
var room_world_h = room.h * TILE_SIZE
|
||||
var room_rect = Rect2(room_world_x, room_world_y, room_world_w, room_world_h)
|
||||
|
||||
# Check cone light (PointLight2D named "ConeLight")
|
||||
var cone_light = player.get_node_or_null("ConeLight")
|
||||
if cone_light:
|
||||
# Get light range from texture or use default
|
||||
# Cone light texture is 256x256, so range is approximately 128 pixels
|
||||
var light_range = 128.0 # Approximate range of cone light
|
||||
var light_pos = cone_light.global_position
|
||||
|
||||
# Check if light circle intersects room rectangle
|
||||
if _circle_intersects_rect(light_pos, light_range, room_rect):
|
||||
return true
|
||||
|
||||
# Check point light (PointLight2D) - even if not visible, it might be used
|
||||
var point_light = player.get_node_or_null("PointLight2D")
|
||||
if point_light:
|
||||
# Get light range from texture or use default
|
||||
# Point light has a gradient texture, estimate range
|
||||
var light_range = 64.0 # Approximate range of point light
|
||||
var light_pos = point_light.global_position
|
||||
|
||||
# Check if light circle intersects room rectangle
|
||||
if _circle_intersects_rect(light_pos, light_range, room_rect):
|
||||
return true
|
||||
|
||||
# Also check if player is inside the room (they can see it)
|
||||
if room_rect.has_point(player_pos):
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
func _circle_intersects_rect(circle_center: Vector2, circle_radius: float, rect: Rect2) -> bool:
|
||||
# Find the closest point on the rectangle to the circle center
|
||||
var closest_x = clamp(circle_center.x, rect.position.x, rect.position.x + rect.size.x)
|
||||
var closest_y = clamp(circle_center.y, rect.position.y, rect.position.y + rect.size.y)
|
||||
var closest_point = Vector2(closest_x, closest_y)
|
||||
|
||||
# Check if the closest point is within the circle
|
||||
var distance = circle_center.distance_to(closest_point)
|
||||
return distance <= circle_radius
|
||||
|
||||
func _update_room_darkness(room_id: String):
|
||||
var overlay = room_darkness_overlays.get(room_id)
|
||||
if not overlay:
|
||||
return
|
||||
|
||||
var lighting_data = room_lighting_data.get(room_id, {})
|
||||
var is_lit = lighting_data.get("lit", false)
|
||||
var darkness_level = lighting_data.get("darkness_level", MAX_DARKNESS)
|
||||
|
||||
# If room is lit, make overlay transparent
|
||||
# Otherwise, apply darkness based on torch count
|
||||
if is_lit:
|
||||
overlay.color = Color(0, 0, 0, 0) # Transparent (lit)
|
||||
else:
|
||||
overlay.color = Color(0, 0, 0, darkness_level) # Dark (unlit)
|
||||
1
src/scripts/room_lighting_system.gd.uid
Normal file
1
src/scripts/room_lighting_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://mgdw7x3bar6f
|
||||
29
src/shaders/light_cone.gdshader
Normal file
29
src/shaders/light_cone.gdshader
Normal file
@@ -0,0 +1,29 @@
|
||||
shader_type canvas_item;
|
||||
|
||||
uniform float cone_angle : hint_range(0.0, 3.14) = 0.8; // Total width of beam
|
||||
uniform float direction_degrees : hint_range(0.0, 360.0) = 0.0;
|
||||
uniform float feather : hint_range(0.0, 1.0) = 0.05;
|
||||
|
||||
void fragment() {
|
||||
// 1. Get the UVs centered at (0.5, 0.5)
|
||||
vec2 uv = UV - vec2(0.5);
|
||||
|
||||
// 2. Calculate the current pixel angle
|
||||
// atan2 returns values from -PI to PI
|
||||
float pixel_angle = atan(uv.y, uv.x);
|
||||
|
||||
// 3. Convert uniform direction to radians
|
||||
float target_rad = radians(direction_degrees);
|
||||
|
||||
// 4. Calculate difference between angles (wrapped)
|
||||
float angle_diff = abs(atan(sin(pixel_angle - target_rad), cos(pixel_angle - target_rad)));
|
||||
|
||||
// 5. Create the cone mask
|
||||
// We compare the difference to half the cone angle
|
||||
float half_cone = cone_angle * 0.5;
|
||||
float mask = 1.0 - smoothstep(half_cone - feather, half_cone, angle_diff);
|
||||
|
||||
// 6. Apply mask to the texture
|
||||
vec4 tex_color = texture(TEXTURE, UV);
|
||||
COLOR = vec4(tex_color.rgb, tex_color.a * mask);
|
||||
}
|
||||
1
src/shaders/light_cone.gdshader.uid
Normal file
1
src/shaders/light_cone.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ce7sy7vkt3qr2
|
||||
Reference in New Issue
Block a user