started working on fog darkness

This commit is contained in:
2026-01-13 00:16:08 +01:00
parent 82a70aa6a2
commit 89a41397d1
30 changed files with 1613 additions and 386 deletions

Binary file not shown.

View 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

Binary file not shown.

View 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

Binary file not shown.

View 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

View File

@@ -2,30 +2,145 @@
[ext_resource type="Texture2D" uid="uid://c4ee36hr5f766" path="res://assets/gfx/RPG DUNGEON VOL 3.png" id="1_e3020"] [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"] [sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_1bvp3"]
texture = ExtResource("1_e3020") texture = ExtResource("1_e3020")
separation = Vector2i(1, 1) separation = Vector2i(1, 1)
0:0/0 = 0 0:0/0 = 0
0:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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 = 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) 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 = 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) 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 = 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) 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 = 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) 4:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
5:0/0 = 0 5:0/0 = 0
6:0/0 = 0 6:0/0 = 0
7: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) 7:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
8:0/0 = 0 8:0/0 = 0
9: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) 9:0/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
10:0/0 = 0 10:0/0 = 0
11: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) 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 = 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) 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 = 0
1:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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 = 0
3:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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 = 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) 4:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
5:1/0 = 0 5:1/0 = 0
6:1/0 = 0 6:1/0 = 0
@@ -46,15 +162,19 @@ separation = Vector2i(1, 1)
11:1/0 = 0 11:1/0 = 0
12:1/0 = 0 12:1/0 = 0
0:2/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) 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 = 0
1:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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 = 0
2:2/0/occlusion_layer_0/polygon_0/polygon = SubResource("OccluderPolygon2D_c2t7l")
3:2/0 = 0 3:2/0 = 0
3:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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 = 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) 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 = 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) 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 = 0
6:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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 = 0
10:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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 = 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) 11:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
12:2/0 = 0 12:2/0 = 0
0:3/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) 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 = 0
1:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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 = 0
3:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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 = 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) 4:3/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
5:3/0 = 0 5:3/0 = 0
6:3/0 = 0 6:3/0 = 0
@@ -83,16 +206,22 @@ separation = Vector2i(1, 1)
11:3/0 = 0 11:3/0 = 0
12:3/0 = 0 12:3/0 = 0
0:4/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) 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 = 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) 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 = 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) 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 = 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) 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 = 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) 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 = 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) 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 = 0
6:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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 = 0
10:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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 = 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) 11:4/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
12:4/0 = 0 12:4/0 = 0
0:5/0 = 0 0:5/0 = 0
@@ -124,8 +254,10 @@ separation = Vector2i(1, 1)
1:6/0 = 0 1:6/0 = 0
1:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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 = 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) 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 = 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) 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 = 0
4:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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) 5:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
6:6/0 = 0 6:6/0 = 0
7: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) 7:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
8:6/0 = 0 8:6/0 = 0
9: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) 9:6/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
10:6/0 = 0 10:6/0 = 0
11:6/0 = 0 11:6/0 = 0
12:6/0 = 0 12:6/0 = 0
0:7/0 = 0 0:7/0 = 0
1: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) 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 = 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) 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 = 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) 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 = 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) 4:7/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
5:7/0 = 0 5:7/0 = 0
6:7/0 = 0 6:7/0 = 0
@@ -165,12 +303,16 @@ separation = Vector2i(1, 1)
18:7/0 = 0 18:7/0 = 0
19:7/0 = 0 19:7/0 = 0
1:8/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) 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 = 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) 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 = 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) 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 = 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) 4:8/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
5:8/0 = 0 5:8/0 = 0
8:8/0 = 0 8:8/0 = 0
@@ -190,8 +332,10 @@ separation = Vector2i(1, 1)
1:9/0 = 0 1:9/0 = 0
1:9/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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 = 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) 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 = 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) 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 = 0
4:9/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8) 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 13:15/0 = 0
[resource] [resource]
occlusion_layer_0/light_mask = 1
physics_layer_0/collision_layer = 64 physics_layer_0/collision_layer = 64
physics_layer_0/collision_mask = 0 physics_layer_0/collision_mask = 0
custom_data_layer_0/name = "terrain" custom_data_layer_0/name = "terrain"

View File

@@ -17,12 +17,19 @@ run/max_fps=60
boot_splash/bg_color=Color(0.09375, 0.09375, 0.09375, 1) boot_splash/bg_color=Color(0.09375, 0.09375, 0.09375, 1)
config/icon="res://icon.svg" config/icon="res://icon.svg"
[audio]
buses/default_bus_layout="uid://psistrevppd1"
[autoload] [autoload]
NetworkManager="*res://scripts/network_manager.gd" NetworkManager="*res://scripts/network_manager.gd"
[display] [display]
window/size/viewport_width=1280
window/size/viewport_height=720
window/stretch/mode="canvas_items"
window/stretch/scale_mode="integer" window/stretch/scale_mode="integer"
[input] [input]

View File

@@ -2,10 +2,25 @@
[ext_resource type="Script" uid="uid://c0wywibyp77c" path="res://scripts/enemy_bat.gd" id="1"] [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="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"] [sub_resource type="CircleShape2D" id="CircleShape2D_bat"]
radius = 6.0 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] [node name="EnemyBat" type="CharacterBody2D" unique_id=909833829]
collision_layer = 2 collision_layer = 2
script = ExtResource("1") script = ExtResource("1")
@@ -29,3 +44,15 @@ frame = 2
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=897277405] [node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=897277405]
shape = SubResource("CircleShape2D_bat") 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

View File

@@ -4,6 +4,7 @@
[ext_resource type="PackedScene" uid="uid://cxfvw8y7jqn2p" path="res://scenes/player.tscn" id="2"] [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://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" 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="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"] [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] [node name="CanvasModulate" type="CanvasModulate" parent="." unique_id=948490815]
light_mask = 1048575 light_mask = 1048575
visibility_layer = 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] [node name="AudioStreamPlayer2D" type="AudioStreamPlayer2D" parent="." unique_id=1141138343]
stream = ExtResource("6_6c6v5") stream = ExtResource("6_6c6v5")

View File

@@ -1,6 +1,12 @@
[gd_scene format=3 uid="uid://cxs0ybxk2blth"] [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="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"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_selection"]
bg_color = Color(0, 0, 0, 0) bg_color = Color(0, 0, 0, 0)
@@ -10,11 +16,24 @@ border_width_right = 2
border_width_bottom = 2 border_width_bottom = 2
border_color = Color(1, 1, 0, 1) 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] [node name="InventoryUI" type="CanvasLayer" unique_id=-1294967296]
layer = 150 layer = 150
script = ExtResource("1_inventory_ui") script = ExtResource("1_inventory_ui")
[node name="InventoryContainer" type="Control" parent="." unique_id=-294967296] [node name="InventoryContainer" type="Control" parent="." unique_id=-294967296]
visible = false
layout_mode = 3 layout_mode = 3
anchors_preset = 3 anchors_preset = 3
anchor_left = 1.0 anchor_left = 1.0
@@ -44,29 +63,39 @@ size_flags_vertical = 3
mouse_filter = 1 mouse_filter = 1
color = Color(0.1, 0.1, 0.1, 0.85) 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 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 layout_mode = 2
theme_override_constants/separation = 10 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) custom_minimum_size = Vector2(200, 0)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 5 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 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" 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 layout_mode = 2
theme_override_constants/separation = 5 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 layout_mode = 2
theme_override_fonts/font = ExtResource("2_ylkvr")
theme_override_font_sizes/font_size = 10 theme_override_font_sizes/font_size = 10
text = "Level text = "Level
@@ -80,8 +109,9 @@ INT
WIS WIS
LCK" 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 layout_mode = 2
theme_override_fonts/font = ExtResource("2_ylkvr")
theme_override_font_sizes/font_size = 10 theme_override_font_sizes/font_size = 10
text = "1 text = "1
@@ -96,8 +126,9 @@ text = "1
10" 10"
horizontal_alignment = 2 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 layout_mode = 2
theme_override_fonts/font = ExtResource("2_ylkvr")
theme_override_font_sizes/font_size = 10 theme_override_font_sizes/font_size = 10
text = "XP text = "XP
Coin Coin
@@ -112,8 +143,9 @@ Sight
SpellAmp SpellAmp
Crit%" 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 layout_mode = 2
theme_override_fonts/font = ExtResource("2_ylkvr")
theme_override_font_sizes/font_size = 10 theme_override_font_sizes/font_size = 10
text = "0/100 text = "0/100
0 0
@@ -129,60 +161,73 @@ text = "0/100
12.0%" 12.0%"
horizontal_alignment = 2 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) custom_minimum_size = Vector2(400, 0)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 5 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 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" 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) custom_minimum_size = Vector2(0, 8)
layout_mode = 2 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 layout_mode = 2
theme_override_constants/h_separation = 15 theme_override_constants/h_separation = 15
theme_override_constants/v_separation = 15 theme_override_constants/v_separation = 15
columns = 3 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) custom_minimum_size = Vector2(0, 8)
layout_mode = 2 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 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" 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) custom_minimum_size = Vector2(380, 120)
layout_mode = 2 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 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 3 size_flags_vertical = 3
theme_override_constants/separation = -4 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 z_index = 100
custom_minimum_size = Vector2(38, 38) custom_minimum_size = Vector2(38, 38)
layout_mode = 2 layout_mode = 2
mouse_filter = 2 mouse_filter = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_selection") 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) custom_minimum_size = Vector2(0, 80)
layout_mode = 2 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) custom_minimum_size = Vector2(300, 64)
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 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 vertical_alignment = 1
autowrap_mode = 3 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")

View File

@@ -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://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"] [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"] [sub_resource type="Gradient" id="Gradient_jej6c"]
offsets = PackedFloat32Array(0.7710843, 0.77710843) offsets = PackedFloat32Array(0.7710843, 0.77710843)
colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 0) colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 0)
[sub_resource type="GradientTexture2D" id="GradientTexture2D_f1ej7"] [sub_resource type="GradientTexture2D" id="GradientTexture2D_f1ej7"]
gradient = SubResource("Gradient_jej6c") gradient = SubResource("Gradient_jej6c")
use_hdr = true
fill = 1 fill = 1
fill_from = Vector2(0.51304346, 0.51304346) 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"] [sub_resource type="Gradient" id="Gradient_3v2ag"]
colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0) colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0)
@@ -84,10 +96,29 @@ collision_mask = 67
motion_mode = 1 motion_mode = 1
script = ExtResource("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 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") 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] [node name="Shadow" type="Sprite2D" parent="." unique_id=937683521]
z_index = -1 z_index = -1
position = Vector2(0, 7) position = Vector2(0, 7)
@@ -216,3 +247,11 @@ stream = ExtResource("28_pf23h")
max_distance = 1246.0 max_distance = 1246.0
attenuation = 6.964403 attenuation = 6.964403
panning_strength = 1.11 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

View File

@@ -7,6 +7,8 @@ var is_stuck = false
var stick_timer = 0.0 var stick_timer = 0.0
var initiated_by: Node2D = null 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 arrow_area = $ArrowArea # Assuming you have an Area2D node named ArrowArea
@onready var shadow = $Shadow # Assuming you have a Shadow node under the CharacterBody2D @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. # Called when the node enters the scene tree for the first time.
func _ready() -> void: func _ready() -> void:
arrow_area.set_deferred("monitoring", true) 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() $SfxArrowFire.play()
call_deferred("_initialize_arrow") call_deferred("_initialize_arrow")
@@ -49,10 +53,11 @@ func _initialize_arrow() -> void:
# Apply the scaling to the shadow # Apply the scaling to the shadow
shadow.rotation = -(angle - PI / 2) 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() direction = shoot_direction.normalized()
global_position = start_pos 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. # Called every frame. 'delta' is the e lapsed time since the previous frame.
func _process(delta: float) -> void: func _process(delta: float) -> void:
@@ -76,94 +81,99 @@ func _physics_process(_delta: float) -> void:
func play_impact(): func play_impact():
$SfxImpactSound.play() $SfxImpactSound.play()
# Called when the arrow hits a wall or another object # Called when the arrow hits a wall or another object (like sword_projectile)
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.
func _on_arrow_area_body_entered(body: Node2D) -> void: func _on_arrow_area_body_entered(body: Node2D) -> void:
if not is_stuck: if is_stuck:
if body == initiated_by: 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 return
$SfxImpactWall.play()
# Stop the arrow play_impact()
velocity = Vector2.ZERO var enemy_peer_id = body.get_multiplayer_authority()
is_stuck = true if enemy_peer_id != 0:
stick_timer = 0.0 if multiplayer.is_server() and enemy_peer_id == multiplayer.get_unique_id():
arrow_area.set_deferred("monitoring", false) body.rpc_take_damage(damage, attacker_pos, false)
pass # Replace with function body. 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)

View File

@@ -85,6 +85,28 @@ var equipment:Dictionary = {
"dark": 0 "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(): func getCalculatedStats():
var _res = { var _res = {
"str": self.str, "str": self.str,
@@ -158,8 +180,9 @@ var damage: float:
var defense: float: var defense: float:
get: get:
# Reduced DEF scaling: 0.2 per END point (was 0.3) to make it less overpowered for low-level enemies # Further reduced DEF scaling: 0.15 per END point (was 0.2) - makes defense weaker overall
return ((baseStats.end + get_pass("end")) * 0.2) + get_pass("def") # 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: var spell_amp: float:
get: get:
@@ -259,17 +282,22 @@ func modify_mana(amount: float) -> void:
character_changed.emit(self) character_changed.emit(self)
func calculate_damage(base_damage: float, is_magical: bool = false, is_critical: bool = false) -> float: func calculate_damage(base_damage: float, is_magical: bool = false, is_critical: bool = false) -> float:
# Apply defense reduction (DEF reduces damage by a flat amount, not percentage) # Apply defense reduction - more like D&D where defense provides minimal protection
# Defense formula: flat reduction based on END and equipment DEF # Defense now provides percentage reduction (like armor) rather than flat reduction
# Critical hits pierce 80% of DEF (only 20% of DEF applies to crits) # This prevents complete damage negation while still providing meaningful protection
var final_damage = base_damage var final_damage = base_damage
if not is_magical: if not is_magical:
# Physical damage: reduce by defense value (flat reduction) # Physical damage: defense provides percentage reduction (like D&D armor)
var effective_defense = defense # 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: if is_critical:
# Critical hits pierce 80% of DEF (only 20% applies) # Critical hits pierce 80% of DEF (only 20% applies)
effective_defense = defense * 0.2 effective_defense = defense_percentage * 0.2
final_damage = max(0.0, base_damage - effective_defense) # 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: else:
# Magical damage: reduce by magic resistance percentage # Magical damage: reduce by magic resistance percentage
final_damage = base_damage * (1 - (resistances.magic / 100.0)) final_damage = base_damage * (1 - (resistances.magic / 100.0))
@@ -464,10 +492,27 @@ func drop_equipment(iItem:Item):
pass pass
func add_item(iItem:Item): func add_item(iItem:Item):
# Try to stack with existing items if possible
if iItem.can_have_multiple_of:
for existing_item in inventory:
# 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) self.inventory.push_back(iItem)
# Auto-equip if slot is empty (only for equippable items) # 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: 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 = "" var slot_key = ""
match iItem.equipment_type: match iItem.equipment_type:
Item.EquipmentType.MAINHAND: Item.EquipmentType.MAINHAND:

View File

@@ -0,0 +1 @@
uid://d7jp3ffh28hr

View File

@@ -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, "%)") 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 "DODGED" text
_show_damage_number(0.0, from_position, false, false, true) # is_dodged = true _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(): if multiplayer.has_multiplayer_peer() and is_inside_tree():
var enemy_name = name var enemy_name = name
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1 var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else -1
var game_world = get_tree().get_first_node_in_group("game_world") var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_sync_enemy_damage_visual"): if game_world and game_world.has_method("_sync_enemy_damage_visual"):
game_world._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 return # No damage taken, exit early
# If not dodged, apply damage with DEF reduction # 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 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") var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_sync_enemy_damage_visual"): if game_world and game_world.has_method("_sync_enemy_damage_visual"):
game_world._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: else:
# Fallback: try direct RPC (may fail if node path doesn't match) # Fallback: try direct RPC (may fail if node path doesn't match)
_sync_damage_visual.rpc() _sync_damage_visual.rpc(actual_damage, from_position, is_critical)
if current_health <= 0: if current_health <= 0:
# Prevent multiple death triggers # 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() _update_client_visuals()
# This function can be called directly (not just via RPC) when game_world routes the update # 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 # Clients receive damage visual sync
# Only process if we're not the authority (i.e., we're a client) # Only process if we're not the authority (i.e., we're a client)
if is_multiplayer_authority(): if is_multiplayer_authority():
@@ -663,6 +663,10 @@ func _sync_damage_visual():
_flash_damage() _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 # This function can be called directly (not just via RPC) when game_world routes the update
func _sync_death(): func _sync_death():
# Clients receive death sync and play death animation locally # Clients receive death sync and play death animation locally

View File

@@ -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 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(): func _ready():
super._ready() super._ready()
@@ -80,9 +85,15 @@ func _idle_behavior(_delta):
if target_player: if target_player:
var dist = global_position.distance_to(target_player.global_position) var dist = global_position.distance_to(target_player.global_position)
if dist < detection_range: if dist < detection_range:
# Start flying # Start flying (chasing player)
var was_idle = (state == BatState.IDLE)
state = BatState.FLYING state = BatState.FLYING
state_timer = fly_duration 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 return
# Switch to flying after idle duration # Switch to flying after idle duration
@@ -177,3 +188,16 @@ func _play_death_animation():
await fade_tween.finished await fade_tween.finished
queue_free() 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()

View File

@@ -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) print("GameWorld: Could not find enemy for death sync: name=", enemy_name, " index=", enemy_index)
@rpc("authority", "reliable") @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 # Clients receive enemy damage visual sync from server
# Find the enemy by name or index # Find the enemy by name or index
if multiplayer.is_server(): 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"): if enemy and enemy.has_method("_sync_damage_visual"):
# Call the enemy's _sync_damage_visual method directly (not via RPC) # 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: else:
# Enemy not found - might already be freed or never spawned # Enemy not found - might already be freed or never spawned
# This is okay, just log it # This is okay, just log it
print("GameWorld: Could not find enemy for damage visual sync: name=", enemy_name, " index=", enemy_index) 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") @rpc("any_peer", "reliable")
func _request_loot_pickup(loot_id: int, loot_position: Vector2, player_peer_id: int): func _request_loot_pickup(loot_id: int, loot_position: Vector2, player_peer_id: int):
# Server receives loot pickup request from client # Server receives loot pickup request from client
@@ -589,6 +640,9 @@ func _generate_dungeon():
var generator = load("res://scripts/dungeon_generator.gd").new() var generator = load("res://scripts/dungeon_generator.gd").new()
var map_size = Vector2i(72, 72) # 72x72 tiles 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) # Generate dungeon (pass current level for scaling)
dungeon_data = generator.generate_dungeon(map_size, dungeon_seed, current_level) 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 any already-spawned players to the correct spawn points
_move_all_players_to_start_room() _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 # 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 await get_tree().process_frame # Wait a frame for players to be fully in scene tree
_update_camera() _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 on client
_spawn_room_triggers() _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 # 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
await get_tree().process_frame # Wait an extra frame to ensure enemies are fully ready 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") 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(): func _move_all_players_to_start_room():
# Move all players to the start room of the new level # Move all players to the start room of the new level
if dungeon_data.is_empty() or not dungeon_data.has("start_room"): if dungeon_data.is_empty() or not dungeon_data.has("start_room"):

View File

@@ -1 +0,0 @@
uid://dvcubtup4odug

View File

@@ -1 +0,0 @@
uid://d1nl6a63n5wtr

View File

@@ -7,36 +7,41 @@ extends CanvasLayer
var is_open: bool = false var is_open: bool = false
var local_player: Node = null var local_player: Node = null
var is_updating_ui: bool = false # Prevent recursive UI updates
# Selection tracking # Selection tracking
var selected_item: Item = null # Selected inventory item var selected_item: Item = null # Selected inventory item
var selected_slot: String = "" # Selected equipment slot name var selected_slot: String = "" # Selected equipment slot name
var selected_type: String = "" # "item" or "equipment" 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) # Navigation tracking (for keyboard navigation)
var inventory_selection_row: int = 0 # Current inventory row (0-based) var inventory_selection_row: int = 0 # Current inventory row (0-based)
var inventory_selection_col: int = 0 # Current inventory column (0-based) var inventory_selection_col: int = 0 # Current inventory column (0-based)
var equipment_selection_index: int = 0 # Current equipment slot index (0-5: mainhand, offhand, headgear, armour, boots, accessory) var equipment_selection_index: int = 0 # Current equipment slot index (0-5: mainhand, offhand, headgear, armour, boots, accessory)
# UI Nodes (from scene) # UI Nodes (from scene)
@onready var container: Control = $InventoryContainer @onready var container: Control = $InventoryContainer
@onready var stats_panel: Control = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel @onready var stats_panel: Control = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel
@onready var equipment_panel: GridContainer = $InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/EquipmentPanel @onready var equipment_panel: GridContainer = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/EquipmentPanel
@onready var scroll_container: ScrollContainer = $InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll @onready var scroll_container: ScrollContainer = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll
@onready var inventory_grid: VBoxContainer = $InventoryContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll/InventoryVBox @onready var inventory_grid: VBoxContainer = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/InventoryPanel/InventoryScroll/InventoryVBox
@onready var selection_rectangle: Panel = $InventoryContainer/MarginContainer/VBoxContainer/SelectionRectangle @onready var selection_rectangle: Panel = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/SelectionRectangle
@onready var info_panel: Control = $InventoryContainer/MarginContainer/VBoxContainer/InfoPanel @onready var info_panel: Control = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/InfoPanel
@onready var info_label: Label = $InventoryContainer/MarginContainer/VBoxContainer/InfoPanel/InfoLabel @onready var info_label: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/InfoPanel/InfoLabel
@onready var label_base_stats: Label = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelBaseStats @onready var label_base_stats: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelBaseStats
@onready var label_base_stats_value: Label = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelBaseStatsValue @onready var label_base_stats_value: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelBaseStatsValue
@onready var label_derived_stats: Label = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelDerivedStats @onready var label_derived_stats: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelDerivedStats
@onready var label_derived_stats_value: Label = $InventoryContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelDerivedStatsValue @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 # Store button/item mappings for selection highlighting
var inventory_buttons: Dictionary = {} # item -> button var inventory_buttons: Dictionary = {} # item -> button
var equipment_buttons: Dictionary = {} # slot_name -> button var equipment_buttons: Dictionary = {} # slot_name -> button
var inventory_items_list: Array = [] # Flat list of items for navigation var inventory_items_list: Array = [] # Flat list of items for navigation
var inventory_rows_list: Array = [] # List of HBoxContainers (rows) var inventory_rows_list: Array = [] # List of HBoxContainers (rows)
# Equipment slot buttons # Equipment slot buttons
var equipment_slots: Dictionary = { var equipment_slots: Dictionary = {
@@ -47,7 +52,7 @@ var equipment_slots: Dictionary = {
"boots": null, "boots": null,
"accessory": null "accessory": null
} }
var equipment_slots_list: Array = ["mainhand", "offhand", "headgear", "armour", "boots", "accessory"] # Order for navigation var equipment_slots_list: Array = ["mainhand", "offhand", "headgear", "armour", "boots", "accessory"] # Order for navigation
# StyleBoxes for inventory slots (like inspiration system) # StyleBoxes for inventory slots (like inspiration system)
var style_box_hover: StyleBox = null var style_box_hover: StyleBox = null
@@ -66,9 +71,6 @@ func _ready():
# Load styleboxes for inventory slots (like inspiration system) # Load styleboxes for inventory slots (like inspiration system)
_setup_styleboxes() _setup_styleboxes()
# Setup fonts for labels
_setup_fonts()
# Create equipment slot buttons (dynamically) # Create equipment slot buttons (dynamically)
_create_equipment_slots() _create_equipment_slots()
@@ -79,23 +81,40 @@ func _ready():
call_deferred("_find_local_player") call_deferred("_find_local_player")
func _setup_styleboxes(): func _setup_styleboxes():
# Create styleboxes similar to inspiration inventory system # Create styleboxes exactly like inspiration inventory system
var slot_texture = preload("res://assets/gfx/ui/inventory_slot_kenny_white.png") var selected_tex = preload("res://assets/gfx/ui/inventory_slot_kenny_white.png")
if not slot_texture:
# Fallback if texture doesn't exist
slot_texture = null
style_box_empty = StyleBoxEmpty.new() 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 = 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 = 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 = 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: else:
# Fallback to empty styleboxes if texture not found # Fallback to empty styleboxes if texture not found
style_box_hover = StyleBoxEmpty.new() style_box_hover = StyleBoxEmpty.new()
@@ -106,41 +125,17 @@ func _setup_styleboxes():
if ResourceLoader.exists("res://assets/fonts/dmg_numbers.png"): if ResourceLoader.exists("res://assets/fonts/dmg_numbers.png"):
quantity_font = load("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(): func _setup_selection_rectangle():
# Selection rectangle is already in scene, just ensure it's configured correctly # Selection rectangle is already in scene, just ensure it's configured correctly
if selection_rectangle: if selection_rectangle:
selection_rectangle.visible = false 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(): func _find_local_player():
# Find the local player # Find the local player
@@ -207,7 +202,6 @@ func _create_equipment_slots():
label.text = slot_label label.text = slot_label
label.add_theme_font_size_override("font_size", 10) label.add_theme_font_size_override("font_size", 10)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
var standard_font_resource = null
if ResourceLoader.exists("res://assets/fonts/standard_font.png"): if ResourceLoader.exists("res://assets/fonts/standard_font.png"):
var font_resource = load("res://assets/fonts/standard_font.png") var font_resource = load("res://assets/fonts/standard_font.png")
if font_resource: if font_resource:
@@ -217,8 +211,9 @@ func _create_equipment_slots():
# Button (use styleboxes like inspiration system) # Button (use styleboxes like inspiration system)
var button = Button.new() var button = Button.new()
button.name = slot_name + "_btn" button.name = slot_name + "_btn"
button.custom_minimum_size = Vector2(24, 24) # Smaller like inspiration (24x24 instead of 60x60) # Button size increased to accommodate 1.5x scaled texture (24 * 1.5 = 36)
button.size = Vector2(24, 24) button.custom_minimum_size = Vector2(36, 36) # Increased to fit 1.5x scaled texture
button.size = Vector2(36, 36)
if style_box_empty: if style_box_empty:
button.add_theme_stylebox_override("normal", style_box_empty) button.add_theme_stylebox_override("normal", style_box_empty)
if style_box_hover: if style_box_hover:
@@ -227,8 +222,16 @@ func _create_equipment_slots():
button.add_theme_stylebox_override("focus", style_box_focused) button.add_theme_stylebox_override("focus", style_box_focused)
if style_box_pressed: if style_box_pressed:
button.add_theme_stylebox_override("pressed", style_box_pressed) button.add_theme_stylebox_override("pressed", style_box_pressed)
button.flat = false # Use styleboxes instead of flat 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)) 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) slot_container.add_child(button)
equipment_slots[slot_name] = 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: if not local_player or not local_player.character_stats:
return 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 # Only select if there's an item equipped
if not _has_equipment_in_slot(slot_name): if not _has_equipment_in_slot(slot_name):
return return
@@ -276,85 +283,110 @@ func _on_equipment_slot_pressed(slot_name: String):
_update_selection_rectangle() _update_selection_rectangle()
func _update_selection_highlight(): func _update_selection_highlight():
# Reset all button styles # This function is kept for compatibility but now uses _update_selection_rectangle()
for button in equipment_buttons.values(): _update_selection_rectangle()
if button:
var highlight = button.get_node_or_null("Highlight")
if highlight:
highlight.queue_free()
for button in inventory_buttons.values(): # Removed _clear_button_highlight and _apply_button_highlight - using focus system instead
if button:
var highlight = button.get_node_or_null("Highlight")
if highlight:
highlight.queue_free()
func _update_selection_rectangle(): func _update_selection_rectangle():
# Update visual selection rectangle position and visibility # Update visual selection indicator - use button focus like inspiration system
if not selection_rectangle: # Hide the old selection rectangle
return if selection_rectangle:
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:
selection_rectangle.visible = false 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 != "": 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): if _has_equipment_in_slot(selected_slot):
target_button = equipment_buttons.get(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(): 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] var row = inventory_rows_list[inventory_selection_row]
if row and inventory_selection_col >= 0 and inventory_selection_col < row.get_child_count(): if row and inventory_selection_col >= 0 and inventory_selection_col < row.get_child_count():
target_button = row.get_child(inventory_selection_col) as Button 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 # Grab focus on selected button (this will automatically show the focus stylebox)
if should_show and target_button: if target_button:
selection_rectangle.visible = true if target_button.is_inside_tree():
selection_rectangle.position = target_position # Don't grab focus if button already has focus (prevents infinite loops)
selection_rectangle.size = Vector2(38, 38) 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:
# 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: else:
selection_rectangle.visible = false print("InventoryUI: No button to focus - selected_type: ", selected_type)
func _process(delta): func _process(delta):
if is_open and selection_rectangle and selection_rectangle.visible: if is_open:
# Animate selection rectangle border color # Animate selection highlight border color on selected button
selection_animation_time += delta * 2.0 # Speed of animation selection_animation_time += delta * 2.0 # Speed of animation
# Animate between yellow and orange # Animate between yellow and orange
var color1 = Color.YELLOW var color1 = Color.YELLOW
var color2 = Color(1.0, 0.7, 0.0) # Orange-yellow var color2 = Color(1.0, 0.7, 0.0) # Orange-yellow
var t = (sin(selection_animation_time) + 1.0) / 2.0 # 0 to 1 var t = (sin(selection_animation_time) + 1.0) / 2.0 # 0 to 1
var animated_color = color1.lerp(color2, t) var animated_color = color1.lerp(color2, t)
# Update border color # Find the selected button and update its highlight color
var stylebox = selection_rectangle.get_theme_stylebox("panel") as StyleBoxFlat var selected_button: Button = null
if stylebox: if selected_type == "equipment" and selected_slot != "":
stylebox.border_color = animated_color 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
func _update_ui(): func _update_ui():
if not local_player or not local_player.character_stats: if not local_player or not local_player.character_stats:
return return
# Prevent recursive updates
if is_updating_ui:
return
is_updating_ui = true
var char_stats = local_player.character_stats 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 # Debug: Print inventory contents
print("InventoryUI: Updating UI - inventory size: ", char_stats.inventory.size()) print("InventoryUI: Updating UI - inventory size: ", char_stats.inventory.size())
for i in range(char_stats.inventory.size()): for i in range(char_stats.inventory.size()):
@@ -366,6 +398,9 @@ func _update_ui():
inventory_items_list.clear() inventory_items_list.clear()
inventory_rows_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 # Update equipment slots
for slot_name in equipment_slots.keys(): for slot_name in equipment_slots.keys():
var button = equipment_slots[slot_name] var button = equipment_slots[slot_name]
@@ -389,33 +424,53 @@ func _update_ui():
sprite.hframes = equipped_item.spriteFrames.x if equipped_item.spriteFrames.x > 0 else 20 sprite.hframes = equipped_item.spriteFrames.x if equipped_item.spriteFrames.x > 0 else 20
sprite.vframes = equipped_item.spriteFrames.y if equipped_item.spriteFrames.y > 0 else 14 sprite.vframes = equipped_item.spriteFrames.y if equipped_item.spriteFrames.y > 0 else 14
sprite.frame = equipped_item.spriteFrame sprite.frame = equipped_item.spriteFrame
sprite.centered = false # Like inspiration system sprite.centered = false # Like inspiration system
sprite.position = Vector2(4, 4) # Like inspiration system sprite.position = Vector2(4, 4) # Like inspiration system
sprite.scale = Vector2(2.0, 2.0) # 2x size as requested sprite.scale = Vector2(2.0, 2.0) # 2x size as requested
button.add_child(sprite) 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 # Update inventory grid - clear existing HBoxContainers
for child in inventory_grid.get_children(): for child in inventory_grid.get_children():
child.queue_free() 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) # Add inventory items using HBoxContainers (like inspiration system)
var current_hbox: HBoxContainer = null 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 var items_in_current_row = 0
for item in char_stats.inventory: for item in char_stats.inventory:
# Create new HBoxContainer if needed # Create new HBoxContainer if needed
if current_hbox == null or items_in_current_row >= items_per_row: if current_hbox == null or items_in_current_row >= items_per_row:
current_hbox = HBoxContainer.new() current_hbox = HBoxContainer.new()
current_hbox.add_theme_constant_override("separation", 0) # No separation like inspiration 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_grid.add_child(current_hbox)
inventory_rows_list.append(current_hbox) inventory_rows_list.append(current_hbox)
items_in_current_row = 0 items_in_current_row = 0
# Create button with styleboxes (like inspiration system) # Create button with styleboxes (like inspiration system)
var button = Button.new() var button = Button.new()
button.custom_minimum_size = Vector2(24, 24) # Smaller like inspiration (24x24) # Button size increased to accommodate 1.5x scaled texture (24 * 1.5 = 36)
button.size = Vector2(24, 24) button.custom_minimum_size = Vector2(36, 36) # Increased to fit 1.5x scaled texture
button.size = Vector2(36, 36)
if style_box_empty: if style_box_empty:
button.add_theme_stylebox_override("normal", style_box_empty) button.add_theme_stylebox_override("normal", style_box_empty)
if style_box_hover: if style_box_hover:
@@ -424,8 +479,13 @@ func _update_ui():
button.add_theme_stylebox_override("focus", style_box_focused) button.add_theme_stylebox_override("focus", style_box_focused)
if style_box_pressed: if style_box_pressed:
button.add_theme_stylebox_override("pressed", style_box_pressed) button.add_theme_stylebox_override("pressed", style_box_pressed)
button.flat = false # Use styleboxes 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)) 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) current_hbox.add_child(button)
# Add item sprite (like inspiration system - positioned at 4,4 with centered=false) # Add item sprite (like inspiration system - positioned at 4,4 with centered=false)
@@ -437,23 +497,28 @@ func _update_ui():
sprite.hframes = item.spriteFrames.x if item.spriteFrames.x > 0 else 20 sprite.hframes = item.spriteFrames.x if item.spriteFrames.x > 0 else 20
sprite.vframes = item.spriteFrames.y if item.spriteFrames.y > 0 else 14 sprite.vframes = item.spriteFrames.y if item.spriteFrames.y > 0 else 14
sprite.frame = item.spriteFrame sprite.frame = item.spriteFrame
sprite.centered = false # Like inspiration system sprite.centered = false # Like inspiration system
sprite.position = Vector2(4, 4) # Like inspiration system sprite.position = Vector2(4, 4) # Like inspiration system
sprite.scale = Vector2(2.0, 2.0) # 2x size as requested sprite.scale = Vector2(2.0, 2.0) # 2x size as requested
button.add_child(sprite) button.add_child(sprite)
# Add quantity label if item can have multiple (like inspiration system) # Add quantity label if item quantity > 1 (show for all stacked items)
if item.can_have_multiple_of and item.quantity > 1: if item.quantity > 1:
var quantity_label = Label.new() var quantity_label = Label.new()
quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
quantity_label.size = Vector2(24, 24) quantity_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
quantity_label.custom_minimum_size = Vector2(0, 0) quantity_label.size = Vector2(36, 36)
quantity_label.position = Vector2(10, 2) quantity_label.custom_minimum_size = Vector2(36, 36)
quantity_label.position = Vector2(0, 0)
quantity_label.text = str(item.quantity) quantity_label.text = str(item.quantity)
if quantity_font: # Use dmg_numbers.png font (same as damage_number.gd)
quantity_label.add_theme_font_override("font", quantity_font) var dmg_font_resource = load("res://assets/fonts/dmg_numbers.png")
quantity_label.add_theme_font_size_override("font_size", 8) if dmg_font_resource:
quantity_label.scale = Vector2(0.5, 0.5) 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) button.add_child(quantity_label)
inventory_buttons[item] = button inventory_buttons[item] = button
@@ -468,19 +533,74 @@ func _update_ui():
if inventory_selection_col >= row.get_child_count(): if inventory_selection_col >= row.get_child_count():
inventory_selection_col = max(0, row.get_child_count() - 1) 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)
_update_selection_from_navigation() if selected_type != "":
_update_selection_rectangle() _update_selection_from_navigation()
_update_info_panel() _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(): func _update_selection_from_navigation():
# Update selected_item/selected_slot based on navigation position # 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(): 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] var slot_name = equipment_slots_list[equipment_selection_index]
if _has_equipment_in_slot(slot_name): if _has_equipment_in_slot(slot_name):
selected_slot = slot_name selected_slot = slot_name
if local_player and local_player.character_stats: if local_player and local_player.character_stats:
selected_item = local_player.character_stats.equipment[slot_name] selected_item = local_player.character_stats.equipment[slot_name]
else:
selected_item = null
print("InventoryUI: Selected equipment slot: ", slot_name, " item: ", selected_item)
else: else:
# Empty slot - switch to inventory # Empty slot - switch to inventory
selected_type = "item" selected_type = "item"
@@ -492,21 +612,35 @@ func _update_selection_from_navigation():
_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(): 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] 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 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(): if item_index >= 0 and item_index < inventory_items_list.size():
selected_item = inventory_items_list[item_index] selected_item = inventory_items_list[item_index]
selected_slot = "" 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: func _format_item_info(item: Item) -> String:
# Format item description, stats modifiers, and controls # Format item description, stats modifiers, and controls
var text = "" var text = ""
# Item name (always show)
text += item.item_name
# Description # Description
if item.description != "": if item.description != "":
text += item.description text += "\n" + item.description
else:
text += item.item_name
text += "\n\n" text += "\n\n"
@@ -538,7 +672,7 @@ func _format_item_info(item: Item) -> String:
stat_lines.append("MAXMP: +%d" % item.modifiers["maxmp"]) stat_lines.append("MAXMP: +%d" % item.modifiers["maxmp"])
if stat_lines.size() > 0: if stat_lines.size() > 0:
text += "\n".join(stat_lines) text += ", ".join(stat_lines)
text += "\n\n" text += "\n\n"
# Controls # Controls
@@ -550,25 +684,28 @@ func _format_item_info(item: Item) -> String:
elif item.item_type == Item.ItemType.Restoration: elif item.item_type == Item.ItemType.Restoration:
text += "Press F to consume" text += "Press F to consume"
# Only show "Press E to drop" for inventory items, not equipment
if selected_type == "item": if selected_type == "item":
text += "\nPress E to drop" text += ", Press E to drop"
return text return text
func _update_info_panel(): func _update_info_panel():
# Update info panel based on selected item # Update info panel based on selected item
if not info_label: if not info_label:
print("InventoryUI: _update_info_panel() - info_label is null!")
return return
print("InventoryUI: _update_info_panel() - selected_item: ", selected_item, " selected_type: ", selected_type)
if selected_item: if selected_item:
info_label.text = _format_item_info(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: else:
info_label.text = "" info_label.text = ""
print("InventoryUI: Info panel text cleared (no selected_item)")
func _navigate_inventory(direction: String): func _navigate_inventory(direction: String):
# Handle navigation within inventory # Handle navigation within inventory
var items_per_row = 10
match direction: match direction:
"left": "left":
if inventory_selection_col > 0: if inventory_selection_col > 0:
@@ -598,7 +735,7 @@ func _navigate_inventory(direction: String):
inventory_selection_col = row.get_child_count() - 1 inventory_selection_col = row.get_child_count() - 1
else: else:
# Move to equipment slots (only if there are filled slots) # Move to equipment slots (only if there are filled slots)
var next_equip_index = _find_next_filled_equipment_slot(-1, 1) # Start from end, go forward var next_equip_index = _find_next_filled_equipment_slot(-1, 1) # Start from end, go forward
if next_equip_index >= 0: if next_equip_index >= 0:
selected_type = "equipment" selected_type = "equipment"
equipment_selection_index = next_equip_index equipment_selection_index = next_equip_index
@@ -625,7 +762,6 @@ func _navigate_equipment(direction: String):
# Equipment layout: 3 columns, 2 rows # Equipment layout: 3 columns, 2 rows
# Row 1: mainhand(0), offhand(1), headgear(2) # Row 1: mainhand(0), offhand(1), headgear(2)
# Row 2: armour(3), boots(4), accessory(5) # Row 2: armour(3), boots(4), accessory(5)
match direction: match direction:
"left": "left":
var next_index = _find_next_filled_equipment_slot(equipment_selection_index, -1) 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 equipment_selection_index = next_index
"up": "up":
# Find next filled slot in row above (same column) # 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 var current_col = equipment_selection_index % 3
if current_row > 0: if current_row > 0:
var target_index = (current_row - 1) * 3 + current_col var target_index = (current_row - 1) * 3 + current_col
@@ -647,12 +783,12 @@ func _navigate_equipment(direction: String):
else: else:
# Skip to next filled slot in that row # Skip to next filled slot in that row
var next_index = _find_next_filled_equipment_slot(target_index - 1, 1) var next_index = _find_next_filled_equipment_slot(target_index - 1, 1)
if next_index >= 0 and next_index < 3: # Make sure it's in row 0 if next_index >= 0 and next_index < 3: # Make sure it's in row 0
equipment_selection_index = next_index equipment_selection_index = next_index
# Can't go up from equipment (already at top) # Can't go up from equipment (already at top)
"down": "down":
# Find next filled slot in row below (same column), or move to inventory # 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 var current_col = equipment_selection_index % 3
if current_row < 1: if current_row < 1:
var target_index = (current_row + 1) * 3 + current_col 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): if _has_equipment_in_slot(target_slot):
equipment_selection_index = target_index equipment_selection_index = target_index
else: 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
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
_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 (only if inventory has items)
if inventory_rows_list.size() > 0 and inventory_items_list.size() > 0:
selected_type = "item" selected_type = "item"
inventory_selection_row = 0 inventory_selection_row = 0
inventory_selection_col = current_col inventory_selection_col = current_col
# Clamp to valid range # Clamp to valid range
if inventory_rows_list.size() > 0: var inv_row = inventory_rows_list[0]
var inv_row = inventory_rows_list[0] if inventory_selection_col >= inv_row.get_child_count():
if inventory_selection_col >= inv_row.get_child_count(): inventory_selection_col = inv_row.get_child_count() - 1
inventory_selection_col = inv_row.get_child_count() - 1
else:
inventory_selection_col = 0
_update_selection_from_navigation() _update_selection_from_navigation()
_update_selection_rectangle() _update_selection_rectangle()
_update_info_panel() _update_info_panel()
return return
else: # No inventory items, stay on equipment
# Already at bottom row, move to inventory
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
_update_selection_from_navigation() _update_selection_from_navigation()
_update_selection_rectangle() _update_selection_rectangle()
@@ -700,6 +834,10 @@ func _on_inventory_item_pressed(item: Item):
if not local_player or not local_player.character_stats: if not local_player or not local_player.character_stats:
return return
# Prevent updates during UI refresh (prevents infinite loops from focus_entered)
if is_updating_ui:
return
selected_item = item selected_item = item
selected_slot = "" selected_slot = ""
selected_type = "item" selected_type = "item"
@@ -707,17 +845,26 @@ func _on_inventory_item_pressed(item: Item):
# Update navigation position # Update navigation position
var item_index = inventory_items_list.find(item) var item_index = inventory_items_list.find(item)
if item_index >= 0: if item_index >= 0:
var items_per_row = 10 var items_per_row: int = 8
inventory_selection_row = int(item_index / items_per_row) inventory_selection_row = floor(item_index / float(items_per_row))
inventory_selection_col = item_index % items_per_row inventory_selection_col = item_index % items_per_row
_update_selection_highlight() _update_selection_highlight()
_update_selection_rectangle() _update_selection_rectangle()
func _on_character_changed(_char: CharacterStats): 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() _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): func _input(event):
# Toggle with Tab key # Toggle with Tab key
if event is InputEventKey and event.keycode == KEY_TAB and event.pressed and not event.echo: 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] var equipped_item = char_stats.equipment[selected_slot]
if equipped_item: if equipped_item:
char_stats.unequip_item(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 # After unequipping, if all equipment is empty, go to inventory
var has_any_equipment = false var has_any_equipment = false
for slot in equipment_slots_list: for slot in equipment_slots_list:
@@ -832,6 +982,10 @@ func _handle_f_key():
char_stats.equip_item(selected_item) 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() # If this was the last item, set selection state BEFORE _update_ui()
# so that _update_selection_from_navigation() works correctly # so that _update_selection_from_navigation() works correctly
if was_last_item and target_slot_name != "": 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 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"): if item.modifiers.has("hp"):
var hp_heal = item.modifiers["hp"] var hp_heal = item.modifiers["hp"]
if local_player.has_method("heal"): if local_player.has_method("heal"):
@@ -880,6 +1046,11 @@ func _handle_e_key():
var char_stats = local_player.character_stats 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: if not selected_item in char_stats.inventory:
return return
@@ -893,10 +1064,24 @@ func _handle_e_key():
if game_world: if game_world:
entities_node = game_world.get_node_or_null("Entities") entities_node = game_world.get_node_or_null("Entities")
if entities_node: # In multiplayer, clients need to request server to spawn loot
var loot = ItemLootHelper.spawn_item_loot(selected_item, drop_position, entities_node, game_world) if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
if loot: # Client: send drop request to server
if local_player.has_method("get_multiplayer_authority"): 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 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() var player_peer_id = local_player.get_multiplayer_authority()
loot.set_meta("dropped_by_peer_id", player_peer_id) loot.set_meta("dropped_by_peer_id", player_peer_id)
loot.set_meta("drop_time", Time.get_ticks_msec()) loot.set_meta("drop_time", Time.get_ticks_msec())
@@ -913,38 +1098,34 @@ func _open_inventory():
if is_open: if is_open:
return 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 is_open = true
if container: if container:
container.visible = true container.visible = true
_lock_player_controls(true) _lock_player_controls(true)
_update_ui()
# Initialize selection - prefer inventory, but if empty, check equipment # Reset selection state BEFORE updating UI (so _update_ui doesn't try to update selection)
if inventory_rows_list.size() > 0: selected_type = ""
selected_type = "item" selected_item = null
inventory_selection_row = 0 selected_slot = ""
inventory_selection_col = 0
_update_selection_from_navigation() _update_ui()
_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
selected_type = ""
if selection_rectangle:
selection_rectangle.visible = false
if info_label:
info_label.text = ""
if not local_player: if not local_player:
_find_local_player() _find_local_player()

View File

@@ -55,6 +55,7 @@ var weapon_type: WeaponType = WeaponType.NONE
var two_handed:bool = false var two_handed:bool = false
var quantity = 1 var quantity = 1
var can_have_multiple_of:bool = false var can_have_multiple_of:bool = false
var weight: float = 1.0 # Item weight for encumbrance system
func save(): func save():
var json = { var json = {
@@ -72,7 +73,8 @@ func save():
"weapon_type": weapon_type, "weapon_type": weapon_type,
"two_handed": two_handed, "two_handed": two_handed,
"quantity": quantity, "quantity": quantity,
"can_have_multiple_of": can_have_multiple_of "can_have_multiple_of": can_have_multiple_of,
"weight": weight
} }
return json return json
@@ -117,4 +119,6 @@ func load(iDic: Dictionary):
quantity = iDic.get("quantity") quantity = iDic.get("quantity")
if iDic.has("can_have_multiple_of"): if iDic.has("can_have_multiple_of"):
can_have_multiple_of = iDic.get("can_have_multiple_of") can_have_multiple_of = iDic.get("can_have_multiple_of")
if iDic.has("weight"):
weight = iDic.get("weight")
pass pass

1
src/scripts/item.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://cx2xp5myos4s7

View File

@@ -1014,6 +1014,7 @@ static func _load_all_items():
"equipment_type": Item.EquipmentType.NONE, "equipment_type": Item.EquipmentType.NONE,
"weapon_type": Item.WeaponType.NONE, "weapon_type": Item.WeaponType.NONE,
"spriteFrame": 7 * 20 + 12, # 12,7 "spriteFrame": 7 * 20 + 12, # 12,7
"weight": 0.2, # Very light consumable
"modifiers": {"hp": 10}, "modifiers": {"hp": 10},
"buy_cost": 15, "buy_cost": 15,
"sell_worth": 4, "sell_worth": 4,
@@ -1145,6 +1146,7 @@ static func _load_all_items():
"equipment_type": Item.EquipmentType.NONE, "equipment_type": Item.EquipmentType.NONE,
"weapon_type": Item.WeaponType.NONE, "weapon_type": Item.WeaponType.NONE,
"spriteFrame": 8 * 20 + 15, # 15,8 "spriteFrame": 8 * 20 + 15, # 15,8
"weight": 0.3, # Light potion
"modifiers": {"hp": 50}, "modifiers": {"hp": 50},
"buy_cost": 50, "buy_cost": 50,
"sell_worth": 15, "sell_worth": 15,
@@ -1158,6 +1160,7 @@ static func _load_all_items():
"equipment_type": Item.EquipmentType.NONE, "equipment_type": Item.EquipmentType.NONE,
"weapon_type": Item.WeaponType.NONE, "weapon_type": Item.WeaponType.NONE,
"spriteFrame": 8 * 20 + 16, # 16,8 "spriteFrame": 8 * 20 + 16, # 16,8
"weight": 0.3, # Light potion
"modifiers": {"hp": 75, "mp": 75}, "modifiers": {"hp": 75, "mp": 75},
"buy_cost": 100, "buy_cost": 100,
"sell_worth": 30, "sell_worth": 30,
@@ -1171,6 +1174,7 @@ static func _load_all_items():
"equipment_type": Item.EquipmentType.NONE, "equipment_type": Item.EquipmentType.NONE,
"weapon_type": Item.WeaponType.NONE, "weapon_type": Item.WeaponType.NONE,
"spriteFrame": 8 * 20 + 17, # 17,8 "spriteFrame": 8 * 20 + 17, # 17,8
"weight": 0.3, # Light potion
"modifiers": {"dodge_chance": 0.15}, # +15% dodge chance (temporary) "modifiers": {"dodge_chance": 0.15}, # +15% dodge chance (temporary)
"duration": 60.0, # 60 seconds "duration": 60.0, # 60 seconds
"buy_cost": 80, "buy_cost": 80,
@@ -1185,6 +1189,7 @@ static func _load_all_items():
"equipment_type": Item.EquipmentType.NONE, "equipment_type": Item.EquipmentType.NONE,
"weapon_type": Item.WeaponType.NONE, "weapon_type": Item.WeaponType.NONE,
"spriteFrame": 8 * 20 + 18, # 18,8 "spriteFrame": 8 * 20 + 18, # 18,8
"weight": 0.3, # Light potion
"modifiers": {"mp": 50}, "modifiers": {"mp": 50},
"buy_cost": 40, "buy_cost": 40,
"sell_worth": 12, "sell_worth": 12,
@@ -1198,6 +1203,7 @@ static func _load_all_items():
"equipment_type": Item.EquipmentType.NONE, "equipment_type": Item.EquipmentType.NONE,
"weapon_type": Item.WeaponType.NONE, "weapon_type": Item.WeaponType.NONE,
"spriteFrame": 8 * 20 + 19, # 19,8 "spriteFrame": 8 * 20 + 19, # 19,8
"weight": 0.3, # Light potion
"modifiers": {"res_all": 25}, # +25% to all resistances "modifiers": {"res_all": 25}, # +25% to all resistances
"duration": 120.0, # 120 seconds "duration": 120.0, # 120 seconds
"buy_cost": 120, "buy_cost": 120,
@@ -1235,6 +1241,7 @@ static func create_item(item_id: String) -> Item:
item.quantity = item_data.get("quantity", 1) item.quantity = item_data.get("quantity", 1)
item.can_have_multiple_of = item_data.get("can_have_multiple_of", false) item.can_have_multiple_of = item_data.get("can_have_multiple_of", false)
item.duration = item_data.get("duration", 0.0) 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 # 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 # spriteFrames defaults to Vector2i(20,14) in Item class, which is correct

View File

@@ -544,6 +544,18 @@ func _request_pickup(player_peer_id: int):
print("Loot: _request_pickup called on non-server, ignoring") print("Loot: _request_pickup called on non-server, ignoring")
return 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) # Use mutex to prevent concurrent processing (race condition protection)
if processing_pickup: if processing_pickup:
print("Loot: Pickup already being processed, ignoring duplicate request") print("Loot: Pickup already being processed, ignoring duplicate request")

View File

@@ -9,6 +9,7 @@ var appearance_rng: RandomNumberGenerator # Deterministic RNG for appearance/sta
@export var move_speed: float = 100.0 @export var move_speed: float = 100.0
@export var grab_range: float = 20.0 @export var grab_range: float = 20.0
@export var throw_force: float = 150.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 # Network identity
var peer_id: int = 1 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 is_attacking: bool = false
var sword_slash_scene = preload("res://scenes/sword_slash.tscn") # Old rotating version (kept for reference) var sword_slash_scene = preload("res://scenes/sword_slash.tscn") # Old rotating version (kept for reference)
var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn") # New projectile version 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") var blood_scene = preload("res://scenes/blood_clot.tscn")
# Simulated Z-axis for height (when thrown) # Simulated Z-axis for height (when thrown)
@@ -92,6 +94,7 @@ var is_airborne: bool = false
@onready var sprite_addons = $Sprite2DAddons @onready var sprite_addons = $Sprite2DAddons
@onready var sprite_headgear = $Sprite2DHeadgear @onready var sprite_headgear = $Sprite2DHeadgear
@onready var sprite_weapon = $Sprite2DWeapon @onready var sprite_weapon = $Sprite2DWeapon
@onready var cone_light = $ConeLight
# Player stats (legacy - now using character_stats) # Player stats (legacy - now using character_stats)
var max_health: float: var max_health: float:
@@ -266,6 +269,12 @@ func _ready():
if interaction_indicator: if interaction_indicator:
interaction_indicator.visible = false 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 # 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 # This prevents "Node not found" errors when RPCs try to resolve node paths
if multiplayer.is_server(): if multiplayer.is_server():
@@ -600,7 +609,7 @@ func _on_character_changed(_char: CharacterStats):
# Update appearance when character stats change (e.g., equipment) # Update appearance when character stats change (e.g., equipment)
_apply_appearance_to_sprites() _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(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
# Sync equipment to all clients # Sync equipment to all clients
var equipment_data = {} var equipment_data = {}
@@ -612,6 +621,32 @@ func _on_character_changed(_char: CharacterStats):
equipment_data[slot_name] = null equipment_data[slot_name] = null
_sync_equipment.rpc(equipment_data) _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: func _get_player_color() -> Color:
# Legacy function - now returns white (no color tint) # Legacy function - now returns white (no color tint)
return Color.WHITE return Color.WHITE
@@ -700,6 +735,107 @@ func _set_animation(anim_name: String):
current_frame = 0 current_frame = 0
time_since_last_frame = 0.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 # Helper function to snap direction to 8-way directions
func _snap_to_8_directions(direction: Vector2) -> Vector2: func _snap_to_8_directions(direction: Vector2) -> Vector2:
if direction.length() < 0.1: 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) # Skip input if controls are disabled (e.g., when inventory is open)
# But still allow knockback to continue (handled above) # But still allow knockback to continue (handled above)
var skip_input = controls_disabled
if controls_disabled: if controls_disabled:
if not is_knocked_back: if not is_knocked_back:
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0) # Slow down movement # Immediately stop movement when controls are disabled (e.g., inventory opened)
return 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 # Check if being held by someone
var being_held_by_someone = false 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 # During knockback, no input control - just let velocity carry the player
# Apply friction to slow down knockback # Apply friction to slow down knockback
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0) velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
elif not is_airborne: elif not is_airborne and not skip_input:
# Normal input handling # Normal input handling (only if controls are not disabled)
struggle_time = 0.0 # Reset struggle timer struggle_time = 0.0 # Reset struggle timer
struggle_direction = Vector2.ZERO struggle_direction = Vector2.ZERO
_handle_input() _handle_input()
@@ -1006,11 +1151,17 @@ func _handle_input():
last_movement_direction = input_vector.normalized() last_movement_direction = input_vector.normalized()
# Update facing direction (except when pushing - locked direction) # Update facing direction (except when pushing - locked direction)
var new_direction = current_direction
if not is_pushing: 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: else:
# Keep locked direction when pushing # 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 # Set animation based on state
if is_lifting: if is_lifting:
@@ -1027,7 +1178,9 @@ func _handle_input():
elif is_pushing: elif is_pushing:
_set_animation("IDLE_PUSH") _set_animation("IDLE_PUSH")
# Keep locked direction when pushing # Keep locked direction when pushing
current_direction = push_direction_locked as Direction if push_direction_locked != current_direction:
current_direction = push_direction_locked as Direction
_update_cone_light_rotation()
else: else:
if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD": if current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD":
_set_animation("IDLE") _set_animation("IDLE")
@@ -1054,7 +1207,13 @@ func _handle_input():
was_dragging_last_frame = is_dragging_now was_dragging_last_frame = is_dragging_now
# Reduce speed by half when pushing/pulling # 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 velocity = input_vector * current_speed
func _handle_movement(_delta): func _handle_movement(_delta):
@@ -1611,8 +1770,20 @@ func _perform_attack():
can_attack = false can_attack = false
is_attacking = true is_attacking = true
# Play attack animation # Check what weapon is equipped
_set_animation("SWORD") 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 # Calculate attack direction based on player's facing direction
var attack_direction = Vector2.ZERO var attack_direction = Vector2.ZERO
@@ -1634,7 +1805,7 @@ func _perform_attack():
Direction.UP_RIGHT: Direction.UP_RIGHT:
attack_direction = Vector2(1, -1).normalized() attack_direction = Vector2(1, -1).normalized()
# Delay before spawning sword slash # Delay before spawning projectile
await get_tree().create_timer(0.15).timeout await get_tree().create_timer(0.15).timeout
# Calculate damage from character_stats with randomization # Calculate damage from character_stats with randomization
@@ -1659,18 +1830,50 @@ func _perform_attack():
# Round to 1 decimal place # Round to 1 decimal place
final_damage = round(final_damage * 10.0) / 10.0 final_damage = round(final_damage * 10.0) / 10.0
# Spawn sword projectile # Handle bow attacks - require arrows in off-hand
if sword_projectile_scene: if is_bow:
var projectile = sword_projectile_scene.instantiate() # Check for arrows in off-hand
get_parent().add_child(projectile) var arrows = null
projectile.setup(attack_direction, self, final_damage) if character_stats and character_stats.equipment.has("offhand"):
# Store crit status for visual feedback var offhand_item = character_stats.equipment["offhand"]
if is_crit: if offhand_item and offhand_item.weapon_type == Item.WeaponType.AMMUNITION:
projectile.set_meta("is_crit", true) arrows = offhand_item
# Spawn projectile a bit in front of the player
var spawn_offset = attack_direction * 10.0 # 10 pixels in front # Only spawn arrow if we have arrows
projectile.global_position = global_position + spawn_offset if arrows and arrows.quantity > 0:
print(name, " attacked with sword projectile! Damage: ", final_damage, " (base: ", base_damage, ", crit: ", is_crit, ")") 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)
projectile.setup(attack_direction, self, final_damage)
# Store crit status for visual feedback
if is_crit:
projectile.set_meta("is_crit", true)
# Spawn projectile a bit in front of the player
var spawn_offset = attack_direction * 10.0 # 10 pixels in front
projectile.global_position = global_position + spawn_offset
print(name, " attacked with sword projectile! Damage: ", final_damage, " (base: ", base_damage, ", crit: ", is_crit, ")")
# Sync attack over network # Sync attack over network
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
@@ -2572,15 +2775,19 @@ func _sync_stats_update(kills_count: int, coins_count: int):
@rpc("any_peer", "reliable") @rpc("any_peer", "reliable")
func _sync_equipment(equipment_data: Dictionary): func _sync_equipment(equipment_data: Dictionary):
# Client receives equipment update from server # Client receives equipment update from server or other clients
# Update equipment to match other players # Update equipment to match server/other players
# Only process if we're not the authority (remote player) # Server also needs to accept equipment sync from clients (joiners) to update server's copy of joiner's player
if is_multiplayer_authority():
return # Authority ignores this (it's the sender)
if not character_stats: if not character_stats:
return 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 # Update equipment from data
for slot_name in equipment_data.keys(): for slot_name in equipment_data.keys():
var item_data = equipment_data[slot_name] var item_data = equipment_data[slot_name]
@@ -2591,7 +2798,29 @@ func _sync_equipment(equipment_data: Dictionary):
# Update appearance # Update appearance
_apply_appearance_to_sprites() _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): func heal(amount: float):
if is_dead: if is_dead:

View 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)

View File

@@ -0,0 +1 @@
uid://mgdw7x3bar6f

View 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);
}

View File

@@ -0,0 +1 @@
uid://ce7sy7vkt3qr2