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