From 89a41397d16cd05f7026330c480b201510f54e27 Mon Sep 17 00:00:00 2001 From: Elrinth Date: Tue, 13 Jan 2026 00:16:08 +0100 Subject: [PATCH] started working on fog darkness --- .../audio/sfx/enemies/bat/bat_chirp.mp3 | Bin 0 -> 24269 bytes .../sfx/enemies/bat/bat_chirp.mp3.import | 19 + .../audio/sfx/enemies/bat/bat_flap1.mp3 | Bin 0 -> 26837 bytes .../sfx/enemies/bat/bat_flap1.mp3.import | 19 + .../audio/sfx/enemies/bat/bat_flap2.mp3 | Bin 0 -> 24878 bytes .../sfx/enemies/bat/bat_flap2.mp3.import | 19 + src/assets/gfx/RPG DUNGEON VOL 3.tres | 145 +++++ src/project.godot | 7 + src/scenes/enemy_bat.tscn | 27 + src/scenes/game_world.tscn | 6 +- src/scenes/inventory_ui.tscn | 93 ++- src/scenes/player.tscn | 43 +- src/scripts/attack_arrow.gd | 194 +++--- .../character_stats.gd | 63 +- src/scripts/character_stats.gd.uid | 1 + src/scripts/enemy_base.gd | 14 +- src/scripts/enemy_bat.gd | 26 +- src/scripts/game_world.gd | 101 ++- .../character_stats.gd.uid | 1 - src/scripts/inspiration_scripts/item.gd.uid | 1 - src/scripts/inventory_ui.gd | 613 ++++++++++++------ src/scripts/{inspiration_scripts => }/item.gd | 6 +- src/scripts/item.gd.uid | 1 + src/scripts/item_database.gd | 7 + src/scripts/loot.gd | 12 + src/scripts/player.gd | 291 ++++++++- src/scripts/room_lighting_system.gd | 259 ++++++++ src/scripts/room_lighting_system.gd.uid | 1 + src/shaders/light_cone.gdshader | 29 + src/shaders/light_cone.gdshader.uid | 1 + 30 files changed, 1613 insertions(+), 386 deletions(-) create mode 100644 src/assets/audio/sfx/enemies/bat/bat_chirp.mp3 create mode 100644 src/assets/audio/sfx/enemies/bat/bat_chirp.mp3.import create mode 100644 src/assets/audio/sfx/enemies/bat/bat_flap1.mp3 create mode 100644 src/assets/audio/sfx/enemies/bat/bat_flap1.mp3.import create mode 100644 src/assets/audio/sfx/enemies/bat/bat_flap2.mp3 create mode 100644 src/assets/audio/sfx/enemies/bat/bat_flap2.mp3.import rename src/scripts/{inspiration_scripts => }/character_stats.gd (88%) create mode 100644 src/scripts/character_stats.gd.uid delete mode 100644 src/scripts/inspiration_scripts/character_stats.gd.uid delete mode 100644 src/scripts/inspiration_scripts/item.gd.uid rename src/scripts/{inspiration_scripts => }/item.gd (93%) create mode 100644 src/scripts/item.gd.uid create mode 100644 src/scripts/room_lighting_system.gd create mode 100644 src/scripts/room_lighting_system.gd.uid create mode 100644 src/shaders/light_cone.gdshader create mode 100644 src/shaders/light_cone.gdshader.uid 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 0000000000000000000000000000000000000000..65ea7b6c691ed50b1bd2abe61714fff10cb4817b GIT binary patch literal 24269 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f299f6adaa92016b24c5a55c1e51ff6246f6afae GIT binary patch literal 24878 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 literal 0 HcmV?d00001 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