fixat mer med traps och arrows och grejjer
This commit is contained in:
BIN
src/assets/audio/sfx/traps/activate.mp3
Normal file
BIN
src/assets/audio/sfx/traps/activate.mp3
Normal file
Binary file not shown.
19
src/assets/audio/sfx/traps/activate.mp3.import
Normal file
19
src/assets/audio/sfx/traps/activate.mp3.import
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="mp3"
|
||||||
|
type="AudioStreamMP3"
|
||||||
|
uid="uid://raqmpvp1vj04"
|
||||||
|
path="res://.godot/imported/activate.mp3-9f00271ed50842203601cea6b7140d55.mp3str"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/traps/activate.mp3"
|
||||||
|
dest_files=["res://.godot/imported/activate.mp3-9f00271ed50842203601cea6b7140d55.mp3str"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -67,25 +67,35 @@ grab={
|
|||||||
"deadzone": 0.5,
|
"deadzone": 0.5,
|
||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
|
||||||
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":74,"key_label":0,"unicode":106,"location":0,"echo":false,"script":null)
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":74,"key_label":0,"unicode":106,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
throw={
|
throw={
|
||||||
"deadzone": 0.5,
|
"deadzone": 0.5,
|
||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"key_label":0,"unicode":113,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"key_label":0,"unicode":113,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
attack={
|
attack={
|
||||||
"deadzone": 0.5,
|
"deadzone": 0.5,
|
||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null)
|
||||||
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":75,"key_label":0,"unicode":107,"location":0,"echo":false,"script":null)
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":75,"key_label":0,"unicode":107,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
inventory={
|
inventory={
|
||||||
"deadzone": 0.2,
|
"deadzone": 0.2,
|
||||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":3,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[layer_names]
|
||||||
|
|
||||||
|
2d_physics/layer_1="Player"
|
||||||
|
2d_physics/layer_7="Walls"
|
||||||
|
2d_physics/layer_13="traps"
|
||||||
|
|
||||||
[physics]
|
[physics]
|
||||||
|
|
||||||
3d/physics_engine="Jolt Physics"
|
3d/physics_engine="Jolt Physics"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
[ext_resource type="AudioStream" uid="uid://hmci4kgvbqib" path="res://assets/audio/sfx/weapons/bow/arrow_fire_swosh.wav" id="3_o8cb2"]
|
[ext_resource type="AudioStream" uid="uid://hmci4kgvbqib" path="res://assets/audio/sfx/weapons/bow/arrow_fire_swosh.wav" id="3_o8cb2"]
|
||||||
[ext_resource type="AudioStream" uid="uid://b140nlsak4ub7" path="res://assets/audio/sfx/weapons/bow/arrow-hit-brick-wall-01.mp3" id="4_8l43l"]
|
[ext_resource type="AudioStream" uid="uid://b140nlsak4ub7" path="res://assets/audio/sfx/weapons/bow/arrow-hit-brick-wall-01.mp3" id="4_8l43l"]
|
||||||
[ext_resource type="AudioStream" uid="uid://dc7nt8gnjt5u5" path="res://assets/audio/sfx/weapons/melee_attack_12.wav.mp3" id="4_ol4b0"]
|
[ext_resource type="AudioStream" uid="uid://dc7nt8gnjt5u5" path="res://assets/audio/sfx/weapons/melee_attack_12.wav.mp3" id="4_ol4b0"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://d4kjyb1olr74s" path="res://assets/audio/sfx/weapons/bow/bow_draw-02.mp3" id="6_e0wjs"]
|
||||||
|
|
||||||
[sub_resource type="Gradient" id="Gradient_yp18a"]
|
[sub_resource type="Gradient" id="Gradient_yp18a"]
|
||||||
colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0)
|
colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0)
|
||||||
@@ -56,21 +57,44 @@ debug_color = Color(0.7, 0, 0.195726, 0.42)
|
|||||||
[node name="SfxArrowFire" type="AudioStreamPlayer2D" parent="." unique_id=1413495156]
|
[node name="SfxArrowFire" type="AudioStreamPlayer2D" parent="." unique_id=1413495156]
|
||||||
stream = ExtResource("3_o8cb2")
|
stream = ExtResource("3_o8cb2")
|
||||||
pitch_scale = 1.61
|
pitch_scale = 1.61
|
||||||
|
attenuation = 7.464256
|
||||||
max_polyphony = 4
|
max_polyphony = 4
|
||||||
|
panning_strength = 1.01
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
[node name="SfxImpactWall" type="AudioStreamPlayer2D" parent="." unique_id=1132726967]
|
[node name="SfxImpactWall" type="AudioStreamPlayer2D" parent="." unique_id=1132726967]
|
||||||
stream = ExtResource("4_8l43l")
|
stream = ExtResource("4_8l43l")
|
||||||
volume_db = -4.0
|
volume_db = -4.0
|
||||||
pitch_scale = 1.29
|
pitch_scale = 1.29
|
||||||
attenuation = 3.4822
|
attenuation = 3.9999971
|
||||||
max_polyphony = 4
|
max_polyphony = 4
|
||||||
panning_strength = 1.3
|
panning_strength = 1.3
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
[node name="SfxImpactSound" type="AudioStreamPlayer2D" parent="." unique_id=463883211]
|
[node name="SfxImpactSound" type="AudioStreamPlayer2D" parent="." unique_id=463883211]
|
||||||
stream = ExtResource("4_ol4b0")
|
stream = ExtResource("4_ol4b0")
|
||||||
volume_db = -4.685
|
volume_db = -4.685
|
||||||
pitch_scale = 1.47
|
pitch_scale = 1.47
|
||||||
|
attenuation = 8.574182
|
||||||
max_polyphony = 4
|
max_polyphony = 4
|
||||||
|
panning_strength = 1.03
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[node name="SfxPickup" type="AudioStreamPlayer2D" parent="." unique_id=916101887]
|
||||||
|
stream = ExtResource("6_e0wjs")
|
||||||
|
volume_db = -2.204
|
||||||
|
pitch_scale = 1.79
|
||||||
|
attenuation = 6.498014
|
||||||
|
panning_strength = 1.06
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[node name="SfxLandsOnGround" type="AudioStreamPlayer2D" parent="." unique_id=1827010383]
|
||||||
|
stream = ExtResource("6_e0wjs")
|
||||||
|
volume_db = -2.645
|
||||||
|
pitch_scale = 1.65
|
||||||
|
attenuation = 5.6568513
|
||||||
|
panning_strength = 1.08
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
[connection signal="area_entered" from="ArrowArea" to="." method="_on_arrow_area_area_entered"]
|
[connection signal="area_entered" from="ArrowArea" to="." method="_on_arrow_area_area_entered"]
|
||||||
[connection signal="body_entered" from="ArrowArea" to="." method="_on_arrow_area_body_entered"]
|
[connection signal="body_entered" from="ArrowArea" to="." method="_on_arrow_area_body_entered"]
|
||||||
|
|||||||
@@ -31,6 +31,10 @@
|
|||||||
[ext_resource type="AudioStream" uid="uid://w6yon88kjfml" path="res://assets/audio/sfx/nickes/lift_sfx.ogg" id="28_pf23h"]
|
[ext_resource type="AudioStream" uid="uid://w6yon88kjfml" path="res://assets/audio/sfx/nickes/lift_sfx.ogg" id="28_pf23h"]
|
||||||
[ext_resource type="AudioStream" uid="uid://d4kjyb1olr74s" path="res://assets/audio/sfx/weapons/bow/bow_draw-02.mp3" id="30_gl8cc"]
|
[ext_resource type="AudioStream" uid="uid://d4kjyb1olr74s" path="res://assets/audio/sfx/weapons/bow/bow_draw-02.mp3" id="30_gl8cc"]
|
||||||
[ext_resource type="AudioStream" uid="uid://mbtpqlb5n3gd" path="res://assets/audio/sfx/weapons/bow/bow_no_arrow.mp3" id="31_487ah"]
|
[ext_resource type="AudioStream" uid="uid://mbtpqlb5n3gd" path="res://assets/audio/sfx/weapons/bow/bow_no_arrow.mp3" id="31_487ah"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://fm6hrpckfknc" path="res://assets/audio/sfx/player/unlock/padlock_wiggle_03.wav" id="32_bj30b"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://be3uspidyqm3x" path="res://assets/audio/sfx/player/unlock/padlock_wiggle_04.wav" id="33_jc3p3"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://dvttykynr671m" path="res://assets/audio/sfx/player/unlock/padlock_wiggle_05.wav" id="34_hax0n"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://sejnuklu653m" path="res://assets/audio/sfx/player/unlock/padlock_wiggle_06.wav" id="35_t4otl"]
|
||||||
|
|
||||||
[sub_resource type="Gradient" id="Gradient_wqfne"]
|
[sub_resource type="Gradient" id="Gradient_wqfne"]
|
||||||
colors = PackedColorArray(0, 0, 0, 1, 1, 0.13732082, 0.092538536, 1)
|
colors = PackedColorArray(0, 0, 0, 1, 1, 0.13732082, 0.092538536, 1)
|
||||||
@@ -280,6 +284,14 @@ random_pitch = 1.0630184
|
|||||||
streams_count = 1
|
streams_count = 1
|
||||||
stream_0/stream = ExtResource("31_487ah")
|
stream_0/stream = ExtResource("31_487ah")
|
||||||
|
|
||||||
|
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_j2b1d"]
|
||||||
|
random_pitch = 1.1036249
|
||||||
|
streams_count = 4
|
||||||
|
stream_0/stream = ExtResource("32_bj30b")
|
||||||
|
stream_1/stream = ExtResource("33_jc3p3")
|
||||||
|
stream_2/stream = ExtResource("34_hax0n")
|
||||||
|
stream_3/stream = ExtResource("35_t4otl")
|
||||||
|
|
||||||
[node name="Player" type="CharacterBody2D" unique_id=937429705]
|
[node name="Player" type="CharacterBody2D" unique_id=937429705]
|
||||||
collision_mask = 67
|
collision_mask = 67
|
||||||
motion_mode = 1
|
motion_mode = 1
|
||||||
|
|||||||
133
src/scenes/trap.tscn
Normal file
133
src/scenes/trap.tscn
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
[gd_scene format=3 uid="uid://bcgdkojealqoo"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://bkp8t4mvdhtqq" path="res://scripts/trap.gd" id="1_62q8x"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://b6eeio3gm7d4u" path="res://assets/gfx/traps/Floor_Lance.png" id="2_aucmg"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://raqmpvp1vj04" path="res://assets/audio/sfx/traps/activate.mp3" id="3_tk2q1"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://dxy2phfh0ojot" path="res://assets/audio/sfx/player/dodge/Dodge.mp3" id="4_1sb0t"]
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_tk2q1"]
|
||||||
|
length = 0.001
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("Sprite2D:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [0]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_1sb0t"]
|
||||||
|
resource_name = "activate"
|
||||||
|
length = 1.4333333
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("Sprite2D:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.033333335, 0.06666667, 0.1),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [0, 1, 2, 3]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_62q8x"]
|
||||||
|
resource_name = "idle"
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("Sprite2D:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [0]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_aucmg"]
|
||||||
|
resource_name = "reset"
|
||||||
|
length = 0.2
|
||||||
|
tracks/0/type = "value"
|
||||||
|
tracks/0/imported = false
|
||||||
|
tracks/0/enabled = true
|
||||||
|
tracks/0/path = NodePath("Sprite2D:frame")
|
||||||
|
tracks/0/interp = 1
|
||||||
|
tracks/0/loop_wrap = true
|
||||||
|
tracks/0/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.033333335, 0.06666667, 0.1),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [3, 2, 1, 0]
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="AnimationLibrary" id="AnimationLibrary_7s8xa"]
|
||||||
|
_data = {
|
||||||
|
&"RESET": SubResource("Animation_tk2q1"),
|
||||||
|
&"activate": SubResource("Animation_1sb0t"),
|
||||||
|
&"idle": SubResource("Animation_62q8x"),
|
||||||
|
&"reset": SubResource("Animation_aucmg")
|
||||||
|
}
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_62q8x"]
|
||||||
|
size = Vector2(16, 16)
|
||||||
|
|
||||||
|
[sub_resource type="CircleShape2D" id="CircleShape2D_aucmg"]
|
||||||
|
radius = 17.117243
|
||||||
|
|
||||||
|
[sub_resource type="CircleShape2D" id="CircleShape2D_62q8x"]
|
||||||
|
radius = 99.0202
|
||||||
|
|
||||||
|
[node name="Trap" type="Node2D" unique_id=131165873]
|
||||||
|
script = ExtResource("1_62q8x")
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1625881554]
|
||||||
|
texture = ExtResource("2_aucmg")
|
||||||
|
hframes = 4
|
||||||
|
|
||||||
|
[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=1394959342]
|
||||||
|
libraries/ = SubResource("AnimationLibrary_7s8xa")
|
||||||
|
|
||||||
|
[node name="ActivationArea" type="Area2D" parent="." unique_id=694690572]
|
||||||
|
collision_layer = 4096
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="ActivationArea" unique_id=1574459451]
|
||||||
|
shape = SubResource("RectangleShape2D_62q8x")
|
||||||
|
|
||||||
|
[node name="DisarmArea" type="Area2D" parent="." unique_id=687041294]
|
||||||
|
collision_layer = 4096
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="DisarmArea" unique_id=1192467236]
|
||||||
|
shape = SubResource("CircleShape2D_aucmg")
|
||||||
|
debug_color = Color(0.70196074, 0.614594, 0.08530171, 0.41960785)
|
||||||
|
|
||||||
|
[node name="DetectionArea" type="Area2D" parent="." unique_id=1872122085]
|
||||||
|
collision_layer = 0
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="DetectionArea" unique_id=1024222789]
|
||||||
|
shape = SubResource("CircleShape2D_62q8x")
|
||||||
|
debug_color = Color(0.53960574, 0.04878081, 0.70196074, 0.41960785)
|
||||||
|
|
||||||
|
[node name="SfxActivate" type="AudioStreamPlayer2D" parent="." unique_id=312292112]
|
||||||
|
stream = ExtResource("3_tk2q1")
|
||||||
|
volume_db = -1.559
|
||||||
|
attenuation = 4.438279
|
||||||
|
panning_strength = 1.03
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[node name="SfxAvoid" type="AudioStreamPlayer2D" parent="." unique_id=322639992]
|
||||||
|
stream = ExtResource("4_1sb0t")
|
||||||
|
pitch_scale = 1.35
|
||||||
|
attenuation = 1.9318731
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[node name="SfxDisarming" type="AudioStreamPlayer2D" parent="." unique_id=920322213]
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[connection signal="body_shape_entered" from="ActivationArea" to="." method="_on_activation_area_body_shape_entered"]
|
||||||
@@ -2,22 +2,37 @@ extends CharacterBody2D
|
|||||||
|
|
||||||
var speed = 300
|
var speed = 300
|
||||||
var direction = Vector2.ZERO
|
var direction = Vector2.ZERO
|
||||||
var stick_duration = 3.0 # How long the arrow stays stuck to walls
|
var stick_duration = 3.0 # How long the arrow stays stuck to enemies/players
|
||||||
|
var wall_stick_duration = 30.0 # Much longer duration for wall-stuck arrows (30 seconds)
|
||||||
var is_stuck = false
|
var is_stuck = false
|
||||||
|
var is_collected = false
|
||||||
var stick_timer = 0.0
|
var stick_timer = 0.0
|
||||||
|
var stuck_to_wall = false # Track if stuck to wall (vs enemy/player)
|
||||||
|
var can_be_collected = false # True after delay on wall
|
||||||
|
var shooter_can_collect = false # Shooter can collect after 0.2 seconds
|
||||||
|
var shooter_collection_delay = 0.2 # Fast pickup for shooter
|
||||||
|
var others_collection_delay = 5.0 # Other players wait 5 seconds
|
||||||
|
|
||||||
|
# Flight duration based on charge
|
||||||
|
var flight_timer = 0.0
|
||||||
|
var max_flight_duration = 6.0 # How long arrow flies before landing (set by charge)
|
||||||
|
var can_deal_damage = true # False after arrow "lands" in flight
|
||||||
|
|
||||||
var initiated_by: Node2D = null
|
var initiated_by: Node2D = null
|
||||||
var player_owner: Node = null # Like sword_projectile
|
var player_owner: Node = null # Like sword_projectile
|
||||||
var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup)
|
var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup)
|
||||||
|
|
||||||
|
# Collection area for wall-stuck arrows
|
||||||
|
var collection_area: Area2D = null
|
||||||
|
|
||||||
@onready var arrow_area = $ArrowArea # Assuming you have an Area2D node named ArrowArea
|
@onready var arrow_area = $ArrowArea # Assuming you have an Area2D node named ArrowArea
|
||||||
@onready var shadow = $Shadow # Assuming you have a Shadow node under the CharacterBody2D
|
@onready var shadow = $Shadow # Assuming you have a Shadow node under the CharacterBody2D
|
||||||
|
|
||||||
# Called when the node enters the scene tree for the first time.
|
# Called when the node enters the scene tree for the first time.
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
arrow_area.set_deferred("monitoring", true)
|
arrow_area.set_deferred("monitoring", true)
|
||||||
# Connect area signals
|
# Connect area signals (only if not already connected)
|
||||||
if arrow_area:
|
if arrow_area and not arrow_area.body_entered.is_connected(_on_arrow_area_body_entered):
|
||||||
arrow_area.body_entered.connect(_on_arrow_area_body_entered)
|
arrow_area.body_entered.connect(_on_arrow_area_body_entered)
|
||||||
$SfxArrowFire.play()
|
$SfxArrowFire.play()
|
||||||
call_deferred("_initialize_arrow")
|
call_deferred("_initialize_arrow")
|
||||||
@@ -53,22 +68,62 @@ func _initialize_arrow() -> void:
|
|||||||
# Apply the scaling to the shadow
|
# Apply the scaling to the shadow
|
||||||
shadow.rotation = -(angle - PI / 2)
|
shadow.rotation = -(angle - PI / 2)
|
||||||
|
|
||||||
func shoot(shoot_direction: Vector2, start_pos: Vector2, owner_player: Node = null) -> void:
|
func shoot(shoot_direction: Vector2, start_pos: Vector2, owner_player: Node = null, charge_percentage: float = 1.0) -> void:
|
||||||
direction = shoot_direction.normalized()
|
direction = shoot_direction.normalized()
|
||||||
global_position = start_pos
|
global_position = start_pos
|
||||||
player_owner = owner_player
|
player_owner = owner_player
|
||||||
initiated_by = owner_player
|
initiated_by = owner_player
|
||||||
|
|
||||||
|
# Apply charge percentage to arrow speed and flight duration
|
||||||
|
# Speed: min 120, max 320, scales with charge % (0.5 to 1.0)
|
||||||
|
var min_speed = 120.0
|
||||||
|
var max_speed = 320.0
|
||||||
|
speed = lerp(min_speed, max_speed, (charge_percentage - 0.5) * 2.0) # Map 0.5-1.0 to 0.0-1.0 range
|
||||||
|
|
||||||
|
# Flight duration: 50% charge = 0.5s, 100% charge = 2.5s
|
||||||
|
max_flight_duration = lerp(0.5, 2.5, (charge_percentage - 0.5) * 2.0)
|
||||||
|
|
||||||
|
print("Arrow shot at ", charge_percentage * 100, "% charge (speed: ", speed, ", flight duration: ", max_flight_duration, "s)")
|
||||||
|
|
||||||
# Called every frame. 'delta' is the e lapsed time since the previous frame.
|
# Called every frame. 'delta' is the e lapsed time since the previous frame.
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
|
if not is_stuck:
|
||||||
|
# Track flight time and "land" arrow after max_flight_duration
|
||||||
|
flight_timer += delta
|
||||||
|
if flight_timer >= max_flight_duration:
|
||||||
|
# Arrow has flown for max duration - "land" it (stop and stick to ground)
|
||||||
|
can_deal_damage = false
|
||||||
|
$SfxLandsOnGround.play()
|
||||||
|
_stick_to_wall() # Land on ground
|
||||||
|
print("Arrow landed after flying for ", flight_timer, " seconds")
|
||||||
|
return # Exit early to prevent further movement this frame
|
||||||
|
|
||||||
|
# Continue flying
|
||||||
|
velocity = direction * speed
|
||||||
|
|
||||||
if is_stuck:
|
if is_stuck:
|
||||||
# Handle fade out here if it's stuck
|
# Handle fade out here if it's stuck
|
||||||
stick_timer += delta
|
stick_timer += delta
|
||||||
if stick_timer >= stick_duration:
|
|
||||||
|
# Enable collection for wall-stuck arrows with different delays
|
||||||
|
if stuck_to_wall:
|
||||||
|
# Shooter can collect after 0.2 seconds
|
||||||
|
if stick_timer >= shooter_collection_delay and not shooter_can_collect:
|
||||||
|
shooter_can_collect = true
|
||||||
|
_enable_collection_area()
|
||||||
|
# Everyone else can collect after 5 seconds
|
||||||
|
if stick_timer >= others_collection_delay and not can_be_collected:
|
||||||
|
can_be_collected = true
|
||||||
|
|
||||||
|
# Use appropriate duration based on what it's stuck to
|
||||||
|
var duration = wall_stick_duration if stuck_to_wall else stick_duration
|
||||||
|
|
||||||
|
if stick_timer >= duration:
|
||||||
# Start fading out after it sticks
|
# Start fading out after it sticks
|
||||||
modulate.a = max(0, 1 - (stick_timer - stick_duration) / 1.0) # Fade out over 1 second
|
modulate.a = max(0, 1 - (stick_timer - duration) / 1.0) # Fade out over 1 second
|
||||||
if stick_timer >= stick_duration + 1.0: # Extra second for fade out
|
if stick_timer >= duration + 1.0: # Extra second for fade out
|
||||||
queue_free() # Remove the arrow after fade out
|
queue_free() # Remove the arrow after fade out
|
||||||
|
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
func _physics_process(_delta: float) -> void:
|
func _physics_process(_delta: float) -> void:
|
||||||
@@ -86,6 +141,14 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
if is_stuck:
|
if is_stuck:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Arrow has landed (flew too long) - can't deal damage anymore
|
||||||
|
if not can_deal_damage:
|
||||||
|
# Check if it's a wall/terrain (not player/enemy) - stick to it
|
||||||
|
if not body.is_in_group("player") and not body.is_in_group("enemy"):
|
||||||
|
$SfxImpactWall.play()
|
||||||
|
_stick_to_wall()
|
||||||
|
return
|
||||||
|
|
||||||
# Don't hit the owner
|
# Don't hit the owner
|
||||||
if body == player_owner or body == initiated_by:
|
if body == player_owner or body == initiated_by:
|
||||||
return
|
return
|
||||||
@@ -94,30 +157,36 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
if body in hit_targets:
|
if body in hit_targets:
|
||||||
return
|
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
|
# Deal damage to players
|
||||||
if body.is_in_group("player") and body.has_method("rpc_take_damage"):
|
if body.is_in_group("player") and body.has_method("rpc_take_damage"):
|
||||||
|
# Add to hit_targets to prevent multiple hits on this target
|
||||||
|
hit_targets[body] = true
|
||||||
|
|
||||||
play_impact()
|
play_impact()
|
||||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
|
||||||
var player_peer_id = body.get_multiplayer_authority()
|
# CRITICAL: Only the projectile owner (authority) should deal damage to players
|
||||||
if player_peer_id != 0:
|
if player_owner and player_owner.is_multiplayer_authority():
|
||||||
if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id():
|
var attacker_pos = player_owner.global_position if player_owner else global_position
|
||||||
body.rpc_take_damage(20.0, attacker_pos) # TODO: Get actual damage from player
|
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:
|
else:
|
||||||
body.rpc_take_damage.rpc_id(player_peer_id, 20.0, attacker_pos)
|
body.rpc_take_damage.rpc(20.0, attacker_pos)
|
||||||
else:
|
|
||||||
body.rpc_take_damage.rpc(20.0, attacker_pos)
|
# Stick to target on ALL clients (both authority and non-authority)
|
||||||
_stick_to_target(body)
|
_stick_to_target(body)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Deal damage to enemies
|
# Deal damage to enemies
|
||||||
if body.is_in_group("enemy") and body.has_method("rpc_take_damage"):
|
if body.is_in_group("enemy") and body.has_method("rpc_take_damage"):
|
||||||
|
# CRITICAL: Only the authority should process enemy collisions
|
||||||
|
# This ensures hit/miss/dodge calculations happen once and are consistent
|
||||||
|
if player_owner and not player_owner.is_multiplayer_authority():
|
||||||
|
return # Non-authority ignores enemy collisions
|
||||||
|
|
||||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
var attacker_pos = player_owner.global_position if player_owner else global_position
|
||||||
var damage = 20.0 # TODO: Get actual damage from player
|
var damage = 20.0 # TODO: Get actual damage from player
|
||||||
if player_owner and player_owner.character_stats:
|
if player_owner and player_owner.character_stats:
|
||||||
@@ -131,12 +200,42 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
var is_miss = hit_roll >= hit_chance
|
var is_miss = hit_roll >= hit_chance
|
||||||
|
|
||||||
if is_miss:
|
if is_miss:
|
||||||
|
# MISS - arrow passes through enemy and continues flying!
|
||||||
if body.has_method("_show_damage_number"):
|
if body.has_method("_show_damage_number"):
|
||||||
body._show_damage_number(0.0, attacker_pos, false, true, false) # is_miss = true
|
body._show_damage_number(0.0, attacker_pos, false, true, false) # is_miss = true
|
||||||
_stick_to_target(body)
|
# Add to hit_targets so we don't check this enemy again
|
||||||
|
hit_targets[body] = true
|
||||||
|
# Sync miss to all clients - arrow continues flying
|
||||||
|
if is_inside_tree():
|
||||||
|
_sync_arrow_miss.rpc(body.get_path())
|
||||||
|
# Don't stick to target - let arrow continue flying
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check enemy dodge chance (based on enemy's DEX stat)
|
||||||
|
var dodge_roll = randf()
|
||||||
|
var dodge_chance = 0.0
|
||||||
|
if body.character_stats:
|
||||||
|
dodge_chance = body.character_stats.dodge_chance
|
||||||
|
var is_dodge = dodge_roll < dodge_chance
|
||||||
|
|
||||||
|
if is_dodge:
|
||||||
|
# DODGE - arrow passes through enemy and continues flying!
|
||||||
|
if body.has_method("_show_damage_number"):
|
||||||
|
body._show_damage_number(0.0, attacker_pos, false, false, true) # is_dodged = true
|
||||||
|
# Add to hit_targets so we don't check this enemy again
|
||||||
|
hit_targets[body] = true
|
||||||
|
# Sync dodge to all clients - arrow continues flying
|
||||||
|
if is_inside_tree():
|
||||||
|
_sync_arrow_dodge.rpc(body.get_path())
|
||||||
|
# Don't stick to target - let arrow continue flying
|
||||||
|
print(body.name, " DODGED arrow! Arrow continues flying...")
|
||||||
|
return
|
||||||
|
|
||||||
|
# HIT - add to hit_targets and stick to enemy
|
||||||
|
hit_targets[body] = true
|
||||||
play_impact()
|
play_impact()
|
||||||
|
|
||||||
|
# Deal damage
|
||||||
var enemy_peer_id = body.get_multiplayer_authority()
|
var enemy_peer_id = body.get_multiplayer_authority()
|
||||||
if enemy_peer_id != 0:
|
if enemy_peer_id != 0:
|
||||||
if multiplayer.is_server() and enemy_peer_id == multiplayer.get_unique_id():
|
if multiplayer.is_server() and enemy_peer_id == multiplayer.get_unique_id():
|
||||||
@@ -145,6 +244,11 @@ func _on_arrow_area_body_entered(body: Node2D) -> void:
|
|||||||
body.rpc_take_damage.rpc_id(enemy_peer_id, damage, attacker_pos, false)
|
body.rpc_take_damage.rpc_id(enemy_peer_id, damage, attacker_pos, false)
|
||||||
else:
|
else:
|
||||||
body.rpc_take_damage.rpc(damage, attacker_pos, false)
|
body.rpc_take_damage.rpc(damage, attacker_pos, false)
|
||||||
|
|
||||||
|
# Sync hit to all clients - arrow sticks
|
||||||
|
if is_inside_tree():
|
||||||
|
_sync_arrow_hit.rpc(body.get_path())
|
||||||
|
|
||||||
_stick_to_target(body)
|
_stick_to_target(body)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -175,5 +279,162 @@ func _stick_to_wall():
|
|||||||
# Stop the arrow
|
# Stop the arrow
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
is_stuck = true
|
is_stuck = true
|
||||||
|
stuck_to_wall = true
|
||||||
stick_timer = 0.0
|
stick_timer = 0.0
|
||||||
arrow_area.set_deferred("monitoring", false)
|
arrow_area.set_deferred("monitoring", false)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func _sync_arrow_collected():
|
||||||
|
# Sync arrow collection across network - mark as collected and remove
|
||||||
|
if not is_collected:
|
||||||
|
is_collected = true
|
||||||
|
print(name, " arrow collected (synced)")
|
||||||
|
# Queue free on next frame to avoid issues
|
||||||
|
call_deferred("queue_free")
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func _sync_arrow_hit(target_path: NodePath):
|
||||||
|
# Authority determined arrow HIT enemy - stick to it on all clients
|
||||||
|
var target = get_node_or_null(target_path)
|
||||||
|
if not target:
|
||||||
|
print("WARNING: Arrow hit target not found at path: ", target_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
if target not in hit_targets:
|
||||||
|
hit_targets[target] = true
|
||||||
|
play_impact()
|
||||||
|
_stick_to_target(target)
|
||||||
|
print("Arrow synced as HIT to: ", target.name)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func _sync_arrow_miss(target_path: NodePath):
|
||||||
|
# Authority determined arrow MISSED enemy - continues flying on all clients
|
||||||
|
var target = get_node_or_null(target_path)
|
||||||
|
if target and target not in hit_targets:
|
||||||
|
hit_targets[target] = true
|
||||||
|
print("Arrow synced as MISS - continuing through: ", target.name if target else "unknown")
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func _sync_arrow_dodge(target_path: NodePath):
|
||||||
|
# Authority determined enemy DODGED arrow - continues flying on all clients
|
||||||
|
var target = get_node_or_null(target_path)
|
||||||
|
if target and target not in hit_targets:
|
||||||
|
hit_targets[target] = true
|
||||||
|
print("Arrow synced as DODGE - continuing through: ", target.name if target else "unknown")
|
||||||
|
|
||||||
|
func _enable_collection_area():
|
||||||
|
# Create an Area2D for collecting the arrow
|
||||||
|
if collection_area:
|
||||||
|
return # Already created
|
||||||
|
|
||||||
|
collection_area = Area2D.new()
|
||||||
|
collection_area.name = "CollectionArea"
|
||||||
|
collection_area.collision_layer = 0
|
||||||
|
collection_area.collision_mask = 1 # Detect players (layer 1)
|
||||||
|
|
||||||
|
var collision_shape = CollisionShape2D.new()
|
||||||
|
var circle = CircleShape2D.new()
|
||||||
|
circle.radius = 16.0 # Collection radius
|
||||||
|
collision_shape.shape = circle
|
||||||
|
collection_area.add_child(collision_shape)
|
||||||
|
|
||||||
|
add_child(collection_area)
|
||||||
|
collection_area.body_entered.connect(_on_collection_area_body_entered)
|
||||||
|
|
||||||
|
func _on_collection_area_body_entered(body: Node2D):
|
||||||
|
if not is_instance_valid(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if it's a player
|
||||||
|
if not body.is_in_group("player"):
|
||||||
|
return
|
||||||
|
if is_collected:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only local players can collect
|
||||||
|
if not body.is_local_player:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if this player is allowed to collect
|
||||||
|
var is_shooter = (body == player_owner or body == initiated_by)
|
||||||
|
if is_shooter:
|
||||||
|
# Shooter can collect after short delay
|
||||||
|
if not shooter_can_collect:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Other players need to wait longer
|
||||||
|
if not can_be_collected:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create arrow item and add to player
|
||||||
|
if not body.character_stats or not is_instance_valid(body.character_stats):
|
||||||
|
print("ERROR: body.character_stats is invalid when trying to collect arrow")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if player has arrows equipped in offhand
|
||||||
|
var offhand_item = body.character_stats.equipment.get("offhand", null)
|
||||||
|
if offhand_item and is_instance_valid(offhand_item) and offhand_item.item_name == "Arrow":
|
||||||
|
is_collected = true
|
||||||
|
|
||||||
|
# Sync arrow collection to all clients
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
_sync_arrow_collected.rpc()
|
||||||
|
|
||||||
|
$SfxPickup.play()
|
||||||
|
# Add directly to equipped arrows
|
||||||
|
offhand_item.quantity += 1
|
||||||
|
body.character_stats.character_changed.emit(body.character_stats)
|
||||||
|
print(body.name, " collected arrow from wall! Total arrows: ", offhand_item.quantity)
|
||||||
|
await $SfxPickup.finished
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if offhand is empty and player has a bow equipped
|
||||||
|
var mainhand_item = body.character_stats.equipment.get("mainhand", null)
|
||||||
|
var has_bow = mainhand_item and is_instance_valid(mainhand_item) and mainhand_item.weapon_type == Item.WeaponType.BOW
|
||||||
|
|
||||||
|
if not offhand_item and has_bow:
|
||||||
|
# Re-equip arrow to offhand if player has a bow and offhand is empty
|
||||||
|
var new_arrow = ItemDatabase.create_item("arrow")
|
||||||
|
if new_arrow and is_instance_valid(new_arrow):
|
||||||
|
is_collected = true
|
||||||
|
|
||||||
|
# Sync arrow collection to all clients
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
_sync_arrow_collected.rpc()
|
||||||
|
|
||||||
|
$SfxPickup.play()
|
||||||
|
new_arrow.quantity = 1
|
||||||
|
body.character_stats.equipment["offhand"] = new_arrow
|
||||||
|
body.character_stats.character_changed.emit(body.character_stats)
|
||||||
|
print(body.name, " collected arrow and re-equipped to offhand!")
|
||||||
|
await $SfxPickup.finished
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add to inventory (will stack if arrows already in inventory)
|
||||||
|
var inventory_arrow = ItemDatabase.create_item("arrow")
|
||||||
|
if not inventory_arrow or not is_instance_valid(inventory_arrow):
|
||||||
|
print("ERROR: Failed to create arrow item for inventory")
|
||||||
|
return
|
||||||
|
|
||||||
|
inventory_arrow.quantity = 1
|
||||||
|
if not body.character_stats.has_method("add_item_to_inventory"):
|
||||||
|
print("ERROR: character_stats missing add_item_to_inventory method")
|
||||||
|
return
|
||||||
|
|
||||||
|
var success = body.character_stats.add_item_to_inventory(inventory_arrow)
|
||||||
|
if success:
|
||||||
|
is_collected = true
|
||||||
|
|
||||||
|
# Sync arrow collection to all clients
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
_sync_arrow_collected.rpc()
|
||||||
|
|
||||||
|
$SfxPickup.play()
|
||||||
|
print(body.name, " collected arrow from wall into inventory!")
|
||||||
|
await $SfxPickup.finished
|
||||||
|
queue_free()
|
||||||
|
else:
|
||||||
|
print(body.name, " inventory full, couldn't collect arrow")
|
||||||
|
# Don't remove arrow if inventory is full
|
||||||
|
|||||||
@@ -73,7 +73,8 @@ var equipment:Dictionary = {
|
|||||||
"end": 10,
|
"end": 10,
|
||||||
"wis": 10,
|
"wis": 10,
|
||||||
"cha": 10,
|
"cha": 10,
|
||||||
"lck": 10
|
"lck": 10,
|
||||||
|
"per": 10 # Perception - affects trap detection
|
||||||
}
|
}
|
||||||
|
|
||||||
@export var def: int = 0
|
@export var def: int = 0
|
||||||
@@ -95,16 +96,20 @@ func get_total_weight() -> float:
|
|||||||
for item in inventory:
|
for item in inventory:
|
||||||
if item:
|
if item:
|
||||||
total += item.weight * item.quantity
|
total += item.weight * item.quantity
|
||||||
# Count equipped items
|
# Count equipped items (also multiply by quantity for stackable items like arrows)
|
||||||
for slot in equipment.values():
|
for slot in equipment.values():
|
||||||
if slot:
|
if slot:
|
||||||
total += slot.weight
|
if slot.can_have_multiple_of:
|
||||||
|
total += slot.weight * slot.quantity
|
||||||
|
else:
|
||||||
|
total += slot.weight
|
||||||
return total
|
return total
|
||||||
|
|
||||||
# Calculate carrying capacity based on STR
|
# Calculate carrying capacity based on STR
|
||||||
func get_carrying_capacity() -> float:
|
func get_carrying_capacity() -> float:
|
||||||
# Base capacity: 20 + (STR * 5)
|
# Base capacity: 7 + (STR * 1.75) - reduced to 35% of original
|
||||||
return 20.0 + (baseStats.str * 5.0)
|
# Example: 10 STR = 24.5, 20 STR = 42
|
||||||
|
return 7.0 + (baseStats.str * 1.75)
|
||||||
|
|
||||||
# Check if over-encumbered
|
# Check if over-encumbered
|
||||||
func is_over_encumbered() -> bool:
|
func is_over_encumbered() -> bool:
|
||||||
@@ -245,7 +250,7 @@ func level_up() -> void:
|
|||||||
var num_stats_to_increase = randi_range(2, 3)
|
var num_stats_to_increase = randi_range(2, 3)
|
||||||
|
|
||||||
# All available stats (excluding cha for now as per user request)
|
# All available stats (excluding cha for now as per user request)
|
||||||
var available_stats = ["str", "dex", "int", "end", "wis", "lck"]
|
var available_stats = ["str", "dex", "int", "end", "wis", "lck", "per"]
|
||||||
|
|
||||||
# Shuffle and pick random stats
|
# Shuffle and pick random stats
|
||||||
var stats_to_increase = []
|
var stats_to_increase = []
|
||||||
@@ -550,27 +555,37 @@ func add_item(iItem:Item):
|
|||||||
func unequip_item(iItem:Item, updateChar:bool = true):
|
func unequip_item(iItem:Item, updateChar:bool = true):
|
||||||
if iItem.equipment_type == Item.EquipmentType.NONE:
|
if iItem.equipment_type == Item.EquipmentType.NONE:
|
||||||
return
|
return
|
||||||
self.inventory.push_back(iItem)
|
|
||||||
|
# Check if we can stack with existing item in inventory (for stackable items like arrows)
|
||||||
|
var stacked = false
|
||||||
|
if iItem.can_have_multiple_of:
|
||||||
|
for inv_item in inventory:
|
||||||
|
if inv_item and inv_item.can_have_multiple_of and inv_item.item_name == iItem.item_name:
|
||||||
|
# Found matching stackable item in inventory - merge quantities
|
||||||
|
inv_item.quantity += iItem.quantity
|
||||||
|
stacked = true
|
||||||
|
print("Unequipped ", iItem.quantity, " ", iItem.item_name, " and stacked with inventory (new total: ", inv_item.quantity, ")")
|
||||||
|
break
|
||||||
|
|
||||||
|
# If not stacked, add to inventory normally
|
||||||
|
if not stacked:
|
||||||
|
self.inventory.push_back(iItem)
|
||||||
|
|
||||||
|
# Clear equipment slot
|
||||||
match iItem.equipment_type:
|
match iItem.equipment_type:
|
||||||
Item.EquipmentType.MAINHAND:
|
Item.EquipmentType.MAINHAND:
|
||||||
equipment["mainhand"] = null
|
equipment["mainhand"] = null
|
||||||
pass
|
|
||||||
Item.EquipmentType.OFFHAND:
|
Item.EquipmentType.OFFHAND:
|
||||||
equipment["offhand"] = null
|
equipment["offhand"] = null
|
||||||
pass
|
|
||||||
Item.EquipmentType.HEADGEAR:
|
Item.EquipmentType.HEADGEAR:
|
||||||
equipment["headgear"] = null
|
equipment["headgear"] = null
|
||||||
pass
|
|
||||||
Item.EquipmentType.ARMOUR:
|
Item.EquipmentType.ARMOUR:
|
||||||
equipment["armour"] = null
|
equipment["armour"] = null
|
||||||
pass
|
|
||||||
Item.EquipmentType.BOOTS:
|
Item.EquipmentType.BOOTS:
|
||||||
equipment["boots"] = null
|
equipment["boots"] = null
|
||||||
pass
|
|
||||||
Item.EquipmentType.ACCESSORY:
|
Item.EquipmentType.ACCESSORY:
|
||||||
equipment["accessory"] = null
|
equipment["accessory"] = null
|
||||||
pass
|
|
||||||
pass
|
|
||||||
if updateChar:
|
if updateChar:
|
||||||
emit_signal("character_changed", self)
|
emit_signal("character_changed", self)
|
||||||
pass
|
pass
|
||||||
@@ -587,55 +602,74 @@ func equip_item(iItem:Item, insert_index: int = -1):
|
|||||||
var old_item = null
|
var old_item = null
|
||||||
var item_index = self.inventory.find(iItem)
|
var item_index = self.inventory.find(iItem)
|
||||||
|
|
||||||
|
# Check if we can stack with already equipped item (for stackable items like arrows)
|
||||||
|
var can_stack = false
|
||||||
|
var equipped_item = null
|
||||||
|
|
||||||
match iItem.equipment_type:
|
match iItem.equipment_type:
|
||||||
Item.EquipmentType.MAINHAND:
|
Item.EquipmentType.MAINHAND:
|
||||||
if equipment["mainhand"] != null:
|
equipped_item = equipment["mainhand"]
|
||||||
old_item = equipment["mainhand"]
|
|
||||||
equipment["mainhand"] = iItem
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
Item.EquipmentType.OFFHAND:
|
Item.EquipmentType.OFFHAND:
|
||||||
if equipment["offhand"] != null:
|
equipped_item = equipment["offhand"]
|
||||||
old_item = equipment["offhand"]
|
|
||||||
equipment["offhand"] = iItem
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
Item.EquipmentType.HEADGEAR:
|
Item.EquipmentType.HEADGEAR:
|
||||||
if equipment["headgear"] != null:
|
equipped_item = equipment["headgear"]
|
||||||
old_item = equipment["headgear"]
|
|
||||||
equipment["headgear"] = iItem
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
Item.EquipmentType.ARMOUR:
|
Item.EquipmentType.ARMOUR:
|
||||||
if equipment["armour"] != null:
|
equipped_item = equipment["armour"]
|
||||||
old_item = equipment["armour"]
|
|
||||||
equipment["armour"] = iItem
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
Item.EquipmentType.BOOTS:
|
Item.EquipmentType.BOOTS:
|
||||||
if equipment["boots"] != null:
|
equipped_item = equipment["boots"]
|
||||||
old_item = equipment["boots"]
|
|
||||||
equipment["boots"] = iItem
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
Item.EquipmentType.ACCESSORY:
|
Item.EquipmentType.ACCESSORY:
|
||||||
if equipment["accessory"] != null:
|
equipped_item = equipment["accessory"]
|
||||||
old_item = equipment["accessory"]
|
|
||||||
equipment["accessory"] = iItem
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Remove the item being equipped from inventory first
|
# Check if items can stack (same item name, both stackable)
|
||||||
if item_index >= 0:
|
if equipped_item and iItem.can_have_multiple_of and equipped_item.can_have_multiple_of:
|
||||||
self.inventory.remove_at(item_index)
|
if iItem.item_name == equipped_item.item_name:
|
||||||
|
can_stack = true
|
||||||
|
|
||||||
# Add old item back to inventory at the specified position (or end if -1)
|
if can_stack:
|
||||||
if old_item != null:
|
# Stack quantities together
|
||||||
if insert_index >= 0 and insert_index <= self.inventory.size():
|
equipped_item.quantity += iItem.quantity
|
||||||
self.inventory.insert(insert_index, old_item)
|
# Remove the item from inventory since we merged it
|
||||||
else:
|
if item_index >= 0:
|
||||||
self.inventory.push_back(old_item)
|
self.inventory.remove_at(item_index)
|
||||||
|
print("Stacked ", iItem.quantity, " ", iItem.item_name, " with equipped (new total: ", equipped_item.quantity, ")")
|
||||||
|
else:
|
||||||
|
# Normal equip (swap items)
|
||||||
|
match iItem.equipment_type:
|
||||||
|
Item.EquipmentType.MAINHAND:
|
||||||
|
if equipment["mainhand"] != null:
|
||||||
|
old_item = equipment["mainhand"]
|
||||||
|
equipment["mainhand"] = iItem
|
||||||
|
Item.EquipmentType.OFFHAND:
|
||||||
|
if equipment["offhand"] != null:
|
||||||
|
old_item = equipment["offhand"]
|
||||||
|
equipment["offhand"] = iItem
|
||||||
|
Item.EquipmentType.HEADGEAR:
|
||||||
|
if equipment["headgear"] != null:
|
||||||
|
old_item = equipment["headgear"]
|
||||||
|
equipment["headgear"] = iItem
|
||||||
|
Item.EquipmentType.ARMOUR:
|
||||||
|
if equipment["armour"] != null:
|
||||||
|
old_item = equipment["armour"]
|
||||||
|
equipment["armour"] = iItem
|
||||||
|
Item.EquipmentType.BOOTS:
|
||||||
|
if equipment["boots"] != null:
|
||||||
|
old_item = equipment["boots"]
|
||||||
|
equipment["boots"] = iItem
|
||||||
|
Item.EquipmentType.ACCESSORY:
|
||||||
|
if equipment["accessory"] != null:
|
||||||
|
old_item = equipment["accessory"]
|
||||||
|
equipment["accessory"] = iItem
|
||||||
|
|
||||||
|
# Remove the item being equipped from inventory first
|
||||||
|
if item_index >= 0:
|
||||||
|
self.inventory.remove_at(item_index)
|
||||||
|
|
||||||
|
# Add old item back to inventory at the specified position (or end if -1)
|
||||||
|
if old_item != null:
|
||||||
|
if insert_index >= 0 and insert_index <= self.inventory.size():
|
||||||
|
self.inventory.insert(insert_index, old_item)
|
||||||
|
else:
|
||||||
|
self.inventory.push_back(old_item)
|
||||||
|
|
||||||
emit_signal("character_changed", self)
|
emit_signal("character_changed", self)
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -339,6 +339,9 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
|
|||||||
var room_objects = _place_interactable_objects_in_room(room, grid, map_size, all_doors, all_enemies, rng, room_puzzle_data)
|
var room_objects = _place_interactable_objects_in_room(room, grid, map_size, all_doors, all_enemies, rng, room_puzzle_data)
|
||||||
all_interactable_objects.append_array(room_objects)
|
all_interactable_objects.append_array(room_objects)
|
||||||
|
|
||||||
|
# 9.6. Place traps (1-2 per level, excluding start and exit rooms)
|
||||||
|
var all_traps = _place_traps_in_dungeon(all_rooms, start_room_index, exit_room_index, grid, map_size, rng)
|
||||||
|
|
||||||
# NOTE: Stairs placement was moved earlier (step 7.5, before torches) to prevent torch overlap
|
# NOTE: Stairs placement was moved earlier (step 7.5, before torches) to prevent torch overlap
|
||||||
# NOTE: Blocking doors placement was moved earlier (step 11, before enemy placement) to identify spawner puzzle rooms
|
# NOTE: Blocking doors placement was moved earlier (step 11, before enemy placement) to identify spawner puzzle rooms
|
||||||
|
|
||||||
@@ -348,6 +351,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
|
|||||||
"torches": all_torches,
|
"torches": all_torches,
|
||||||
"enemies": all_enemies,
|
"enemies": all_enemies,
|
||||||
"interactable_objects": all_interactable_objects,
|
"interactable_objects": all_interactable_objects,
|
||||||
|
"traps": all_traps,
|
||||||
"stairs": stairs_data,
|
"stairs": stairs_data,
|
||||||
"blocking_doors": blocking_doors,
|
"blocking_doors": blocking_doors,
|
||||||
"grid": grid,
|
"grid": grid,
|
||||||
@@ -2725,3 +2729,75 @@ func _determine_door_direction_for_puzzle_room(door: Dictionary, puzzle_room: Di
|
|||||||
return "Left" # Door is left of puzzle room center - door is on left wall
|
return "Left" # Door is left of puzzle room center - door is on left wall
|
||||||
else:
|
else:
|
||||||
return "Right" # Door is right of puzzle room center - door is on right wall
|
return "Right" # Door is right of puzzle room center - door is on right wall
|
||||||
|
|
||||||
|
func _place_traps_in_dungeon(all_rooms: Array, start_room_index: int, exit_room_index: int, grid: Array, map_size: Vector2i, rng: RandomNumberGenerator) -> Array:
|
||||||
|
# Place 1-2 traps in the dungeon (not in start or exit rooms)
|
||||||
|
var traps = []
|
||||||
|
var tile_size = 16
|
||||||
|
|
||||||
|
# Get eligible rooms (not start, not exit)
|
||||||
|
var eligible_rooms = []
|
||||||
|
for i in range(all_rooms.size()):
|
||||||
|
if i != start_room_index and i != exit_room_index:
|
||||||
|
eligible_rooms.append(all_rooms[i])
|
||||||
|
|
||||||
|
# If no eligible rooms, return empty array
|
||||||
|
if eligible_rooms.size() == 0:
|
||||||
|
LogManager.log("DungeonGenerator: No eligible rooms for trap placement", LogManager.CATEGORY_DUNGEON)
|
||||||
|
return traps
|
||||||
|
|
||||||
|
# Decide number of traps (1 or 2)
|
||||||
|
var num_traps = rng.randi_range(1, 2)
|
||||||
|
LogManager.log("DungeonGenerator: Placing " + str(num_traps) + " trap(s) in dungeon", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
|
# Place traps in random eligible rooms
|
||||||
|
for trap_idx in range(num_traps):
|
||||||
|
if eligible_rooms.size() == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Pick random room
|
||||||
|
var room_index = rng.randi() % eligible_rooms.size()
|
||||||
|
var room = eligible_rooms[room_index]
|
||||||
|
|
||||||
|
# Remove room from eligible list to avoid placing multiple traps in same room
|
||||||
|
eligible_rooms.remove_at(room_index)
|
||||||
|
|
||||||
|
# Try to find a valid floor position in the room (not near walls, doors, or blocking cells)
|
||||||
|
var attempts = 50
|
||||||
|
var trap_placed = false
|
||||||
|
|
||||||
|
while attempts > 0 and not trap_placed:
|
||||||
|
# Random position in room floor (excluding 2-tile wall border, plus extra safety margin)
|
||||||
|
var floor_margin = 3 # Extra margin from walls for safety
|
||||||
|
var local_x = rng.randi_range(floor_margin, room.w - floor_margin - 1)
|
||||||
|
var local_y = rng.randi_range(floor_margin, room.h - floor_margin - 1)
|
||||||
|
var world_x = room.x + local_x
|
||||||
|
var world_y = room.y + local_y
|
||||||
|
|
||||||
|
# Check if position is valid (floor tile, not blocked)
|
||||||
|
if world_x >= 0 and world_x < map_size.x and world_y >= 0 and world_y < map_size.y:
|
||||||
|
if grid[world_x][world_y] == 1: # Floor tile
|
||||||
|
# Check if position is not too close to door (avoid blocking doorways)
|
||||||
|
var too_close_to_door = false
|
||||||
|
# Simplified check - just ensure we're not right at door position
|
||||||
|
# (More complex check could look at actual door positions)
|
||||||
|
|
||||||
|
if not too_close_to_door:
|
||||||
|
# Valid position - place trap
|
||||||
|
var trap_world_x = world_x * tile_size + tile_size / 2
|
||||||
|
var trap_world_y = world_y * tile_size + tile_size / 2
|
||||||
|
|
||||||
|
traps.append({
|
||||||
|
"position": Vector2(trap_world_x, trap_world_y),
|
||||||
|
"room": room
|
||||||
|
})
|
||||||
|
|
||||||
|
trap_placed = true
|
||||||
|
LogManager.log("DungeonGenerator: Placed trap at (" + str(trap_world_x) + ", " + str(trap_world_y) + ") in room (" + str(room.x) + ", " + str(room.y) + ")", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
|
attempts -= 1
|
||||||
|
|
||||||
|
if not trap_placed:
|
||||||
|
LogManager.log("DungeonGenerator: Failed to place trap in room (" + str(room.x) + ", " + str(room.y) + ") after 50 attempts", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
|
return traps
|
||||||
|
|||||||
@@ -1754,6 +1754,7 @@ func _init_mouse_cursor():
|
|||||||
cursor_sprite.hframes = 2
|
cursor_sprite.hframes = 2
|
||||||
cursor_sprite.vframes = 1
|
cursor_sprite.vframes = 1
|
||||||
cursor_sprite.frame = 0 # Frame 0 = free movement
|
cursor_sprite.frame = 0 # Frame 0 = free movement
|
||||||
|
cursor_sprite.modulate.a = 0.75 # 75% opacity
|
||||||
cursor_layer.add_child(cursor_sprite)
|
cursor_layer.add_child(cursor_sprite)
|
||||||
|
|
||||||
# Create grid-locked cursor sprite (frame 1)
|
# Create grid-locked cursor sprite (frame 1)
|
||||||
@@ -1763,7 +1764,7 @@ func _init_mouse_cursor():
|
|||||||
grid_cursor_sprite.hframes = 2
|
grid_cursor_sprite.hframes = 2
|
||||||
grid_cursor_sprite.vframes = 1
|
grid_cursor_sprite.vframes = 1
|
||||||
grid_cursor_sprite.frame = 1 # Frame 1 = grid-locked
|
grid_cursor_sprite.frame = 1 # Frame 1 = grid-locked
|
||||||
grid_cursor_sprite.modulate.a = 0.5 # 50% opacity
|
grid_cursor_sprite.modulate.a = 0.3 # 30% opacity
|
||||||
cursor_layer.add_child(grid_cursor_sprite)
|
cursor_layer.add_child(grid_cursor_sprite)
|
||||||
|
|
||||||
# Hide system cursor
|
# Hide system cursor
|
||||||
@@ -1826,20 +1827,30 @@ func _update_mouse_cursor(delta: float):
|
|||||||
var base_color = Color(1.0, 1.0, 1.0)
|
var base_color = Color(1.0, 1.0, 1.0)
|
||||||
var pulse_color = Color(1.5, 1.2, 1.0)
|
var pulse_color = Color(1.5, 1.2, 1.0)
|
||||||
grid_cursor_sprite.modulate = base_color.lerp(pulse_color, pulse_value * 0.5) # 50% of the way to pulse color
|
grid_cursor_sprite.modulate = base_color.lerp(pulse_color, pulse_value * 0.5) # 50% of the way to pulse color
|
||||||
grid_cursor_sprite.modulate.a = 0.5 # Keep opacity at 50%
|
grid_cursor_sprite.modulate.a = 0.3 # Keep opacity at 30%
|
||||||
|
|
||||||
# Update player facing direction based on mouse position (use world position)
|
# Update player facing direction based on mouse position (use world position)
|
||||||
|
# Only update if mouse is inside the window viewport
|
||||||
|
var viewport_rect = get_viewport().get_visible_rect()
|
||||||
|
var mouse_in_window = viewport_rect.has_point(mouse_pos)
|
||||||
|
|
||||||
if local_players.size() > 0:
|
if local_players.size() > 0:
|
||||||
var player = local_players[0] # Use first local player
|
var player = local_players[0] # Use first local player
|
||||||
if player and is_instance_valid(player) and player.is_local_player:
|
if player and is_instance_valid(player) and player.is_local_player:
|
||||||
var player_pos = player.global_position
|
if mouse_in_window:
|
||||||
# Use grid-locked position if available, otherwise use free mouse position
|
# Mouse is in window - use mouse for direction control
|
||||||
var target_world_pos = grid_locked_world_pos if show_grid_cursor else world_pos
|
var player_pos = player.global_position
|
||||||
var mouse_direction = (target_world_pos - player_pos).normalized()
|
# Use grid-locked position if available, otherwise use free mouse position
|
||||||
|
var target_world_pos = grid_locked_world_pos if show_grid_cursor else world_pos
|
||||||
|
var mouse_direction = (target_world_pos - player_pos).normalized()
|
||||||
|
|
||||||
# Only update facing if mouse is far enough from player
|
# Only update facing if mouse is far enough from player
|
||||||
if mouse_direction.length() > 0.1:
|
if mouse_direction.length() > 0.1:
|
||||||
player._update_facing_from_mouse(mouse_direction)
|
player._update_facing_from_mouse(mouse_direction)
|
||||||
|
else:
|
||||||
|
# Mouse is outside window - disable mouse control (use WASD/movement for direction)
|
||||||
|
if "mouse_control_active" in player:
|
||||||
|
player.mouse_control_active = false
|
||||||
|
|
||||||
func _init_fog_of_war():
|
func _init_fog_of_war():
|
||||||
if dungeon_data.is_empty() or not dungeon_data.has("map_size"):
|
if dungeon_data.is_empty() or not dungeon_data.has("map_size"):
|
||||||
@@ -2356,6 +2367,9 @@ func _generate_dungeon():
|
|||||||
# Spawn interactable objects
|
# Spawn interactable objects
|
||||||
_spawn_interactable_objects()
|
_spawn_interactable_objects()
|
||||||
|
|
||||||
|
# Spawn traps
|
||||||
|
_spawn_traps()
|
||||||
|
|
||||||
# Spawn blocking doors
|
# Spawn blocking doors
|
||||||
_spawn_blocking_doors()
|
_spawn_blocking_doors()
|
||||||
|
|
||||||
@@ -3310,6 +3324,7 @@ func _pack_dungeon_blob():
|
|||||||
"torches": dungeon_data.get("torches", []),
|
"torches": dungeon_data.get("torches", []),
|
||||||
"enemies": dungeon_data.get("enemies", []),
|
"enemies": dungeon_data.get("enemies", []),
|
||||||
"interactable_objects": dungeon_data.get("interactable_objects", []),
|
"interactable_objects": dungeon_data.get("interactable_objects", []),
|
||||||
|
"traps": dungeon_data.get("traps", []),
|
||||||
"stairs": dungeon_data.get("stairs", {}),
|
"stairs": dungeon_data.get("stairs", {}),
|
||||||
"blocking_doors": dungeon_data.get("blocking_doors", [])
|
"blocking_doors": dungeon_data.get("blocking_doors", [])
|
||||||
}
|
}
|
||||||
@@ -3318,8 +3333,9 @@ func _pack_dungeon_blob():
|
|||||||
var enemy_count = full_dungeon_data.enemies.size() if full_dungeon_data.enemies is Array else 0
|
var enemy_count = full_dungeon_data.enemies.size() if full_dungeon_data.enemies is Array else 0
|
||||||
var torch_count = full_dungeon_data.torches.size() if full_dungeon_data.torches is Array else 0
|
var torch_count = full_dungeon_data.torches.size() if full_dungeon_data.torches is Array else 0
|
||||||
var object_count = full_dungeon_data.interactable_objects.size() if full_dungeon_data.interactable_objects is Array else 0
|
var object_count = full_dungeon_data.interactable_objects.size() if full_dungeon_data.interactable_objects is Array else 0
|
||||||
print("GameWorld: HOST - Packing dungeon blob with: ", enemy_count, " enemies, ", torch_count, " torches, ", object_count, " interactable objects")
|
var trap_count = full_dungeon_data.traps.size() if full_dungeon_data.traps is Array else 0
|
||||||
LogManager.log("GameWorld: Packing dungeon blob - enemies: " + str(enemy_count) + ", torches: " + str(torch_count) + ", objects: " + str(object_count), LogManager.CATEGORY_DUNGEON)
|
print("GameWorld: HOST - Packing dungeon blob with: ", enemy_count, " enemies, ", torch_count, " torches, ", object_count, " interactable objects, ", trap_count, " traps")
|
||||||
|
LogManager.log("GameWorld: Packing dungeon blob - enemies: " + str(enemy_count) + ", torches: " + str(torch_count) + ", objects: " + str(object_count) + ", traps: " + str(trap_count), LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
# Store only STATIC metadata (dynamic state is collected on-demand when syncing to clients)
|
# Store only STATIC metadata (dynamic state is collected on-demand when syncing to clients)
|
||||||
# This ensures joiners always get the current world state, not what it was when blob was packed
|
# This ensures joiners always get the current world state, not what it was when blob was packed
|
||||||
@@ -3414,6 +3430,12 @@ func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, h
|
|||||||
# Clear previous level on client
|
# Clear previous level on client
|
||||||
_clear_level()
|
_clear_level()
|
||||||
|
|
||||||
|
# Reset all player grab/lift/push states to prevent being stuck in lifting animation
|
||||||
|
var all_players = get_tree().get_nodes_in_group("player")
|
||||||
|
for player in all_players:
|
||||||
|
if player and is_instance_valid(player) and player.has_method("reset_grab_state"):
|
||||||
|
player.reset_grab_state()
|
||||||
|
|
||||||
# Wait for old entities to be fully freed before spawning new ones
|
# Wait for old entities to be fully freed before spawning new ones
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete
|
await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete
|
||||||
@@ -4015,6 +4037,12 @@ func _check_and_render_dungeon():
|
|||||||
print("GameWorld: Client - Dungeon data assembled! Rendering...")
|
print("GameWorld: Client - Dungeon data assembled! Rendering...")
|
||||||
LogManager.log("GameWorld: Client assembled dungeon from chunks for level " + str(current_level), LogManager.CATEGORY_DUNGEON)
|
LogManager.log("GameWorld: Client assembled dungeon from chunks for level " + str(current_level), LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
|
# Reset all player grab/lift/push states to prevent being stuck in lifting animation
|
||||||
|
var all_players = get_tree().get_nodes_in_group("player")
|
||||||
|
for player in all_players:
|
||||||
|
if player and is_instance_valid(player) and player.has_method("reset_grab_state"):
|
||||||
|
player.reset_grab_state()
|
||||||
|
|
||||||
# Fix player appearance
|
# Fix player appearance
|
||||||
_fix_player_appearance_after_dungeon_sync()
|
_fix_player_appearance_after_dungeon_sync()
|
||||||
|
|
||||||
@@ -4348,6 +4376,62 @@ func _spawn_enemies():
|
|||||||
else:
|
else:
|
||||||
print("GameWorld: [CLEANUP] No defeated enemies found in scene to remove (all were properly skipped during spawn)")
|
print("GameWorld: [CLEANUP] No defeated enemies found in scene to remove (all were properly skipped during spawn)")
|
||||||
|
|
||||||
|
func _spawn_traps():
|
||||||
|
# Spawn traps from dungeon data
|
||||||
|
if dungeon_data.is_empty() or not dungeon_data.has("traps"):
|
||||||
|
LogManager.log("GameWorld: No traps to spawn", LogManager.CATEGORY_DUNGEON)
|
||||||
|
return
|
||||||
|
|
||||||
|
var is_server = multiplayer.is_server() or not multiplayer.has_multiplayer_peer()
|
||||||
|
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if not entities_node:
|
||||||
|
push_error("ERROR: Could not find Entities node!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Remove existing traps first (avoid name conflicts / desync)
|
||||||
|
var traps_to_remove = []
|
||||||
|
for child in entities_node.get_children():
|
||||||
|
if child.is_in_group("trap"):
|
||||||
|
traps_to_remove.append(child)
|
||||||
|
|
||||||
|
for trap in traps_to_remove:
|
||||||
|
print("GameWorld: Removing old trap: ", trap.name)
|
||||||
|
if is_instance_valid(trap):
|
||||||
|
trap.queue_free()
|
||||||
|
|
||||||
|
# Wait a frame to ensure removals are processed
|
||||||
|
if traps_to_remove.size() > 0:
|
||||||
|
await get_tree().process_frame
|
||||||
|
|
||||||
|
var traps = dungeon_data.traps
|
||||||
|
if traps == null or not traps is Array:
|
||||||
|
print("GameWorld: WARNING: dungeon_data.traps is not an Array!")
|
||||||
|
return
|
||||||
|
|
||||||
|
LogManager.log("GameWorld: Spawning " + str(traps.size()) + " trap(s)", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
|
var trap_scene = load("res://scenes/trap.tscn")
|
||||||
|
if not trap_scene:
|
||||||
|
push_error("ERROR: Could not load trap scene!")
|
||||||
|
return
|
||||||
|
|
||||||
|
for i in range(traps.size()):
|
||||||
|
var trap_data = traps[i]
|
||||||
|
if not trap_data is Dictionary:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var trap = trap_scene.instantiate()
|
||||||
|
trap.name = "Trap_" + str(i)
|
||||||
|
trap.global_position = trap_data.position
|
||||||
|
|
||||||
|
# Set multiplayer authority to server
|
||||||
|
if is_server:
|
||||||
|
trap.set_multiplayer_authority(1) # Server is authority
|
||||||
|
|
||||||
|
entities_node.add_child(trap, true)
|
||||||
|
LogManager.log("GameWorld: Spawned trap at " + str(trap_data.position), LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
func _spawn_interactable_objects():
|
func _spawn_interactable_objects():
|
||||||
# Spawn interactable objects from dungeon data
|
# Spawn interactable objects from dungeon data
|
||||||
if dungeon_data.is_empty() or not dungeon_data.has("interactable_objects"):
|
if dungeon_data.is_empty() or not dungeon_data.has("interactable_objects"):
|
||||||
@@ -5549,6 +5633,12 @@ func _move_all_players_to_start_room():
|
|||||||
if dungeon_data.is_empty() or not dungeon_data.has("start_room"):
|
if dungeon_data.is_empty() or not dungeon_data.has("start_room"):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Reset all player grab/lift/push states to prevent being stuck in lifting animation
|
||||||
|
var all_players = get_tree().get_nodes_in_group("player")
|
||||||
|
for player in all_players:
|
||||||
|
if player and is_instance_valid(player) and player.has_method("reset_grab_state"):
|
||||||
|
player.reset_grab_state()
|
||||||
|
|
||||||
var start_room = dungeon_data.start_room
|
var start_room = dungeon_data.start_room
|
||||||
_update_spawn_points(start_room)
|
_update_spawn_points(start_room)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ var held_by_player = null
|
|||||||
var is_frozen: bool = false
|
var is_frozen: bool = false
|
||||||
var thrown_by_player = null # Track who threw this box
|
var thrown_by_player = null # Track who threw this box
|
||||||
var is_broken: bool = false
|
var is_broken: bool = false
|
||||||
|
var has_dealt_damage: bool = false # Track if this thrown object has already damaged something
|
||||||
|
|
||||||
# Physics for thrown objects
|
# Physics for thrown objects
|
||||||
var throw_velocity: Vector2 = Vector2.ZERO
|
var throw_velocity: Vector2 = Vector2.ZERO
|
||||||
@@ -129,6 +130,7 @@ func _land():
|
|||||||
is_being_held = false # Make sure it can be grabbed again
|
is_being_held = false # Make sure it can be grabbed again
|
||||||
held_by_player = null
|
held_by_player = null
|
||||||
thrown_by_player = null # Clear who threw it
|
thrown_by_player = null # Clear who threw it
|
||||||
|
has_dealt_damage = false # Reset damage flag for next throw
|
||||||
|
|
||||||
# Re-enable collision when landing
|
# Re-enable collision when landing
|
||||||
set_collision_layer_value(2, true)
|
set_collision_layer_value(2, true)
|
||||||
@@ -158,15 +160,20 @@ func _land():
|
|||||||
|
|
||||||
func _handle_air_collision():
|
func _handle_air_collision():
|
||||||
# Handle collision while airborne
|
# Handle collision while airborne
|
||||||
|
# CRITICAL: Only allow ONE damage event per throw
|
||||||
|
if has_dealt_damage:
|
||||||
|
return
|
||||||
|
|
||||||
for i in get_slide_collision_count():
|
for i in get_slide_collision_count():
|
||||||
var collision = get_slide_collision(i)
|
var collision = get_slide_collision(i)
|
||||||
var collider = collision.get_collider()
|
var collider = collision.get_collider()
|
||||||
|
|
||||||
# Pot special case: break on wall collision
|
# Break on wall collision (pots and boxes)
|
||||||
if object_type == "Pot" and _is_wall_collider(collider):
|
if (object_type == "Pot" or object_type == "Box") and _is_wall_collider(collider):
|
||||||
# Only process on server to prevent duplicates
|
# Only process on server to prevent duplicates
|
||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
continue
|
continue
|
||||||
|
has_dealt_damage = true # Mark as dealt damage (wall hit counts)
|
||||||
if is_destroyable:
|
if is_destroyable:
|
||||||
if multiplayer.has_multiplayer_peer():
|
if multiplayer.has_multiplayer_peer():
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
@@ -181,11 +188,15 @@ func _handle_air_collision():
|
|||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Damage enemy
|
has_dealt_damage = true # Mark as dealt damage - can't damage anything else now
|
||||||
|
|
||||||
|
# Damage enemy (pots deal less damage than boxes)
|
||||||
|
# Enemy's take_damage() already handles defense calculation
|
||||||
if collider.has_method("take_damage"):
|
if collider.has_method("take_damage"):
|
||||||
var attacker_pos = thrown_by_player.global_position if thrown_by_player and is_instance_valid(thrown_by_player) else global_position
|
var attacker_pos = thrown_by_player.global_position if thrown_by_player and is_instance_valid(thrown_by_player) else global_position
|
||||||
collider.take_damage(15.0, attacker_pos)
|
var base_damage = 10.0 if object_type == "Pot" else 15.0
|
||||||
print(name, " hit enemy ", collider.name, "!")
|
collider.take_damage(base_damage, attacker_pos)
|
||||||
|
print(name, " hit enemy ", collider.name, " with thrown object (", base_damage, " base damage, defense will reduce)!")
|
||||||
|
|
||||||
# Box breaks (only if destroyable)
|
# Box breaks (only if destroyable)
|
||||||
if is_destroyable:
|
if is_destroyable:
|
||||||
@@ -209,6 +220,8 @@ func _handle_air_collision():
|
|||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
has_dealt_damage = true # Mark as dealt damage - can't damage anything else now
|
||||||
|
|
||||||
# Hit a player! Break locally and sync to others (only if destroyable)
|
# Hit a player! Break locally and sync to others (only if destroyable)
|
||||||
if is_destroyable:
|
if is_destroyable:
|
||||||
# Sync break to OTHER clients via RPC BEFORE breaking locally
|
# Sync break to OTHER clients via RPC BEFORE breaking locally
|
||||||
@@ -220,23 +233,25 @@ func _handle_air_collision():
|
|||||||
|
|
||||||
_break_into_pieces()
|
_break_into_pieces()
|
||||||
|
|
||||||
# Damage and knockback player using RPC
|
# Damage and knockback player using RPC (pots deal less damage than boxes)
|
||||||
|
# Player's take_damage() already handles defense calculation
|
||||||
# Pass the thrower's position for accurate direction
|
# Pass the thrower's position for accurate direction
|
||||||
if collider.has_method("rpc_take_damage"):
|
if collider.has_method("rpc_take_damage"):
|
||||||
var attacker_pos = thrown_by_player.global_position if thrown_by_player and is_instance_valid(thrown_by_player) else global_position
|
var attacker_pos = thrown_by_player.global_position if thrown_by_player and is_instance_valid(thrown_by_player) else global_position
|
||||||
|
var base_damage = 7.0 if object_type == "Pot" else 10.0
|
||||||
var player_peer_id = collider.get_multiplayer_authority()
|
var player_peer_id = collider.get_multiplayer_authority()
|
||||||
if player_peer_id != 0:
|
if player_peer_id != 0:
|
||||||
# If target peer is the same as server (us), call directly
|
# If target peer is the same as server (us), call directly
|
||||||
# rpc_id() might not execute locally when called to same peer
|
# rpc_id() might not execute locally when called to same peer
|
||||||
if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id():
|
if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id():
|
||||||
# Call directly on the same peer
|
# Call directly on the same peer
|
||||||
collider.rpc_take_damage(10.0, attacker_pos)
|
collider.rpc_take_damage(base_damage, attacker_pos)
|
||||||
else:
|
else:
|
||||||
# Send RPC to remote peer
|
# Send RPC to remote peer
|
||||||
collider.rpc_take_damage.rpc_id(player_peer_id, 10.0, attacker_pos)
|
collider.rpc_take_damage.rpc_id(player_peer_id, base_damage, attacker_pos)
|
||||||
else:
|
else:
|
||||||
# Fallback: broadcast if we can't get peer_id
|
# Fallback: broadcast if we can't get peer_id
|
||||||
collider.rpc_take_damage.rpc(10.0, attacker_pos)
|
collider.rpc_take_damage.rpc(base_damage, attacker_pos)
|
||||||
|
|
||||||
print(name, " hit player ", collider.name, "!")
|
print(name, " hit player ", collider.name, "!")
|
||||||
return
|
return
|
||||||
@@ -245,6 +260,8 @@ func _handle_air_collision():
|
|||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
has_dealt_damage = true # Mark as dealt damage - can't damage anything else now
|
||||||
|
|
||||||
# Hit another box! Break both locally (only if destroyable)
|
# Hit another box! Break both locally (only if destroyable)
|
||||||
if is_destroyable:
|
if is_destroyable:
|
||||||
# Sync break to OTHER clients via RPC BEFORE breaking locally
|
# Sync break to OTHER clients via RPC BEFORE breaking locally
|
||||||
@@ -416,6 +433,7 @@ func on_grabbed(by_player):
|
|||||||
|
|
||||||
is_being_held = true
|
is_being_held = true
|
||||||
held_by_player = by_player
|
held_by_player = by_player
|
||||||
|
has_dealt_damage = false # Reset damage flag when picked up
|
||||||
print(name, " grabbed by ", by_player.name)
|
print(name, " grabbed by ", by_player.name)
|
||||||
|
|
||||||
func on_lifted(by_player):
|
func on_lifted(by_player):
|
||||||
@@ -434,6 +452,7 @@ func on_released(by_player):
|
|||||||
position_z = 0.0
|
position_z = 0.0
|
||||||
velocity_z = 0.0
|
velocity_z = 0.0
|
||||||
throw_velocity = Vector2.ZERO
|
throw_velocity = Vector2.ZERO
|
||||||
|
has_dealt_damage = false # Reset damage flag when released
|
||||||
|
|
||||||
# Re-enable collision (in case it was disabled)
|
# Re-enable collision (in case it was disabled)
|
||||||
set_collision_layer_value(2, true)
|
set_collision_layer_value(2, true)
|
||||||
@@ -459,6 +478,7 @@ func on_thrown(by_player, force: Vector2):
|
|||||||
held_by_player = null
|
held_by_player = null
|
||||||
thrown_by_player = by_player # Remember who threw this
|
thrown_by_player = by_player # Remember who threw this
|
||||||
is_frozen = false
|
is_frozen = false
|
||||||
|
has_dealt_damage = false # Reset damage flag - this throw can deal damage to ONE target
|
||||||
|
|
||||||
# Set throw velocity (affected by weight) - increased for longer arc
|
# Set throw velocity (affected by weight) - increased for longer arc
|
||||||
throw_velocity = force / weight
|
throw_velocity = force / weight
|
||||||
@@ -495,7 +515,7 @@ func setup_pot():
|
|||||||
can_be_pushed = true
|
can_be_pushed = true
|
||||||
is_destroyable = true
|
is_destroyable = true
|
||||||
is_liftable = true
|
is_liftable = true
|
||||||
weight = 1.0
|
weight = 0.8 # Pots are very light and easy to throw far!
|
||||||
|
|
||||||
var pot_frames = [1, 2, 3, 20, 21, 22, 58]
|
var pot_frames = [1, 2, 3, 20, 21, 22, 58]
|
||||||
if sprite:
|
if sprite:
|
||||||
@@ -554,7 +574,7 @@ func setup_box():
|
|||||||
can_be_pushed = true
|
can_be_pushed = true
|
||||||
is_destroyable = true
|
is_destroyable = true
|
||||||
is_liftable = true
|
is_liftable = true
|
||||||
weight = 1.0
|
weight = 1.5 # Boxes are heavier than pots
|
||||||
|
|
||||||
var box_frames = [7, 26]
|
var box_frames = [7, 26]
|
||||||
if sprite:
|
if sprite:
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ var equipment_selection_index: int = 0 # Current equipment slot index (0-5: main
|
|||||||
@onready var sfx_food: AudioStreamPlayer2D = $SfxFood
|
@onready var sfx_food: AudioStreamPlayer2D = $SfxFood
|
||||||
@onready var sfx_armour: AudioStreamPlayer2D = $SfxArmour
|
@onready var sfx_armour: AudioStreamPlayer2D = $SfxArmour
|
||||||
|
|
||||||
|
# Weight UI elements (created programmatically)
|
||||||
|
var weight_container: HBoxContainer = null
|
||||||
|
var weight_label: Label = null
|
||||||
|
var weight_progress_bar: ProgressBar = null
|
||||||
|
|
||||||
# Store button/item mappings for selection highlighting
|
# Store button/item mappings for selection highlighting
|
||||||
var inventory_buttons: Dictionary = {} # item -> button
|
var inventory_buttons: Dictionary = {} # item -> button
|
||||||
var equipment_buttons: Dictionary = {} # slot_name -> button
|
var equipment_buttons: Dictionary = {} # slot_name -> button
|
||||||
@@ -75,6 +80,9 @@ func _ready():
|
|||||||
# Create equipment slot buttons (dynamically)
|
# Create equipment slot buttons (dynamically)
|
||||||
_create_equipment_slots()
|
_create_equipment_slots()
|
||||||
|
|
||||||
|
# Create weight progress bar
|
||||||
|
_create_weight_ui()
|
||||||
|
|
||||||
# Setup selection rectangle (already in scene, just configure it)
|
# Setup selection rectangle (already in scene, just configure it)
|
||||||
_setup_selection_rectangle()
|
_setup_selection_rectangle()
|
||||||
|
|
||||||
@@ -185,9 +193,26 @@ func _update_stats():
|
|||||||
str(char_stats.defense) + "\n" + \
|
str(char_stats.defense) + "\n" + \
|
||||||
str(char_stats.move_speed) + "\n" + \
|
str(char_stats.move_speed) + "\n" + \
|
||||||
str(char_stats.attack_speed) + "\n" + \
|
str(char_stats.attack_speed) + "\n" + \
|
||||||
str(char_stats.sight) + "\n" + \
|
str(char_stats.sight)
|
||||||
str(char_stats.spell_amp) + "\n" + \
|
|
||||||
str(char_stats.crit_chance) + "%"
|
# Update weight progress bar
|
||||||
|
if weight_progress_bar and weight_label:
|
||||||
|
var current_weight = char_stats.get_total_weight()
|
||||||
|
var max_weight = char_stats.get_carrying_capacity()
|
||||||
|
weight_progress_bar.max_value = max_weight
|
||||||
|
weight_progress_bar.value = current_weight
|
||||||
|
weight_label.text = "Weight: " + str(int(current_weight)) + "/" + str(int(max_weight))
|
||||||
|
|
||||||
|
# Change color based on weight (green -> yellow -> red)
|
||||||
|
var weight_ratio = current_weight / max_weight
|
||||||
|
var fill_style = StyleBoxFlat.new()
|
||||||
|
if weight_ratio < 0.7:
|
||||||
|
fill_style.bg_color = Color(0.6, 0.8, 0.3) # Green
|
||||||
|
elif weight_ratio < 0.9:
|
||||||
|
fill_style.bg_color = Color(0.9, 0.8, 0.2) # Yellow
|
||||||
|
else:
|
||||||
|
fill_style.bg_color = Color(0.9, 0.3, 0.2) # Red
|
||||||
|
weight_progress_bar.add_theme_stylebox_override("fill", fill_style)
|
||||||
|
|
||||||
func _create_equipment_slots():
|
func _create_equipment_slots():
|
||||||
# Equipment slot order: mainhand, offhand, headgear, armour, boots, accessory
|
# Equipment slot order: mainhand, offhand, headgear, armour, boots, accessory
|
||||||
@@ -244,6 +269,46 @@ func _create_equipment_slots():
|
|||||||
equipment_slots[slot_name] = button
|
equipment_slots[slot_name] = button
|
||||||
equipment_buttons[slot_name] = button
|
equipment_buttons[slot_name] = button
|
||||||
|
|
||||||
|
func _create_weight_ui():
|
||||||
|
# Create weight display (label + progress bar)
|
||||||
|
if not stats_panel:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create container for weight UI
|
||||||
|
weight_container = HBoxContainer.new()
|
||||||
|
weight_container.name = "WeightContainer"
|
||||||
|
weight_container.add_theme_constant_override("separation", 4)
|
||||||
|
|
||||||
|
# Create label
|
||||||
|
weight_label = Label.new()
|
||||||
|
weight_label.text = "Weight:"
|
||||||
|
weight_label.add_theme_font_size_override("font_size", 10)
|
||||||
|
if ResourceLoader.exists("res://assets/fonts/standard_font.png"):
|
||||||
|
var font_resource = load("res://assets/fonts/standard_font.png")
|
||||||
|
if font_resource:
|
||||||
|
weight_label.add_theme_font_override("font", font_resource)
|
||||||
|
weight_container.add_child(weight_label)
|
||||||
|
|
||||||
|
# Create progress bar
|
||||||
|
weight_progress_bar = ProgressBar.new()
|
||||||
|
weight_progress_bar.custom_minimum_size = Vector2(100, 12)
|
||||||
|
weight_progress_bar.show_percentage = false
|
||||||
|
# Style the progress bar
|
||||||
|
var progress_style = StyleBoxFlat.new()
|
||||||
|
progress_style.bg_color = Color(0.2, 0.2, 0.2, 0.8)
|
||||||
|
progress_style.border_color = Color(0.4, 0.4, 0.4)
|
||||||
|
progress_style.set_border_width_all(1)
|
||||||
|
weight_progress_bar.add_theme_stylebox_override("background", progress_style)
|
||||||
|
|
||||||
|
var fill_style = StyleBoxFlat.new()
|
||||||
|
fill_style.bg_color = Color(0.6, 0.8, 0.3) # Green color
|
||||||
|
weight_progress_bar.add_theme_stylebox_override("fill", fill_style)
|
||||||
|
|
||||||
|
weight_container.add_child(weight_progress_bar)
|
||||||
|
|
||||||
|
# Add to stats panel (after stats labels)
|
||||||
|
stats_panel.add_child(weight_container)
|
||||||
|
|
||||||
func _has_equipment_in_slot(slot_name: String) -> bool:
|
func _has_equipment_in_slot(slot_name: String) -> bool:
|
||||||
# Check if there's an item equipped in this slot
|
# Check if there's an item equipped in this slot
|
||||||
if not local_player or not local_player.character_stats:
|
if not local_player or not local_player.character_stats:
|
||||||
@@ -453,14 +518,20 @@ func _update_ui():
|
|||||||
if equipped_item.can_have_multiple_of and equipped_item.quantity > 1:
|
if equipped_item.can_have_multiple_of and equipped_item.quantity > 1:
|
||||||
var quantity_label = Label.new()
|
var quantity_label = Label.new()
|
||||||
quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
quantity_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
quantity_label.size = Vector2(24, 24)
|
quantity_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||||
quantity_label.custom_minimum_size = Vector2(0, 0)
|
quantity_label.size = Vector2(36, 36)
|
||||||
quantity_label.position = Vector2(10, 2)
|
quantity_label.custom_minimum_size = Vector2(36, 36)
|
||||||
|
quantity_label.position = Vector2(0, 0)
|
||||||
quantity_label.text = str(equipped_item.quantity)
|
quantity_label.text = str(equipped_item.quantity)
|
||||||
if quantity_font:
|
# Use dmg_numbers.png font (same as inventory items)
|
||||||
quantity_label.add_theme_font_override("font", quantity_font)
|
var dmg_font_resource = load("res://assets/fonts/dmg_numbers.png")
|
||||||
quantity_label.add_theme_font_size_override("font_size", 8)
|
if dmg_font_resource:
|
||||||
quantity_label.scale = Vector2(0.5, 0.5)
|
var font_file = FontFile.new()
|
||||||
|
font_file.font_data = dmg_font_resource
|
||||||
|
quantity_label.add_theme_font_override("font", font_file)
|
||||||
|
quantity_label.add_theme_font_size_override("font_size", 16)
|
||||||
|
quantity_label.z_index = 100 # High z-index to show above item sprite
|
||||||
|
quantity_label.z_as_relative = false # Absolute z-index
|
||||||
button.add_child(quantity_label)
|
button.add_child(quantity_label)
|
||||||
|
|
||||||
# Update inventory grid - clear existing HBoxContainers
|
# Update inventory grid - clear existing HBoxContainers
|
||||||
@@ -727,6 +798,13 @@ func _format_item_info(item: Item) -> String:
|
|||||||
text += ", ".join(stat_lines)
|
text += ", ".join(stat_lines)
|
||||||
text += "\n\n"
|
text += "\n\n"
|
||||||
|
|
||||||
|
# Weight
|
||||||
|
var item_weight = item.weight
|
||||||
|
if item.can_have_multiple_of:
|
||||||
|
item_weight *= item.quantity
|
||||||
|
text += "Weight: %.1f" % item_weight
|
||||||
|
text += "\n\n"
|
||||||
|
|
||||||
# Controls
|
# Controls
|
||||||
if item.item_type == Item.ItemType.Equippable:
|
if item.item_type == Item.ItemType.Equippable:
|
||||||
if selected_type == "equipment":
|
if selected_type == "equipment":
|
||||||
@@ -903,6 +981,7 @@ func _on_inventory_item_pressed(item: Item):
|
|||||||
|
|
||||||
_update_selection_highlight()
|
_update_selection_highlight()
|
||||||
_update_selection_rectangle()
|
_update_selection_rectangle()
|
||||||
|
_update_info_panel() # Show item description on single-click
|
||||||
|
|
||||||
func _on_inventory_item_gui_input(event: InputEvent, item: Item):
|
func _on_inventory_item_gui_input(event: InputEvent, item: Item):
|
||||||
# Handle double-click to equip/consume and right-click to drop
|
# Handle double-click to equip/consume and right-click to drop
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ var weapon_type: WeaponType = WeaponType.NONE
|
|||||||
var two_handed:bool = false
|
var two_handed:bool = false
|
||||||
var quantity = 1
|
var quantity = 1
|
||||||
var can_have_multiple_of:bool = false
|
var can_have_multiple_of:bool = false
|
||||||
var weight: float = 1.0 # Item weight for encumbrance system
|
var weight: float = 2.0 # Item weight for encumbrance system (default 2.0 for equipment)
|
||||||
|
|
||||||
func save():
|
func save():
|
||||||
var json = {
|
var json = {
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ static func _load_all_items():
|
|||||||
"modifiers": {"def": 3},
|
"modifiers": {"def": 3},
|
||||||
"buy_cost": 80,
|
"buy_cost": 80,
|
||||||
"sell_worth": 24,
|
"sell_worth": 24,
|
||||||
"rarity": ItemRarity.COMMON
|
"rarity": ItemRarity.COMMON,
|
||||||
|
"weight": 4.0 # Light armour
|
||||||
})
|
})
|
||||||
|
|
||||||
_register_item("plate", {
|
_register_item("plate", {
|
||||||
@@ -90,7 +91,8 @@ static func _load_all_items():
|
|||||||
"modifiers": {"def": 5, "end": 1},
|
"modifiers": {"def": 5, "end": 1},
|
||||||
"buy_cost": 150,
|
"buy_cost": 150,
|
||||||
"sell_worth": 45,
|
"sell_worth": 45,
|
||||||
"rarity": ItemRarity.UNCOMMON
|
"rarity": ItemRarity.UNCOMMON,
|
||||||
|
"weight": 10.0 # Heavy armour!
|
||||||
})
|
})
|
||||||
|
|
||||||
_register_item("full_mail", {
|
_register_item("full_mail", {
|
||||||
@@ -326,7 +328,8 @@ static func _load_all_items():
|
|||||||
"modifiers": {"def": 2},
|
"modifiers": {"def": 2},
|
||||||
"buy_cost": 40,
|
"buy_cost": 40,
|
||||||
"sell_worth": 12,
|
"sell_worth": 12,
|
||||||
"rarity": ItemRarity.COMMON
|
"rarity": ItemRarity.COMMON,
|
||||||
|
"weight": 1.5 # Boots are light
|
||||||
})
|
})
|
||||||
|
|
||||||
_register_item("sturdy_boots", {
|
_register_item("sturdy_boots", {
|
||||||
@@ -450,7 +453,8 @@ static func _load_all_items():
|
|||||||
"modifiers": {"dmg": 5},
|
"modifiers": {"dmg": 5},
|
||||||
"buy_cost": 100,
|
"buy_cost": 100,
|
||||||
"sell_worth": 30,
|
"sell_worth": 30,
|
||||||
"rarity": ItemRarity.COMMON
|
"rarity": ItemRarity.COMMON,
|
||||||
|
"weight": 3.5
|
||||||
})
|
})
|
||||||
|
|
||||||
_register_item("sword_of_blaze", {
|
_register_item("sword_of_blaze", {
|
||||||
@@ -935,7 +939,8 @@ static func _load_all_items():
|
|||||||
"modifiers": {"dmg": 4, "dex": 2},
|
"modifiers": {"dmg": 4, "dex": 2},
|
||||||
"buy_cost": 100,
|
"buy_cost": 100,
|
||||||
"sell_worth": 30,
|
"sell_worth": 30,
|
||||||
"rarity": ItemRarity.COMMON
|
"rarity": ItemRarity.COMMON,
|
||||||
|
"weight": 2.5 # Bows are moderate weight
|
||||||
})
|
})
|
||||||
|
|
||||||
_register_item("dark_bow", {
|
_register_item("dark_bow", {
|
||||||
@@ -998,12 +1003,14 @@ static func _load_all_items():
|
|||||||
"equipment_type": Item.EquipmentType.OFFHAND,
|
"equipment_type": Item.EquipmentType.OFFHAND,
|
||||||
"weapon_type": Item.WeaponType.AMMUNITION,
|
"weapon_type": Item.WeaponType.AMMUNITION,
|
||||||
"spriteFrame": 7 * 20 + 11, # 11,7
|
"spriteFrame": 7 * 20 + 11, # 11,7
|
||||||
"quantity": 13,
|
"quantity": 15, # Increased from 13 to 15
|
||||||
"can_have_multiple_of": true,
|
"can_have_multiple_of": true,
|
||||||
"modifiers": {"dmg": 2},
|
"modifiers": {"dmg": 2},
|
||||||
"buy_cost": 20,
|
"buy_cost": 20,
|
||||||
"sell_worth": 6,
|
"sell_worth": 6,
|
||||||
"rarity": ItemRarity.COMMON
|
"rarity": ItemRarity.COMMON,
|
||||||
|
"weight": 0.1, # Very light in inventory (arrows are light!)
|
||||||
|
"drop_chance": 15.0 # Much higher drop chance = drops MUCH more often!
|
||||||
})
|
})
|
||||||
|
|
||||||
# CONSUMABLE FOOD ITEMS (row 7)
|
# CONSUMABLE FOOD ITEMS (row 7)
|
||||||
@@ -1258,22 +1265,37 @@ static func create_item(item_id: String) -> Item:
|
|||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
# Get a random item by rarity
|
# Get a random item by rarity (weighted by item drop_chance field)
|
||||||
static func get_random_item_by_rarity(rarity: ItemRarity) -> Item:
|
static func get_random_item_by_rarity(rarity: ItemRarity) -> Item:
|
||||||
_initialize()
|
_initialize()
|
||||||
|
|
||||||
var candidates = []
|
var candidates = []
|
||||||
|
var weights = []
|
||||||
|
var total_weight = 0.0
|
||||||
|
|
||||||
for item_id in item_definitions.keys():
|
for item_id in item_definitions.keys():
|
||||||
var item_data = item_definitions[item_id]
|
var item_data = item_definitions[item_id]
|
||||||
if item_data.has("rarity") and item_data["rarity"] == rarity:
|
if item_data.has("rarity") and item_data["rarity"] == rarity:
|
||||||
candidates.append(item_id)
|
candidates.append(item_id)
|
||||||
|
# Use drop_chance field if present, otherwise default to 1.0
|
||||||
|
var drop_chance = item_data.get("drop_chance", 1.0)
|
||||||
|
weights.append(drop_chance)
|
||||||
|
total_weight += drop_chance
|
||||||
|
|
||||||
if candidates.is_empty():
|
if candidates.is_empty():
|
||||||
# Fallback to common items
|
# Fallback to common items
|
||||||
return get_random_item_by_rarity(ItemRarity.COMMON)
|
return get_random_item_by_rarity(ItemRarity.COMMON)
|
||||||
|
|
||||||
var random_item_id = candidates[randi() % candidates.size()]
|
# Weighted random selection based on drop_chance
|
||||||
return create_item(random_item_id)
|
var roll = randf() * total_weight
|
||||||
|
var cumulative = 0.0
|
||||||
|
for i in range(candidates.size()):
|
||||||
|
cumulative += weights[i]
|
||||||
|
if roll <= cumulative:
|
||||||
|
return create_item(candidates[i])
|
||||||
|
|
||||||
|
# Fallback (shouldn't reach here)
|
||||||
|
return create_item(candidates[candidates.size() - 1])
|
||||||
|
|
||||||
# Get a random item (weighted by rarity)
|
# Get a random item (weighted by rarity)
|
||||||
static func get_random_item() -> Item:
|
static func get_random_item() -> Item:
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ var item: Item = null # Item instance (for LootType.ITEM)
|
|||||||
@onready var sfx_banana_collect = $SfxBananaCollect
|
@onready var sfx_banana_collect = $SfxBananaCollect
|
||||||
@onready var sfx_key_collect = $SfxKeyCollect
|
@onready var sfx_key_collect = $SfxKeyCollect
|
||||||
|
|
||||||
|
# Quantity badge for items with quantity > 1
|
||||||
|
var quantity_badge: Label = null
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
add_to_group("loot")
|
add_to_group("loot")
|
||||||
|
|
||||||
@@ -150,6 +153,10 @@ func _setup_sprite():
|
|||||||
sprite.vframes = item.spriteFrames.y if item.spriteFrames.y > 0 else 14
|
sprite.vframes = item.spriteFrames.y if item.spriteFrames.y > 0 else 14
|
||||||
sprite.frame = item.spriteFrame
|
sprite.frame = item.spriteFrame
|
||||||
print("Loot: Set up item sprite for ", item.item_name, " frame=", sprite.frame)
|
print("Loot: Set up item sprite for ", item.item_name, " frame=", sprite.frame)
|
||||||
|
|
||||||
|
# Add quantity badge if quantity > 1
|
||||||
|
if item.quantity > 1:
|
||||||
|
_create_quantity_badge(item.quantity)
|
||||||
else:
|
else:
|
||||||
print("Loot: ERROR - Could not load texture from spritePath: ", item.spritePath)
|
print("Loot: ERROR - Could not load texture from spritePath: ", item.spritePath)
|
||||||
else:
|
else:
|
||||||
@@ -179,6 +186,18 @@ func _setup_collision_shape():
|
|||||||
|
|
||||||
collision_shape.shape = circle_shape
|
collision_shape.shape = circle_shape
|
||||||
|
|
||||||
|
func _create_quantity_badge(quantity: int):
|
||||||
|
# Create a label to show the quantity
|
||||||
|
quantity_badge = Label.new()
|
||||||
|
quantity_badge.text = str(quantity)
|
||||||
|
quantity_badge.add_theme_font_size_override("font_size", 8)
|
||||||
|
quantity_badge.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
quantity_badge.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
quantity_badge.add_theme_constant_override("outline_size", 2)
|
||||||
|
quantity_badge.z_index = 100 # Above the sprite
|
||||||
|
quantity_badge.position = Vector2(6, -8) # Bottom right of sprite
|
||||||
|
add_child(quantity_badge)
|
||||||
|
|
||||||
func _physics_process(delta):
|
func _physics_process(delta):
|
||||||
# Stop all physics processing if collected
|
# Stop all physics processing if collected
|
||||||
if collected:
|
if collected:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ var appearance_rng: RandomNumberGenerator # Deterministic RNG for appearance/sta
|
|||||||
|
|
||||||
@export var move_speed: float = 80.0
|
@export var move_speed: float = 80.0
|
||||||
@export var grab_range: float = 20.0
|
@export var grab_range: float = 20.0
|
||||||
@export var throw_force: float = 150.0
|
@export var base_throw_force: float = 80.0 # Base throw force (reduced from 150), scales with STR
|
||||||
@export var cone_light_angle: float = 180.0 # Cone spread angle in degrees (adjustable, default wider)
|
@export var cone_light_angle: float = 180.0 # Cone spread angle in degrees (adjustable, default wider)
|
||||||
|
|
||||||
# Network identity
|
# Network identity
|
||||||
@@ -25,6 +25,7 @@ var input_device: int = -1 # -1 for keyboard, 0+ for gamepad index
|
|||||||
var virtual_joystick_input: Vector2 = Vector2.ZERO # Virtual joystick input from mobile controls
|
var virtual_joystick_input: Vector2 = Vector2.ZERO # Virtual joystick input from mobile controls
|
||||||
var was_mouse_right_pressed: bool = false # Track previous mouse right button state
|
var was_mouse_right_pressed: bool = false # Track previous mouse right button state
|
||||||
var was_mouse_left_pressed: bool = false # Track previous mouse left button state
|
var was_mouse_left_pressed: bool = false # Track previous mouse left button state
|
||||||
|
var mouse_control_active: bool = false # True when mouse is controlling facing direction
|
||||||
|
|
||||||
# Interaction
|
# Interaction
|
||||||
var held_object = null
|
var held_object = null
|
||||||
@@ -63,6 +64,9 @@ var knockback_duration: float = 0.3 # How long knockback lasts
|
|||||||
var can_attack: bool = true
|
var can_attack: bool = true
|
||||||
var attack_cooldown: float = 0.0 # No cooldown - instant attacks!
|
var attack_cooldown: float = 0.0 # No cooldown - instant attacks!
|
||||||
var is_attacking: bool = false
|
var is_attacking: bool = false
|
||||||
|
var is_charging_bow: bool = false # True when holding attack with bow+arrows
|
||||||
|
var bow_charge_start_time: float = 0.0
|
||||||
|
var bow_charge_percentage: float = 1.0 # 0.5, 0.75, or 1.0 based on charge time
|
||||||
var sword_slash_scene = preload("res://scenes/sword_slash.tscn") # Old rotating version (kept for reference)
|
var sword_slash_scene = preload("res://scenes/sword_slash.tscn") # Old rotating version (kept for reference)
|
||||||
var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn") # New projectile version
|
var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn") # New projectile version
|
||||||
var staff_projectile_scene = preload("res://scenes/attack_staff.tscn") # Staff magic ball projectile
|
var staff_projectile_scene = preload("res://scenes/attack_staff.tscn") # Staff magic ball projectile
|
||||||
@@ -167,6 +171,12 @@ const ANIMATIONS = {
|
|||||||
"loop": false,
|
"loop": false,
|
||||||
"nextAnimation": "IDLE"
|
"nextAnimation": "IDLE"
|
||||||
},
|
},
|
||||||
|
"BOW_STRING": {
|
||||||
|
"frames": [9],
|
||||||
|
"frameDurations": [30],
|
||||||
|
"loop": true,
|
||||||
|
"nextAnimation": null,
|
||||||
|
},
|
||||||
"BOW": {
|
"BOW": {
|
||||||
"frames": [9, 10, 11, 12],
|
"frames": [9, 10, 11, 12],
|
||||||
"frameDurations": [80, 110, 110, 80],
|
"frameDurations": [80, 110, 110, 80],
|
||||||
@@ -244,6 +254,7 @@ const ANIMATIONS = {
|
|||||||
var current_animation = "IDLE"
|
var current_animation = "IDLE"
|
||||||
var current_frame = 0
|
var current_frame = 0
|
||||||
var current_direction = Direction.DOWN
|
var current_direction = Direction.DOWN
|
||||||
|
var facing_direction_vector: Vector2 = Vector2.DOWN # Full 360-degree facing direction for attacks
|
||||||
var time_since_last_frame = 0.0
|
var time_since_last_frame = 0.0
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
@@ -253,6 +264,9 @@ func _ready():
|
|||||||
# Set respawn point to starting position
|
# Set respawn point to starting position
|
||||||
respawn_point = global_position
|
respawn_point = global_position
|
||||||
|
|
||||||
|
# Initialize facing direction vector based on current direction
|
||||||
|
facing_direction_vector = Vector2.DOWN
|
||||||
|
|
||||||
# Set up input device based on local player index
|
# Set up input device based on local player index
|
||||||
if is_local_player:
|
if is_local_player:
|
||||||
if local_player_index == 0:
|
if local_player_index == 0:
|
||||||
@@ -268,7 +282,10 @@ func _ready():
|
|||||||
_duplicate_sprite_materials()
|
_duplicate_sprite_materials()
|
||||||
|
|
||||||
# Set up player appearance (randomized based on stats)
|
# Set up player appearance (randomized based on stats)
|
||||||
_setup_player_appearance()
|
# ONLY run this for the authority (owner of this player)
|
||||||
|
# Remote players will receive appearance via _sync_equipment and character_changed signal
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
_setup_player_appearance()
|
||||||
|
|
||||||
# Authority is set by player_manager after adding to scene
|
# Authority is set by player_manager after adding to scene
|
||||||
|
|
||||||
@@ -552,27 +569,31 @@ func _randomize_stats():
|
|||||||
character_stats.baseStats.wis = appearance_rng.randi_range(8, 12)
|
character_stats.baseStats.wis = appearance_rng.randi_range(8, 12)
|
||||||
character_stats.baseStats.cha = appearance_rng.randi_range(8, 12)
|
character_stats.baseStats.cha = appearance_rng.randi_range(8, 12)
|
||||||
character_stats.baseStats.lck = appearance_rng.randi_range(8, 12)
|
character_stats.baseStats.lck = appearance_rng.randi_range(8, 12)
|
||||||
|
character_stats.baseStats.per = appearance_rng.randi_range(8, 12)
|
||||||
|
|
||||||
# Apply race-based stat modifiers
|
# Apply race-based stat modifiers
|
||||||
match character_stats.race:
|
match character_stats.race:
|
||||||
"Dwarf":
|
"Dwarf":
|
||||||
# Dwarf: Higher STR, Medium DEX, lower INT, lower WIS, lower LCK
|
# Dwarf: Higher STR, Medium DEX, lower INT, lower WIS, lower LCK, Medium PER (for disarming)
|
||||||
character_stats.baseStats.str += 3
|
character_stats.baseStats.str += 3
|
||||||
character_stats.baseStats.int -= 2
|
character_stats.baseStats.int -= 2
|
||||||
character_stats.baseStats.wis -= 2
|
character_stats.baseStats.wis -= 2
|
||||||
character_stats.baseStats.lck -= 2
|
character_stats.baseStats.lck -= 2
|
||||||
|
character_stats.baseStats.per += 1
|
||||||
"Elf":
|
"Elf":
|
||||||
# Elf: Medium STR, Higher DEX, lower INT, medium WIS, higher LCK
|
# Elf: Medium STR, Higher DEX, lower INT, medium WIS, higher LCK, Highest PER (trap detection)
|
||||||
character_stats.baseStats.dex += 3
|
character_stats.baseStats.dex += 3
|
||||||
character_stats.baseStats.int -= 2
|
character_stats.baseStats.int -= 2
|
||||||
character_stats.baseStats.lck += 2
|
character_stats.baseStats.lck += 2
|
||||||
|
character_stats.baseStats.per += 4 # Highest perception for trap detection
|
||||||
"Human":
|
"Human":
|
||||||
# Human: Lower STR, lower DEX, higher INT, higher WIS, lower LCK
|
# Human: Lower STR, lower DEX, higher INT, higher WIS, lower LCK, Lower PER
|
||||||
character_stats.baseStats.str -= 2
|
character_stats.baseStats.str -= 2
|
||||||
character_stats.baseStats.dex -= 2
|
character_stats.baseStats.dex -= 2
|
||||||
character_stats.baseStats.int += 3
|
character_stats.baseStats.int += 3
|
||||||
character_stats.baseStats.wis += 3
|
character_stats.baseStats.wis += 3
|
||||||
character_stats.baseStats.lck -= 2
|
character_stats.baseStats.lck -= 2
|
||||||
|
character_stats.baseStats.per -= 1
|
||||||
|
|
||||||
# Stats randomized (verbose logging removed)
|
# Stats randomized (verbose logging removed)
|
||||||
|
|
||||||
@@ -593,6 +614,16 @@ func _setup_player_appearance():
|
|||||||
# Randomize stats AFTER race is set (race affects stat modifiers)
|
# Randomize stats AFTER race is set (race affects stat modifiers)
|
||||||
_randomize_stats()
|
_randomize_stats()
|
||||||
|
|
||||||
|
# Give Elf race starting bow and arrows
|
||||||
|
if selected_race == "Elf":
|
||||||
|
var starting_bow = ItemDatabase.create_item("short_bow")
|
||||||
|
var starting_arrows = ItemDatabase.create_item("arrow")
|
||||||
|
if starting_bow and starting_arrows:
|
||||||
|
starting_arrows.quantity = 3
|
||||||
|
character_stats.equipment["mainhand"] = starting_bow
|
||||||
|
character_stats.equipment["offhand"] = starting_arrows
|
||||||
|
print("Elf player ", name, " spawned with short bow and 3 arrows")
|
||||||
|
|
||||||
# Randomize skin (human only for players)
|
# Randomize skin (human only for players)
|
||||||
# Weighted random: Human1 has highest chance, Human7 has lowest chance
|
# Weighted random: Human1 has highest chance, Human7 has lowest chance
|
||||||
# Weights: Human1=7, Human2=6, Human3=5, Human4=4, Human5=3, Human6=2, Human7=1 (total=28)
|
# Weights: Human1=7, Human2=6, Human3=5, Human4=4, Human5=3, Human6=2, Human7=1 (total=28)
|
||||||
@@ -725,6 +756,12 @@ func _setup_player_appearance():
|
|||||||
# Apply appearance to sprite layers
|
# Apply appearance to sprite layers
|
||||||
_apply_appearance_to_sprites()
|
_apply_appearance_to_sprites()
|
||||||
|
|
||||||
|
# Emit character_changed to trigger equipment/race sync
|
||||||
|
if character_stats:
|
||||||
|
character_stats.character_changed.emit(character_stats)
|
||||||
|
|
||||||
|
print("Player ", name, " appearance set up: race=", character_stats.race)
|
||||||
|
|
||||||
func _apply_appearance_to_sprites():
|
func _apply_appearance_to_sprites():
|
||||||
# Apply character_stats appearance to sprite layers
|
# Apply character_stats appearance to sprite layers
|
||||||
if not character_stats:
|
if not character_stats:
|
||||||
@@ -999,6 +1036,9 @@ func _on_character_changed(_char: CharacterStats):
|
|||||||
equipment_data[slot_name] = null
|
equipment_data[slot_name] = null
|
||||||
_rpc_to_ready_peers("_sync_equipment", [equipment_data])
|
_rpc_to_ready_peers("_sync_equipment", [equipment_data])
|
||||||
|
|
||||||
|
# Sync race and base stats to all clients (for proper display)
|
||||||
|
_rpc_to_ready_peers("_sync_race_and_stats", [character_stats.race, character_stats.baseStats.duplicate()])
|
||||||
|
|
||||||
# Sync equipment and inventory to client (when server adds/removes items for a client player)
|
# 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 ensures joiners see items they pick up and equipment changes
|
||||||
# This must be checked separately from the authority-based sync because on the server,
|
# This must be checked separately from the authority-based sync because on the server,
|
||||||
@@ -1221,8 +1261,15 @@ func _update_animation(delta):
|
|||||||
sprite_addons.frame = frame_index
|
sprite_addons.frame = frame_index
|
||||||
if sprite_headgear:
|
if sprite_headgear:
|
||||||
sprite_headgear.frame = frame_index
|
sprite_headgear.frame = frame_index
|
||||||
|
|
||||||
|
# Update weapon sprite - use BOW_STRING animation if charging bow
|
||||||
if sprite_weapon:
|
if sprite_weapon:
|
||||||
sprite_weapon.frame = frame_index
|
if is_charging_bow:
|
||||||
|
# Show BOW_STRING animation on weapon sprite only
|
||||||
|
var bow_string_frame = current_direction * 35 + ANIMATIONS["BOW_STRING"]["frames"][0]
|
||||||
|
sprite_weapon.frame = bow_string_frame
|
||||||
|
else:
|
||||||
|
sprite_weapon.frame = frame_index
|
||||||
|
|
||||||
func _get_direction_from_vector(vec: Vector2) -> int:
|
func _get_direction_from_vector(vec: Vector2) -> int:
|
||||||
if vec.length() < 0.1:
|
if vec.length() < 0.1:
|
||||||
@@ -1263,6 +1310,13 @@ func _update_facing_from_mouse(mouse_direction: Vector2):
|
|||||||
if is_pushing:
|
if is_pushing:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Mark that mouse control is active (prevents movement keys from overriding attack direction)
|
||||||
|
mouse_control_active = true
|
||||||
|
|
||||||
|
# Store full 360-degree direction for attacks
|
||||||
|
if mouse_direction.length() > 0.1:
|
||||||
|
facing_direction_vector = mouse_direction.normalized()
|
||||||
|
|
||||||
var new_direction = _get_direction_from_vector(mouse_direction) as Direction
|
var new_direction = _get_direction_from_vector(mouse_direction) as Direction
|
||||||
|
|
||||||
# Update direction and cone light rotation if changed
|
# Update direction and cone light rotation if changed
|
||||||
@@ -1702,19 +1756,25 @@ func _handle_input():
|
|||||||
if input_vector.length() > 0.1:
|
if input_vector.length() > 0.1:
|
||||||
last_movement_direction = input_vector.normalized()
|
last_movement_direction = input_vector.normalized()
|
||||||
|
|
||||||
# Update facing direction (except when pushing - locked direction)
|
# Update full 360-degree facing direction for attacks (gamepad/keyboard input)
|
||||||
# Note: Mouse control will override this if mouse is being used
|
# Only update if mouse control is not active (i.e., mouse is outside window or using gamepad)
|
||||||
var new_direction = current_direction
|
if not is_pushing and (not mouse_control_active or input_device != -1):
|
||||||
if not is_pushing:
|
facing_direction_vector = input_vector.normalized()
|
||||||
new_direction = _get_direction_from_vector(input_vector) as Direction
|
|
||||||
else:
|
|
||||||
# Keep locked direction when pushing
|
|
||||||
new_direction = push_direction_locked as Direction
|
|
||||||
|
|
||||||
# Update direction and cone light rotation if changed
|
# Update facing direction for animations (except when pushing - locked direction)
|
||||||
if new_direction != current_direction:
|
# Only update from movement input if mouse control is not active or using gamepad
|
||||||
current_direction = new_direction
|
if not is_pushing and (not mouse_control_active or input_device != -1):
|
||||||
_update_cone_light_rotation()
|
var new_direction = _get_direction_from_vector(input_vector) as Direction
|
||||||
|
|
||||||
|
# Update direction and cone light rotation if changed
|
||||||
|
if new_direction != current_direction:
|
||||||
|
current_direction = new_direction
|
||||||
|
_update_cone_light_rotation()
|
||||||
|
elif is_pushing:
|
||||||
|
# Keep locked direction when pushing
|
||||||
|
if push_direction_locked != current_direction:
|
||||||
|
current_direction = push_direction_locked as Direction
|
||||||
|
_update_cone_light_rotation()
|
||||||
|
|
||||||
# Set animation based on state
|
# Set animation based on state
|
||||||
if is_lifting:
|
if is_lifting:
|
||||||
@@ -1817,6 +1877,36 @@ func _handle_interactions():
|
|||||||
else:
|
else:
|
||||||
grab_just_released = false
|
grab_just_released = false
|
||||||
|
|
||||||
|
# Cancel bow charging if grab is pressed
|
||||||
|
if grab_just_pressed and is_charging_bow:
|
||||||
|
is_charging_bow = false
|
||||||
|
|
||||||
|
# Sync bow charge end to other clients
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
_sync_bow_charge_end.rpc()
|
||||||
|
|
||||||
|
print(name, " cancelled bow charge")
|
||||||
|
|
||||||
|
# Check for trap disarm (Dwarf only)
|
||||||
|
if character_stats and character_stats.race == "Dwarf":
|
||||||
|
var nearby_trap = _get_nearby_disarmable_trap()
|
||||||
|
if nearby_trap:
|
||||||
|
if grab_just_pressed:
|
||||||
|
# Start disarming
|
||||||
|
nearby_trap.disarming_player = self
|
||||||
|
nearby_trap.disarm_progress = 0.0
|
||||||
|
print(name, " (Dwarf) started disarming trap")
|
||||||
|
elif grab_just_released:
|
||||||
|
# Cancel disarm if released early
|
||||||
|
if nearby_trap.disarming_player == self:
|
||||||
|
nearby_trap._cancel_disarm()
|
||||||
|
print(name, " (Dwarf) cancelled disarm")
|
||||||
|
# Don't process regular grab actions if near trap
|
||||||
|
if grab_button_down:
|
||||||
|
# Skip grab handling below
|
||||||
|
just_grabbed_this_frame = false
|
||||||
|
return
|
||||||
|
|
||||||
# Track how long grab button is held
|
# Track how long grab button is held
|
||||||
if grab_button_down:
|
if grab_button_down:
|
||||||
grab_button_pressed_time += get_process_delta_time()
|
grab_button_pressed_time += get_process_delta_time()
|
||||||
@@ -1839,14 +1929,8 @@ func _handle_interactions():
|
|||||||
grab_start_time = Time.get_ticks_msec() / 1000.0
|
grab_start_time = Time.get_ticks_msec() / 1000.0
|
||||||
print("DEBUG: After _try_grab(), held_object=", held_object != null, " is_lifting=", is_lifting, " grab_button_down=", grab_button_down, " just_grabbed_this_frame=", just_grabbed_this_frame)
|
print("DEBUG: After _try_grab(), held_object=", held_object != null, " is_lifting=", is_lifting, " grab_button_down=", grab_button_down, " just_grabbed_this_frame=", just_grabbed_this_frame)
|
||||||
elif is_lifting:
|
elif is_lifting:
|
||||||
# Already lifting - check if moving to throw, or just put down
|
# Already lifting - always place down (throwing is now only via attack button)
|
||||||
var is_moving = velocity.length() > 10.0
|
_place_down_object()
|
||||||
if is_moving:
|
|
||||||
# Moving + tap E = throw
|
|
||||||
_throw_object()
|
|
||||||
else:
|
|
||||||
# Not moving + tap E = put down
|
|
||||||
_place_down_object()
|
|
||||||
|
|
||||||
# Handle grab button release
|
# Handle grab button release
|
||||||
# CRITICAL: Don't process release if:
|
# CRITICAL: Don't process release if:
|
||||||
@@ -1926,27 +2010,108 @@ func _handle_interactions():
|
|||||||
_start_pushing()
|
_start_pushing()
|
||||||
# Lift will only happen on release if it was a quick tap
|
# Lift will only happen on release if it was a quick tap
|
||||||
|
|
||||||
# Handle attack input
|
# Handle attack input with bow charging mechanic
|
||||||
|
var attack_pressed = false
|
||||||
var attack_just_pressed = false
|
var attack_just_pressed = false
|
||||||
|
var attack_just_released = false
|
||||||
|
|
||||||
if input_device == -1:
|
if input_device == -1:
|
||||||
# Keyboard or Mouse
|
# Keyboard or Mouse
|
||||||
var mouse_left_pressed = Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)
|
attack_pressed = Input.is_action_pressed("attack")
|
||||||
attack_just_pressed = Input.is_action_just_pressed("attack") or (mouse_left_pressed and not was_mouse_left_pressed)
|
attack_just_pressed = Input.is_action_just_pressed("attack")
|
||||||
was_mouse_left_pressed = mouse_left_pressed
|
attack_just_released = Input.is_action_just_released("attack")
|
||||||
else:
|
else:
|
||||||
# Gamepad (X button)
|
# Gamepad (X button)
|
||||||
attack_just_pressed = Input.is_joy_button_pressed(input_device, JOY_BUTTON_X)
|
attack_pressed = Input.is_joy_button_pressed(input_device, JOY_BUTTON_X)
|
||||||
|
attack_just_pressed = attack_pressed and not is_attacking and not is_charging_bow
|
||||||
|
# For gamepad, detect release by checking if was pressing last frame
|
||||||
|
attack_just_released = not attack_pressed and is_charging_bow
|
||||||
|
|
||||||
|
# Check if player has bow + arrows equipped
|
||||||
|
var has_bow_and_arrows = false
|
||||||
|
var equipped_weapon = null
|
||||||
|
var equipped_arrows = null
|
||||||
|
if character_stats and character_stats.equipment.has("mainhand") and character_stats.equipment.has("offhand"):
|
||||||
|
equipped_weapon = character_stats.equipment["mainhand"]
|
||||||
|
equipped_arrows = character_stats.equipment["offhand"]
|
||||||
|
if equipped_weapon and equipped_arrows:
|
||||||
|
if equipped_weapon.weapon_type == Item.WeaponType.BOW and equipped_arrows.weapon_type == Item.WeaponType.AMMUNITION and equipped_arrows.quantity > 0:
|
||||||
|
has_bow_and_arrows = true
|
||||||
|
|
||||||
|
# Handle bow charging
|
||||||
|
if has_bow_and_arrows and not is_lifting and not is_pushing:
|
||||||
|
if attack_just_pressed and can_attack and not is_charging_bow:
|
||||||
|
# Start charging bow
|
||||||
|
is_charging_bow = true
|
||||||
|
bow_charge_start_time = Time.get_ticks_msec() / 1000.0
|
||||||
|
|
||||||
|
# Sync bow charge start to other clients
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
_sync_bow_charge_start.rpc()
|
||||||
|
|
||||||
|
print(name, " started charging bow")
|
||||||
|
elif attack_just_released and is_charging_bow:
|
||||||
|
# Calculate charge time
|
||||||
|
var charge_time = (Time.get_ticks_msec() / 1000.0) - bow_charge_start_time
|
||||||
|
|
||||||
|
# Minimum charge time: 0.2 seconds, otherwise cancel
|
||||||
|
if charge_time < 0.2:
|
||||||
|
is_charging_bow = false
|
||||||
|
print(name, " cancelled arrow (released too quickly, need at least 0.2s)")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Smooth curve: charge from 0.2s to 1.0s
|
||||||
|
# Speed scales from 50% to 100% (160 to 320 speed)
|
||||||
|
var charge_progress = clamp((charge_time - 0.2) / 0.8, 0.0, 1.0) # 0.0 at 0.2s, 1.0 at 1.0s
|
||||||
|
bow_charge_percentage = 0.5 + (charge_progress * 0.5) # 0.5 to 1.0
|
||||||
|
|
||||||
|
# Release bow and shoot
|
||||||
|
is_charging_bow = false
|
||||||
|
|
||||||
|
# Sync bow charge end to other clients
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
_sync_bow_charge_end.rpc()
|
||||||
|
|
||||||
if attack_just_pressed and can_attack:
|
|
||||||
if is_lifting:
|
|
||||||
# Attack while lifting -> throw immediately (no movement required)
|
|
||||||
_force_throw_held_object(last_movement_direction)
|
|
||||||
elif not is_pushing:
|
|
||||||
_perform_attack()
|
_perform_attack()
|
||||||
|
print(name, " released bow and shot arrow at ", bow_charge_percentage * 100, "% charge (", charge_time, "s)")
|
||||||
|
else:
|
||||||
|
# Reset charging if conditions changed (no bow/arrows, started lifting/pushing)
|
||||||
|
if is_charging_bow:
|
||||||
|
is_charging_bow = false
|
||||||
|
|
||||||
|
# Sync bow charge end to other clients
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
_sync_bow_charge_end.rpc()
|
||||||
|
|
||||||
|
print(name, " bow charge cancelled (conditions changed)")
|
||||||
|
|
||||||
|
# Normal attack (non-bow or no arrows)
|
||||||
|
if attack_just_pressed and can_attack:
|
||||||
|
if is_lifting:
|
||||||
|
# Attack while lifting -> throw immediately in facing direction
|
||||||
|
_force_throw_held_object(facing_direction_vector)
|
||||||
|
elif not is_pushing:
|
||||||
|
_perform_attack()
|
||||||
|
|
||||||
# Note: just_grabbed_this_frame is reset when we block a release, or stays true if we grabbed this frame
|
# Note: just_grabbed_this_frame is reset when we block a release, or stays true if we grabbed this frame
|
||||||
# This ensures it persists to the next frame to block immediate release
|
# This ensures it persists to the next frame to block immediate release
|
||||||
|
|
||||||
|
func _get_nearby_disarmable_trap() -> Node:
|
||||||
|
# Check for nearby trap that can be disarmed (Dwarf only)
|
||||||
|
var traps = get_tree().get_nodes_in_group("trap")
|
||||||
|
for trap in traps:
|
||||||
|
if not trap or not is_instance_valid(trap):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if trap is detected, not disarmed, and within disarm range
|
||||||
|
if trap.is_detected and not trap.is_disarmed:
|
||||||
|
var distance = global_position.distance_to(trap.global_position)
|
||||||
|
# Check if within disarm area range (approximate - trap's DisarmArea has radius ~17)
|
||||||
|
if distance < 20:
|
||||||
|
return trap
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
func _try_grab():
|
func _try_grab():
|
||||||
if not grab_area:
|
if not grab_area:
|
||||||
return
|
return
|
||||||
@@ -2102,8 +2267,9 @@ func _start_pushing():
|
|||||||
# Snap to one of 8 directions
|
# Snap to one of 8 directions
|
||||||
push_axis = _snap_to_8_directions(initial_direction)
|
push_axis = _snap_to_8_directions(initial_direction)
|
||||||
|
|
||||||
# Lock the facing direction
|
# Lock the facing direction (for both animation and attacks)
|
||||||
push_direction_locked = _get_direction_from_vector(push_axis)
|
push_direction_locked = _get_direction_from_vector(push_axis)
|
||||||
|
facing_direction_vector = push_axis.normalized()
|
||||||
|
|
||||||
# Re-enable collision with walls (layer 7) for pushing, but keep collision with players/objects disabled
|
# Re-enable collision with walls (layer 7) for pushing, but keep collision with players/objects disabled
|
||||||
if _is_box(held_object):
|
if _is_box(held_object):
|
||||||
@@ -2131,6 +2297,51 @@ func _force_drop_held_object():
|
|||||||
# Just release
|
# Just release
|
||||||
_stop_pushing()
|
_stop_pushing()
|
||||||
|
|
||||||
|
func reset_grab_state():
|
||||||
|
# Force reset all grab/lift/push states (used when transitioning levels)
|
||||||
|
if held_object and is_instance_valid(held_object):
|
||||||
|
# Re-enable collision on held object
|
||||||
|
if _is_box(held_object):
|
||||||
|
held_object.set_collision_layer_value(2, true)
|
||||||
|
held_object.set_collision_mask_value(1, true)
|
||||||
|
held_object.set_collision_mask_value(2, true)
|
||||||
|
held_object.set_collision_mask_value(7, true)
|
||||||
|
if "is_frozen" in held_object:
|
||||||
|
held_object.is_frozen = false
|
||||||
|
if "is_being_held" in held_object:
|
||||||
|
held_object.is_being_held = false
|
||||||
|
if "held_by_player" in held_object:
|
||||||
|
held_object.held_by_player = null
|
||||||
|
elif _is_player(held_object):
|
||||||
|
held_object.set_collision_layer_value(1, true)
|
||||||
|
held_object.set_collision_mask_value(1, true)
|
||||||
|
held_object.set_collision_mask_value(7, true)
|
||||||
|
if held_object.has_method("set_being_held"):
|
||||||
|
held_object.set_being_held(false)
|
||||||
|
|
||||||
|
# Stop drag sound if playing
|
||||||
|
if held_object.has_method("stop_drag_sound"):
|
||||||
|
held_object.stop_drag_sound()
|
||||||
|
|
||||||
|
# Clear all state
|
||||||
|
held_object = null
|
||||||
|
grab_offset = Vector2.ZERO
|
||||||
|
grab_distance = 0.0
|
||||||
|
is_lifting = false
|
||||||
|
is_pushing = false
|
||||||
|
push_axis = Vector2.ZERO
|
||||||
|
initial_grab_position = Vector2.ZERO
|
||||||
|
initial_player_position = Vector2.ZERO
|
||||||
|
just_grabbed_this_frame = false
|
||||||
|
grab_start_time = 0.0
|
||||||
|
was_dragging_last_frame = false
|
||||||
|
|
||||||
|
# Reset to idle animation
|
||||||
|
if current_animation == "IDLE_HOLD" or current_animation == "RUN_HOLD" or current_animation == "LIFT" or current_animation == "IDLE_PUSH" or current_animation == "RUN_PUSH":
|
||||||
|
_set_animation("IDLE")
|
||||||
|
|
||||||
|
print("Reset grab state for ", name)
|
||||||
|
|
||||||
func _stop_pushing():
|
func _stop_pushing():
|
||||||
if not held_object:
|
if not held_object:
|
||||||
return
|
return
|
||||||
@@ -2158,10 +2369,12 @@ func _stop_pushing():
|
|||||||
released_obj.set_collision_layer_value(2, true)
|
released_obj.set_collision_layer_value(2, true)
|
||||||
released_obj.set_collision_mask_value(1, true)
|
released_obj.set_collision_mask_value(1, true)
|
||||||
released_obj.set_collision_mask_value(2, true)
|
released_obj.set_collision_mask_value(2, true)
|
||||||
|
released_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
elif _is_player(released_obj):
|
elif _is_player(released_obj):
|
||||||
# Players: back on layer 1
|
# Players: back on layer 1
|
||||||
released_obj.set_collision_layer_value(1, true)
|
released_obj.set_collision_layer_value(1, true)
|
||||||
released_obj.set_collision_mask_value(1, true)
|
released_obj.set_collision_mask_value(1, true)
|
||||||
|
released_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
|
|
||||||
if released_obj is CharacterBody2D and released_obj.has_method("set_being_held"):
|
if released_obj is CharacterBody2D and released_obj.has_method("set_being_held"):
|
||||||
released_obj.set_being_held(false)
|
released_obj.set_being_held(false)
|
||||||
@@ -2183,6 +2396,14 @@ func _stop_pushing():
|
|||||||
initial_player_position = Vector2.ZERO
|
initial_player_position = Vector2.ZERO
|
||||||
print("Stopped pushing")
|
print("Stopped pushing")
|
||||||
|
|
||||||
|
func _get_throw_force() -> float:
|
||||||
|
# Calculate throw force based on player's STR stat
|
||||||
|
# Base: 80, +3 per STR point
|
||||||
|
var str_stat = 10.0 # Default STR
|
||||||
|
if character_stats:
|
||||||
|
str_stat = character_stats.baseStats.str + character_stats.get_pass("str")
|
||||||
|
return base_throw_force + (str_stat * 3.0)
|
||||||
|
|
||||||
func _throw_object():
|
func _throw_object():
|
||||||
if not held_object or not is_lifting:
|
if not held_object or not is_lifting:
|
||||||
return
|
return
|
||||||
@@ -2201,6 +2422,9 @@ func _throw_object():
|
|||||||
_place_down_object()
|
_place_down_object()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Calculate throw force based on STR
|
||||||
|
var throw_force = _get_throw_force()
|
||||||
|
|
||||||
# Position object at player's position before throwing
|
# Position object at player's position before throwing
|
||||||
var throw_start_pos = global_position + throw_direction * 10 # Start slightly in front
|
var throw_start_pos = global_position + throw_direction * 10 # Start slightly in front
|
||||||
|
|
||||||
@@ -2294,6 +2518,9 @@ func _force_throw_held_object(direction: Vector2):
|
|||||||
if throw_direction.length() < 0.1:
|
if throw_direction.length() < 0.1:
|
||||||
throw_direction = Vector2.RIGHT
|
throw_direction = Vector2.RIGHT
|
||||||
|
|
||||||
|
# Calculate throw force based on STR
|
||||||
|
var throw_force = _get_throw_force()
|
||||||
|
|
||||||
# Position object at player's position before throwing
|
# Position object at player's position before throwing
|
||||||
var throw_start_pos = global_position + throw_direction * 10 # Start slightly in front
|
var throw_start_pos = global_position + throw_direction * 10 # Start slightly in front
|
||||||
|
|
||||||
@@ -2333,6 +2560,7 @@ func _force_throw_held_object(direction: Vector2):
|
|||||||
thrown_obj.set_collision_layer_value(2, true)
|
thrown_obj.set_collision_layer_value(2, true)
|
||||||
thrown_obj.set_collision_mask_value(1, true)
|
thrown_obj.set_collision_mask_value(1, true)
|
||||||
thrown_obj.set_collision_mask_value(2, true)
|
thrown_obj.set_collision_mask_value(2, true)
|
||||||
|
thrown_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
elif _is_player(thrown_obj):
|
elif _is_player(thrown_obj):
|
||||||
# Player: set position and physics first
|
# Player: set position and physics first
|
||||||
thrown_obj.global_position = throw_start_pos
|
thrown_obj.global_position = throw_start_pos
|
||||||
@@ -2354,6 +2582,7 @@ func _force_throw_held_object(direction: Vector2):
|
|||||||
if thrown_obj and is_instance_valid(thrown_obj):
|
if thrown_obj and is_instance_valid(thrown_obj):
|
||||||
thrown_obj.set_collision_layer_value(1, true)
|
thrown_obj.set_collision_layer_value(1, true)
|
||||||
thrown_obj.set_collision_mask_value(1, true)
|
thrown_obj.set_collision_mask_value(1, true)
|
||||||
|
thrown_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
|
|
||||||
if thrown_obj.has_method("on_thrown"):
|
if thrown_obj.has_method("on_thrown"):
|
||||||
thrown_obj.on_thrown(self, throw_direction * throw_force)
|
thrown_obj.on_thrown(self, throw_direction * throw_force)
|
||||||
@@ -2373,8 +2602,8 @@ func _place_down_object():
|
|||||||
if not held_object:
|
if not held_object:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Place object in front of player based on last movement direction
|
# Place object in front of player based on facing direction (mouse or movement)
|
||||||
var place_pos = _find_closest_place_pos(last_movement_direction, held_object)
|
var place_pos = _find_closest_place_pos(facing_direction_vector, held_object)
|
||||||
var placed_obj = held_object
|
var placed_obj = held_object
|
||||||
|
|
||||||
print("DEBUG: Attempting to place ", placed_obj.name if placed_obj else "null", " at ", place_pos, " (player at ", global_position, ", distance: ", global_position.distance_to(place_pos), ")")
|
print("DEBUG: Attempting to place ", placed_obj.name if placed_obj else "null", " at ", place_pos, " (player at ", global_position, ", distance: ", global_position.distance_to(place_pos), ")")
|
||||||
@@ -2397,6 +2626,7 @@ func _place_down_object():
|
|||||||
placed_obj.set_collision_layer_value(2, true)
|
placed_obj.set_collision_layer_value(2, true)
|
||||||
placed_obj.set_collision_mask_value(1, true)
|
placed_obj.set_collision_mask_value(1, true)
|
||||||
placed_obj.set_collision_mask_value(2, true)
|
placed_obj.set_collision_mask_value(2, true)
|
||||||
|
placed_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
|
|
||||||
# Stop movement and reset all state
|
# Stop movement and reset all state
|
||||||
if "throw_velocity" in placed_obj:
|
if "throw_velocity" in placed_obj:
|
||||||
@@ -2417,6 +2647,7 @@ func _place_down_object():
|
|||||||
# Player: back on layer 1
|
# Player: back on layer 1
|
||||||
placed_obj.set_collision_layer_value(1, true)
|
placed_obj.set_collision_layer_value(1, true)
|
||||||
placed_obj.set_collision_mask_value(1, true)
|
placed_obj.set_collision_mask_value(1, true)
|
||||||
|
placed_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
placed_obj.global_position = place_pos
|
placed_obj.global_position = place_pos
|
||||||
placed_obj.velocity = Vector2.ZERO
|
placed_obj.velocity = Vector2.ZERO
|
||||||
if placed_obj.has_method("set_being_held"):
|
if placed_obj.has_method("set_being_held"):
|
||||||
@@ -2460,25 +2691,8 @@ func _perform_attack():
|
|||||||
else:
|
else:
|
||||||
_set_animation("SWORD")
|
_set_animation("SWORD")
|
||||||
|
|
||||||
# Calculate attack direction based on player's facing direction
|
# Use full 360-degree facing direction for attack
|
||||||
var attack_direction = Vector2.ZERO
|
var attack_direction = facing_direction_vector.normalized()
|
||||||
match current_direction:
|
|
||||||
Direction.RIGHT:
|
|
||||||
attack_direction = Vector2.RIGHT
|
|
||||||
Direction.DOWN_RIGHT:
|
|
||||||
attack_direction = Vector2(1, 1).normalized()
|
|
||||||
Direction.DOWN:
|
|
||||||
attack_direction = Vector2.DOWN
|
|
||||||
Direction.DOWN_LEFT:
|
|
||||||
attack_direction = Vector2(-1, 1).normalized()
|
|
||||||
Direction.LEFT:
|
|
||||||
attack_direction = Vector2.LEFT
|
|
||||||
Direction.UP_LEFT:
|
|
||||||
attack_direction = Vector2(-1, -1).normalized()
|
|
||||||
Direction.UP:
|
|
||||||
attack_direction = Vector2.UP
|
|
||||||
Direction.UP_RIGHT:
|
|
||||||
attack_direction = Vector2(1, -1).normalized()
|
|
||||||
|
|
||||||
# Delay before spawning projectile
|
# Delay before spawning projectile
|
||||||
await get_tree().create_timer(0.15).timeout
|
await get_tree().create_timer(0.15).timeout
|
||||||
@@ -2519,7 +2733,10 @@ func _perform_attack():
|
|||||||
if attack_arrow_scene:
|
if attack_arrow_scene:
|
||||||
var arrow_projectile = attack_arrow_scene.instantiate()
|
var arrow_projectile = attack_arrow_scene.instantiate()
|
||||||
get_parent().add_child(arrow_projectile)
|
get_parent().add_child(arrow_projectile)
|
||||||
arrow_projectile.shoot(attack_direction, global_position, self)
|
# Spawn arrow 4 pixels in the direction player is looking
|
||||||
|
var arrow_spawn_pos = global_position + (attack_direction * 4.0)
|
||||||
|
# Pass charge percentage to arrow (affects speed)
|
||||||
|
arrow_projectile.shoot(attack_direction, arrow_spawn_pos, self, bow_charge_percentage)
|
||||||
# Play bow shoot sound
|
# Play bow shoot sound
|
||||||
if has_node("SfxBowShoot"):
|
if has_node("SfxBowShoot"):
|
||||||
$SfxBowShoot.play()
|
$SfxBowShoot.play()
|
||||||
@@ -2571,7 +2788,7 @@ func _perform_attack():
|
|||||||
|
|
||||||
# Sync attack over network
|
# Sync attack over network
|
||||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||||
_rpc_to_ready_peers("_sync_attack", [current_direction, attack_direction])
|
_rpc_to_ready_peers("_sync_attack", [current_direction, attack_direction, bow_charge_percentage])
|
||||||
|
|
||||||
# Reset attack cooldown (instant if cooldown is 0)
|
# Reset attack cooldown (instant if cooldown is 0)
|
||||||
if attack_cooldown > 0:
|
if attack_cooldown > 0:
|
||||||
@@ -2823,7 +3040,7 @@ func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, airborne: bo
|
|||||||
shadow.modulate.a = 0.5 - (position_z / 100.0) * 0.3
|
shadow.modulate.a = 0.5 - (position_z / 100.0) * 0.3
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _sync_attack(direction: int, attack_dir: Vector2):
|
func _sync_attack(direction: int, attack_dir: Vector2, charge_percentage: float = 1.0):
|
||||||
# Sync attack to other clients
|
# Sync attack to other clients
|
||||||
# Check if node still exists and is valid before processing
|
# Check if node still exists and is valid before processing
|
||||||
if not is_inside_tree() or not is_instance_valid(self):
|
if not is_inside_tree() or not is_instance_valid(self):
|
||||||
@@ -2881,8 +3098,9 @@ func _sync_attack(direction: int, attack_dir: Vector2):
|
|||||||
if attack_arrow_scene:
|
if attack_arrow_scene:
|
||||||
var arrow_projectile = attack_arrow_scene.instantiate()
|
var arrow_projectile = attack_arrow_scene.instantiate()
|
||||||
get_parent().add_child(arrow_projectile)
|
get_parent().add_child(arrow_projectile)
|
||||||
arrow_projectile.shoot(attack_dir, global_position, self)
|
# Use charge percentage from sync (matches local player's arrow)
|
||||||
print(name, " performed synced bow attack with arrow!")
|
arrow_projectile.shoot(attack_dir, global_position, self, charge_percentage)
|
||||||
|
print(name, " performed synced bow attack with arrow (charge: ", charge_percentage * 100, "%)")
|
||||||
else:
|
else:
|
||||||
# No arrows - just play animation, no projectile (matches host behavior)
|
# No arrows - just play animation, no projectile (matches host behavior)
|
||||||
print(name, " performed synced bow attack without arrows (no projectile)")
|
print(name, " performed synced bow attack without arrows (no projectile)")
|
||||||
@@ -2895,6 +3113,20 @@ func _sync_attack(direction: int, attack_dir: Vector2):
|
|||||||
projectile.global_position = global_position + spawn_offset
|
projectile.global_position = global_position + spawn_offset
|
||||||
print(name, " performed synced attack!")
|
print(name, " performed synced attack!")
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_bow_charge_start():
|
||||||
|
# Sync bow charge start to other clients
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
is_charging_bow = true
|
||||||
|
print(name, " (synced) started charging bow")
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_bow_charge_end():
|
||||||
|
# Sync bow charge end to other clients
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
is_charging_bow = false
|
||||||
|
print(name, " (synced) ended charging bow")
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _sync_throw(obj_name: String, throw_pos: Vector2, force: Vector2, thrower_name: String):
|
func _sync_throw(obj_name: String, throw_pos: Vector2, force: Vector2, thrower_name: String):
|
||||||
# Sync throw to all clients (RPC sender already threw on their side)
|
# Sync throw to all clients (RPC sender already threw on their side)
|
||||||
@@ -3128,6 +3360,7 @@ func _sync_release(obj_name: String):
|
|||||||
obj.set_collision_layer_value(2, true)
|
obj.set_collision_layer_value(2, true)
|
||||||
obj.set_collision_mask_value(1, true)
|
obj.set_collision_mask_value(1, true)
|
||||||
obj.set_collision_mask_value(2, true)
|
obj.set_collision_mask_value(2, true)
|
||||||
|
obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
if "is_frozen" in obj:
|
if "is_frozen" in obj:
|
||||||
obj.is_frozen = false
|
obj.is_frozen = false
|
||||||
# CRITICAL: Clear is_being_held so object can be grabbed again and switches can detect it
|
# CRITICAL: Clear is_being_held so object can be grabbed again and switches can detect it
|
||||||
@@ -3138,6 +3371,7 @@ func _sync_release(obj_name: String):
|
|||||||
elif _is_player(obj):
|
elif _is_player(obj):
|
||||||
obj.set_collision_layer_value(1, true)
|
obj.set_collision_layer_value(1, true)
|
||||||
obj.set_collision_mask_value(1, true)
|
obj.set_collision_mask_value(1, true)
|
||||||
|
obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
if obj.has_method("set_being_held"):
|
if obj.has_method("set_being_held"):
|
||||||
obj.set_being_held(false)
|
obj.set_being_held(false)
|
||||||
|
|
||||||
@@ -3177,6 +3411,7 @@ func _sync_place_down(obj_name: String, place_pos: Vector2):
|
|||||||
obj.set_collision_layer_value(2, true)
|
obj.set_collision_layer_value(2, true)
|
||||||
obj.set_collision_mask_value(1, true)
|
obj.set_collision_mask_value(1, true)
|
||||||
obj.set_collision_mask_value(2, true)
|
obj.set_collision_mask_value(2, true)
|
||||||
|
obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
|
|
||||||
# Reset all state
|
# Reset all state
|
||||||
if "throw_velocity" in obj:
|
if "throw_velocity" in obj:
|
||||||
@@ -3196,6 +3431,7 @@ func _sync_place_down(obj_name: String, place_pos: Vector2):
|
|||||||
elif _is_player(obj):
|
elif _is_player(obj):
|
||||||
obj.set_collision_layer_value(1, true)
|
obj.set_collision_layer_value(1, true)
|
||||||
obj.set_collision_mask_value(1, true)
|
obj.set_collision_mask_value(1, true)
|
||||||
|
obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
obj.velocity = Vector2.ZERO
|
obj.velocity = Vector2.ZERO
|
||||||
if obj.has_method("set_being_held"):
|
if obj.has_method("set_being_held"):
|
||||||
obj.set_being_held(false)
|
obj.set_being_held(false)
|
||||||
@@ -3344,6 +3580,7 @@ func _force_place_down(direction: Vector2):
|
|||||||
placed_obj.set_collision_layer_value(2, true)
|
placed_obj.set_collision_layer_value(2, true)
|
||||||
placed_obj.set_collision_mask_value(1, true)
|
placed_obj.set_collision_mask_value(1, true)
|
||||||
placed_obj.set_collision_mask_value(2, true)
|
placed_obj.set_collision_mask_value(2, true)
|
||||||
|
placed_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
|
|
||||||
if "throw_velocity" in placed_obj:
|
if "throw_velocity" in placed_obj:
|
||||||
placed_obj.throw_velocity = Vector2.ZERO
|
placed_obj.throw_velocity = Vector2.ZERO
|
||||||
@@ -3362,6 +3599,7 @@ func _force_place_down(direction: Vector2):
|
|||||||
elif _is_player(placed_obj):
|
elif _is_player(placed_obj):
|
||||||
placed_obj.set_collision_layer_value(1, true)
|
placed_obj.set_collision_layer_value(1, true)
|
||||||
placed_obj.set_collision_mask_value(1, true)
|
placed_obj.set_collision_mask_value(1, true)
|
||||||
|
placed_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
placed_obj.velocity = Vector2.ZERO
|
placed_obj.velocity = Vector2.ZERO
|
||||||
if placed_obj.has_method("set_being_held"):
|
if placed_obj.has_method("set_being_held"):
|
||||||
placed_obj.set_being_held(false)
|
placed_obj.set_being_held(false)
|
||||||
@@ -3398,6 +3636,14 @@ func take_damage(amount: float, attacker_position: Vector2):
|
|||||||
if is_dead:
|
if is_dead:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Cancel bow charging when taking damage
|
||||||
|
if is_charging_bow:
|
||||||
|
is_charging_bow = false
|
||||||
|
|
||||||
|
# Sync bow charge end to other clients
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
_sync_bow_charge_end.rpc()
|
||||||
|
|
||||||
# Check for dodge chance (based on DEX)
|
# Check for dodge chance (based on DEX)
|
||||||
var _was_dodged = false
|
var _was_dodged = false
|
||||||
if character_stats:
|
if character_stats:
|
||||||
@@ -3458,7 +3704,9 @@ func take_damage(amount: float, attacker_position: Vector2):
|
|||||||
velocity = direction_from_attacker * 250.0 # Scaled down for 1x scale
|
velocity = direction_from_attacker * 250.0 # Scaled down for 1x scale
|
||||||
|
|
||||||
# Face the attacker (opposite of knockback direction)
|
# Face the attacker (opposite of knockback direction)
|
||||||
current_direction = _get_direction_from_vector(-direction_from_attacker) as Direction
|
var face_direction = -direction_from_attacker
|
||||||
|
current_direction = _get_direction_from_vector(face_direction) as Direction
|
||||||
|
facing_direction_vector = face_direction.normalized()
|
||||||
|
|
||||||
# Enable knockback state (prevents player control for a short time)
|
# Enable knockback state (prevents player control for a short time)
|
||||||
is_knocked_back = true
|
is_knocked_back = true
|
||||||
@@ -3514,6 +3762,7 @@ func _die():
|
|||||||
released_obj.set_collision_layer_value(2, true)
|
released_obj.set_collision_layer_value(2, true)
|
||||||
released_obj.set_collision_mask_value(1, true)
|
released_obj.set_collision_mask_value(1, true)
|
||||||
released_obj.set_collision_mask_value(2, true)
|
released_obj.set_collision_mask_value(2, true)
|
||||||
|
released_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
if "is_being_held" in released_obj:
|
if "is_being_held" in released_obj:
|
||||||
released_obj.is_being_held = false
|
released_obj.is_being_held = false
|
||||||
if "held_by_player" in released_obj:
|
if "held_by_player" in released_obj:
|
||||||
@@ -3521,6 +3770,7 @@ func _die():
|
|||||||
elif _is_player(released_obj):
|
elif _is_player(released_obj):
|
||||||
released_obj.set_collision_layer_value(1, true)
|
released_obj.set_collision_layer_value(1, true)
|
||||||
released_obj.set_collision_mask_value(1, true)
|
released_obj.set_collision_mask_value(1, true)
|
||||||
|
released_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
if released_obj.has_method("set_being_held"):
|
if released_obj.has_method("set_being_held"):
|
||||||
released_obj.set_being_held(false)
|
released_obj.set_being_held(false)
|
||||||
|
|
||||||
@@ -3593,6 +3843,7 @@ func _die():
|
|||||||
# Re-enable our collision
|
# Re-enable our collision
|
||||||
set_collision_layer_value(1, true)
|
set_collision_layer_value(1, true)
|
||||||
set_collision_mask_value(1, true)
|
set_collision_mask_value(1, true)
|
||||||
|
set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
|
|
||||||
# THEN sync to other clients
|
# THEN sync to other clients
|
||||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
@@ -3621,6 +3872,7 @@ func _respawn():
|
|||||||
# Re-enable collision in case it was disabled while being carried
|
# Re-enable collision in case it was disabled while being carried
|
||||||
set_collision_layer_value(1, true)
|
set_collision_layer_value(1, true)
|
||||||
set_collision_mask_value(1, true)
|
set_collision_mask_value(1, true)
|
||||||
|
set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
|
|
||||||
# Reset health and state
|
# Reset health and state
|
||||||
if character_stats:
|
if character_stats:
|
||||||
@@ -3748,6 +4000,7 @@ func _force_holder_to_drop_local(holder_name: String):
|
|||||||
# Re-enable collision on dropped player
|
# Re-enable collision on dropped player
|
||||||
set_collision_layer_value(1, true)
|
set_collision_layer_value(1, true)
|
||||||
set_collision_mask_value(1, true)
|
set_collision_mask_value(1, true)
|
||||||
|
set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
else:
|
else:
|
||||||
print(" ✗ held_object doesn't match self")
|
print(" ✗ held_object doesn't match self")
|
||||||
else:
|
else:
|
||||||
@@ -3766,6 +4019,7 @@ func _sync_respawn(spawn_pos: Vector2):
|
|||||||
# Re-enable collision in case it was disabled while being carried
|
# Re-enable collision in case it was disabled while being carried
|
||||||
set_collision_layer_value(1, true)
|
set_collision_layer_value(1, true)
|
||||||
set_collision_mask_value(1, true)
|
set_collision_mask_value(1, true)
|
||||||
|
set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
|
|
||||||
# Just teleport and reset on clients (AFTER release is processed)
|
# Just teleport and reset on clients (AFTER release is processed)
|
||||||
global_position = spawn_pos
|
global_position = spawn_pos
|
||||||
@@ -3814,6 +4068,14 @@ func _sync_stats_update(kills_count: int, coins_count: int):
|
|||||||
character_stats.coin = coins_count
|
character_stats.coin = coins_count
|
||||||
print(name, " stats synced from server: kills=", kills_count, " coins=", coins_count)
|
print(name, " stats synced from server: kills=", kills_count, " coins=", coins_count)
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_race_and_stats(race: String, base_stats: Dictionary):
|
||||||
|
# Client receives race and base stats from authority player
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
character_stats.race = race
|
||||||
|
character_stats.baseStats = base_stats
|
||||||
|
print(name, " race and stats synced: race=", race, " STR=", base_stats.str, " PER=", base_stats.per)
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _sync_equipment(equipment_data: Dictionary):
|
func _sync_equipment(equipment_data: Dictionary):
|
||||||
# Client receives equipment update from server or other clients
|
# Client receives equipment update from server or other clients
|
||||||
@@ -3822,6 +4084,12 @@ func _sync_equipment(equipment_data: Dictionary):
|
|||||||
if not character_stats:
|
if not character_stats:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# CRITICAL: Don't accept equipment syncs for our own player
|
||||||
|
# Each client manages their own equipment locally
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
print(name, " ignoring equipment sync (I'm the authority)")
|
||||||
|
return
|
||||||
|
|
||||||
# On server, only accept if this is a client player (not server's own player)
|
# On server, only accept if this is a client player (not server's own player)
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
var the_peer_id = get_multiplayer_authority()
|
var the_peer_id = get_multiplayer_authority()
|
||||||
@@ -3845,15 +4113,19 @@ func _sync_equipment(equipment_data: Dictionary):
|
|||||||
func _sync_inventory(inventory_data: Array):
|
func _sync_inventory(inventory_data: Array):
|
||||||
# Client receives inventory update from server
|
# Client receives inventory update from server
|
||||||
# Update inventory to match server's inventory
|
# Update inventory to match server's inventory
|
||||||
# Unlike _sync_equipment, we WANT to receive our own inventory from the server
|
# CRITICAL: Don't accept inventory syncs for our own player
|
||||||
# So we check if we're the server (sender) and ignore, not if we're the authority
|
# Each client manages their own inventory locally (same as equipment)
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
print(name, " ignoring inventory sync (I'm the authority)")
|
||||||
|
return
|
||||||
|
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
return # Server ignores this (it's the sender)
|
return # Server ignores this (it's the sender)
|
||||||
|
|
||||||
if not character_stats:
|
if not character_stats:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Clear and rebuild inventory from server data
|
# Clear and rebuild inventory from server data (only for OTHER players we're viewing)
|
||||||
character_stats.inventory.clear()
|
character_stats.inventory.clear()
|
||||||
for item_data in inventory_data:
|
for item_data in inventory_data:
|
||||||
if item_data != null:
|
if item_data != null:
|
||||||
@@ -4039,7 +4311,9 @@ func _sync_damage(_amount: float, attacker_position: Vector2, is_crit: bool = fa
|
|||||||
velocity = direction_from_attacker * 250.0
|
velocity = direction_from_attacker * 250.0
|
||||||
|
|
||||||
# Face the attacker
|
# Face the attacker
|
||||||
current_direction = _get_direction_from_vector(-direction_from_attacker) as Direction
|
var face_direction = -direction_from_attacker
|
||||||
|
current_direction = _get_direction_from_vector(face_direction) as Direction
|
||||||
|
facing_direction_vector = face_direction.normalized()
|
||||||
|
|
||||||
# Enable knockback state
|
# Enable knockback state
|
||||||
is_knocked_back = true
|
is_knocked_back = true
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ func _ready():
|
|||||||
_apply_color_replacements()
|
_apply_color_replacements()
|
||||||
$SfxSwosh.play()
|
$SfxSwosh.play()
|
||||||
$AnimationPlayer.play("flying")
|
$AnimationPlayer.play("flying")
|
||||||
# Connect area signals
|
# Connect area signals (only if not already connected)
|
||||||
if hit_area:
|
if hit_area and not hit_area.body_entered.is_connected(_on_body_entered):
|
||||||
hit_area.body_entered.connect(_on_body_entered)
|
hit_area.body_entered.connect(_on_body_entered)
|
||||||
|
|
||||||
func setup(direction: Vector2, owner_player: Node, damage_value: float = 20.0, staff_item: Item = null):
|
func setup(direction: Vector2, owner_player: Node, damage_value: float = 20.0, staff_item: Item = null):
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup)
|
|||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
$SfxSwosh.play()
|
$SfxSwosh.play()
|
||||||
# Connect area signals
|
# Connect area signals (only if not already connected)
|
||||||
if hit_area:
|
if hit_area and not hit_area.body_entered.is_connected(_on_body_entered):
|
||||||
hit_area.body_entered.connect(_on_body_entered)
|
hit_area.body_entered.connect(_on_body_entered)
|
||||||
|
|
||||||
func setup(direction: Vector2, owner_player: Node, damage_value: float = 20.0):
|
func setup(direction: Vector2, owner_player: Node, damage_value: float = 20.0):
|
||||||
|
|||||||
321
src/scripts/trap.gd
Normal file
321
src/scripts/trap.gd
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
@onready var sprite = $Sprite2D
|
||||||
|
@onready var animation_player = $AnimationPlayer
|
||||||
|
@onready var activation_area = $ActivationArea
|
||||||
|
@onready var disarm_area = $DisarmArea
|
||||||
|
@onready var detection_area = $DetectionArea
|
||||||
|
|
||||||
|
# Trap state
|
||||||
|
var is_detected: bool = false # Becomes true when any player detects it
|
||||||
|
var is_disarmed: bool = false # True if trap has been disarmed
|
||||||
|
var is_active: bool = false # True when trap is currently triggering
|
||||||
|
var has_cooldown: bool = false # Some traps can reset
|
||||||
|
var cooldown_time: float = 5.0 # Time until trap can re-activate
|
||||||
|
var cooldown_timer: float = 0.0
|
||||||
|
|
||||||
|
# Trap properties
|
||||||
|
var trap_damage: float = 15.0
|
||||||
|
var trap_type: String = "Floor_Lance"
|
||||||
|
|
||||||
|
# Per-player detection tracking (Dictionary: peer_id -> has_attempted_detection)
|
||||||
|
var player_detection_attempts: Dictionary = {}
|
||||||
|
|
||||||
|
# Disarm tracking
|
||||||
|
var disarming_player: Node = null
|
||||||
|
var disarm_progress: float = 0.0
|
||||||
|
var disarm_duration: float = 1.0
|
||||||
|
var disarm_label: Label = null
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
# Add to trap group for detection by players
|
||||||
|
add_to_group("trap")
|
||||||
|
|
||||||
|
# Randomize trap visual based on dungeon seed
|
||||||
|
var highbox_seed = 0
|
||||||
|
var world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if world and "dungeon_seed" in world:
|
||||||
|
highbox_seed = world.dungeon_seed
|
||||||
|
highbox_seed += int(global_position.x) * 1000 + int(global_position.y)
|
||||||
|
|
||||||
|
var rng = RandomNumberGenerator.new()
|
||||||
|
rng.seed = highbox_seed
|
||||||
|
var index = rng.randi() % 2
|
||||||
|
if index == 0:
|
||||||
|
sprite.texture = load("res://assets/gfx/traps/Floor_Lance.png")
|
||||||
|
trap_type = "Floor_Lance"
|
||||||
|
has_cooldown = true # Lance traps can reset
|
||||||
|
|
||||||
|
# Start hidden (invisible until detected)
|
||||||
|
sprite.modulate.a = 0.0
|
||||||
|
|
||||||
|
# Setup detection area to check for players
|
||||||
|
if detection_area:
|
||||||
|
detection_area.body_entered.connect(_on_detection_area_body_entered)
|
||||||
|
detection_area.body_exited.connect(_on_detection_area_body_exited)
|
||||||
|
|
||||||
|
# Setup disarm area to show "DISARM" text for dwarves
|
||||||
|
if disarm_area:
|
||||||
|
disarm_area.body_entered.connect(_on_disarm_area_body_entered)
|
||||||
|
disarm_area.body_exited.connect(_on_disarm_area_body_exited)
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
# Handle cooldown timer for resetting traps
|
||||||
|
if has_cooldown and not is_active and cooldown_timer > 0:
|
||||||
|
cooldown_timer -= delta
|
||||||
|
if cooldown_timer <= 0:
|
||||||
|
# Trap has cooled down - ready to trigger again
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Handle disarm progress
|
||||||
|
if disarming_player and is_instance_valid(disarming_player):
|
||||||
|
# Check if player is still holding grab button
|
||||||
|
if disarming_player.is_multiplayer_authority() and Input.is_action_pressed("grab"):
|
||||||
|
# Play disarming sound (only if not already playing)
|
||||||
|
if $SfxDisarming.playing == false:
|
||||||
|
$SfxDisarming.play()
|
||||||
|
|
||||||
|
disarm_progress += delta
|
||||||
|
_update_disarm_ui()
|
||||||
|
|
||||||
|
if disarm_progress >= disarm_duration:
|
||||||
|
# Disarm complete!
|
||||||
|
_complete_disarm()
|
||||||
|
else:
|
||||||
|
# Player released grab - cancel disarm
|
||||||
|
_cancel_disarm()
|
||||||
|
|
||||||
|
func _on_detection_area_body_entered(body: Node) -> void:
|
||||||
|
# When a player enters detection range, roll perception check (once per player per game)
|
||||||
|
if not body.is_in_group("player"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_detected or is_disarmed:
|
||||||
|
return # Already detected or disarmed
|
||||||
|
|
||||||
|
# Get player peer ID
|
||||||
|
var peer_id = body.get_multiplayer_authority()
|
||||||
|
|
||||||
|
# Check if this player has already attempted detection
|
||||||
|
if player_detection_attempts.has(peer_id):
|
||||||
|
return # Already tried once this game
|
||||||
|
|
||||||
|
# Mark that this player has attempted detection
|
||||||
|
player_detection_attempts[peer_id] = true
|
||||||
|
|
||||||
|
# Roll perception check (only on server/authority)
|
||||||
|
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer():
|
||||||
|
_roll_perception_check(body)
|
||||||
|
|
||||||
|
func _on_detection_area_body_exited(_body: Node) -> void:
|
||||||
|
pass # Detection is permanent once attempted
|
||||||
|
|
||||||
|
func _roll_perception_check(player: Node) -> void:
|
||||||
|
# Roll perception check for player
|
||||||
|
if not player or not player.character_stats:
|
||||||
|
return
|
||||||
|
|
||||||
|
var per_stat = player.character_stats.baseStats.per + player.character_stats.get_pass("per")
|
||||||
|
|
||||||
|
# Perception roll: d20 + PER modifier
|
||||||
|
# Target DC: 15 (medium difficulty)
|
||||||
|
var roll = randi() % 20 + 1 # 1d20
|
||||||
|
var total = roll + int(per_stat / 2) - 5 # PER modifier: (PER - 10) / 2
|
||||||
|
var dc = 15
|
||||||
|
|
||||||
|
print(player.name, " rolls Perception: ", roll, " + ", int(per_stat / 2) - 5, " = ", total, " vs DC ", dc)
|
||||||
|
|
||||||
|
if total >= dc:
|
||||||
|
# Success! Player detects the trap
|
||||||
|
_detect_trap(player)
|
||||||
|
else:
|
||||||
|
# Failure - trap remains hidden to this player
|
||||||
|
print(player.name, " failed to detect trap")
|
||||||
|
|
||||||
|
func _detect_trap(detecting_player: Node) -> void:
|
||||||
|
# Trap is detected - make it visible to ALL players
|
||||||
|
is_detected = true
|
||||||
|
|
||||||
|
# Make trap visible
|
||||||
|
sprite.modulate.a = 1.0
|
||||||
|
|
||||||
|
# Sync detection to all clients (including server with call_local)
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
||||||
|
if multiplayer.is_server():
|
||||||
|
_sync_trap_detected.rpc()
|
||||||
|
|
||||||
|
print(detecting_player.name, " detected trap at ", global_position)
|
||||||
|
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func _sync_trap_detected() -> void:
|
||||||
|
# Client receives trap detection notification
|
||||||
|
is_detected = true
|
||||||
|
sprite.modulate.a = 1.0
|
||||||
|
|
||||||
|
func _on_disarm_area_body_entered(body: Node) -> void:
|
||||||
|
# Show "DISARM" text if player is Dwarf and trap is detected
|
||||||
|
if not body.is_in_group("player"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if not is_detected or is_disarmed:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if player is Dwarf
|
||||||
|
if body.character_stats and body.character_stats.race == "Dwarf":
|
||||||
|
_show_disarm_text(body)
|
||||||
|
|
||||||
|
func _on_disarm_area_body_exited(body: Node) -> void:
|
||||||
|
# Hide disarm text when player leaves area
|
||||||
|
if body == disarming_player:
|
||||||
|
_cancel_disarm()
|
||||||
|
_hide_disarm_text(body)
|
||||||
|
|
||||||
|
func _show_disarm_text(_player: Node) -> void:
|
||||||
|
# Create "DISARM" label above trap
|
||||||
|
if disarm_label:
|
||||||
|
return # Already showing
|
||||||
|
|
||||||
|
disarm_label = Label.new()
|
||||||
|
disarm_label.text = "DISARM"
|
||||||
|
disarm_label.add_theme_font_size_override("font_size", 16)
|
||||||
|
disarm_label.add_theme_color_override("font_color", Color.YELLOW)
|
||||||
|
disarm_label.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
disarm_label.add_theme_constant_override("outline_size", 2)
|
||||||
|
disarm_label.position = Vector2(-25, -30)
|
||||||
|
disarm_label.z_index = 100
|
||||||
|
add_child(disarm_label)
|
||||||
|
|
||||||
|
func _hide_disarm_text(_player: Node) -> void:
|
||||||
|
if disarm_label:
|
||||||
|
disarm_label.queue_free()
|
||||||
|
disarm_label = null
|
||||||
|
|
||||||
|
func _update_disarm_ui() -> void:
|
||||||
|
# Update disarm progress (could show radial timer here)
|
||||||
|
if disarm_label:
|
||||||
|
var progress_percent = int((disarm_progress / disarm_duration) * 100)
|
||||||
|
disarm_label.text = "DISARM (" + str(progress_percent) + "%)"
|
||||||
|
|
||||||
|
func _cancel_disarm() -> void:
|
||||||
|
disarming_player = null
|
||||||
|
disarm_progress = 0.0
|
||||||
|
# Stop disarming sound
|
||||||
|
if $SfxDisarming.playing:
|
||||||
|
$SfxDisarming.stop()
|
||||||
|
if disarm_label:
|
||||||
|
disarm_label.text = "DISARM"
|
||||||
|
|
||||||
|
func _complete_disarm() -> void:
|
||||||
|
# Trap successfully disarmed!
|
||||||
|
is_disarmed = true
|
||||||
|
disarming_player = null
|
||||||
|
disarm_progress = 0.0
|
||||||
|
|
||||||
|
# Stop disarming sound
|
||||||
|
if $SfxDisarming.playing:
|
||||||
|
$SfxDisarming.stop()
|
||||||
|
|
||||||
|
# Hide disarm text
|
||||||
|
_hide_disarm_text(null)
|
||||||
|
|
||||||
|
# Disable activation area
|
||||||
|
if activation_area:
|
||||||
|
activation_area.monitoring = false
|
||||||
|
|
||||||
|
# Show "TRAP DISARMED" in chat
|
||||||
|
var chat_ui = get_tree().get_first_node_in_group("chat_ui")
|
||||||
|
if chat_ui and chat_ui.has_method("send_system_message"):
|
||||||
|
chat_ui.send_system_message("Trap disarmed by Dwarf!")
|
||||||
|
|
||||||
|
# Show floating text "TRAP DISARMED"
|
||||||
|
_show_floating_text("TRAP DISARMED", Color.YELLOW)
|
||||||
|
|
||||||
|
# Change trap visual to show it's disarmed (optional - could fade out or change color)
|
||||||
|
sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
|
||||||
|
|
||||||
|
# Sync disarm to all clients
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
||||||
|
if multiplayer.is_server():
|
||||||
|
_sync_trap_disarmed.rpc()
|
||||||
|
|
||||||
|
print("Trap disarmed!")
|
||||||
|
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func _sync_trap_disarmed() -> void:
|
||||||
|
# Client receives trap disarm notification
|
||||||
|
is_disarmed = true
|
||||||
|
sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
|
||||||
|
if activation_area:
|
||||||
|
activation_area.monitoring = false
|
||||||
|
|
||||||
|
func _show_floating_text(text: String, color: Color) -> void:
|
||||||
|
var floating_text_scene = preload("res://scenes/floating_text.tscn")
|
||||||
|
if floating_text_scene:
|
||||||
|
var floating_text = floating_text_scene.instantiate()
|
||||||
|
var parent = get_parent()
|
||||||
|
if parent:
|
||||||
|
parent.add_child(floating_text)
|
||||||
|
floating_text.global_position = Vector2(global_position.x, global_position.y - 20)
|
||||||
|
floating_text.setup(text, color, 0.5, 0.5, null, 1, 1, 0)
|
||||||
|
|
||||||
|
func _on_activation_area_body_shape_entered(_body_rid: RID, body: Node2D, _body_shape_index: int, _local_shape_index: int) -> void:
|
||||||
|
# Trap triggered!
|
||||||
|
if not body.is_in_group("player"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_disarmed or is_active:
|
||||||
|
return # Can't trigger if disarmed or already active
|
||||||
|
|
||||||
|
if has_cooldown and cooldown_timer > 0:
|
||||||
|
return # Still on cooldown
|
||||||
|
|
||||||
|
# Trigger trap
|
||||||
|
is_active = true
|
||||||
|
$SfxActivate.play()
|
||||||
|
animation_player.play("activate")
|
||||||
|
|
||||||
|
# Trap is now visible to all players (once triggered)
|
||||||
|
if not is_detected:
|
||||||
|
is_detected = true
|
||||||
|
sprite.modulate.a = 1.0
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
||||||
|
if multiplayer.is_server():
|
||||||
|
_sync_trap_detected.rpc()
|
||||||
|
|
||||||
|
# Deal damage to player (with luck-based avoidance)
|
||||||
|
_deal_trap_damage(body)
|
||||||
|
|
||||||
|
# Start cooldown if applicable
|
||||||
|
if has_cooldown:
|
||||||
|
cooldown_timer = cooldown_time
|
||||||
|
await animation_player.animation_finished
|
||||||
|
animation_player.play("reset")
|
||||||
|
await animation_player.animation_finished
|
||||||
|
is_active = false
|
||||||
|
else:
|
||||||
|
# One-time trap - stays triggered
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _deal_trap_damage(player: Node) -> void:
|
||||||
|
if not player or not player.character_stats:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Luck-based avoidance check
|
||||||
|
var luck_stat = player.character_stats.baseStats.lck + player.character_stats.get_pass("lck")
|
||||||
|
var avoid_chance = luck_stat * 0.02 # 2% per luck point (e.g., 10 luck = 20% avoid)
|
||||||
|
var avoid_roll = randf()
|
||||||
|
|
||||||
|
if avoid_roll < avoid_chance:
|
||||||
|
$SfxAvoid.play()
|
||||||
|
# Player avoided trap damage!
|
||||||
|
print(player.name, " avoided trap damage! (", avoid_chance * 100, "% chance)")
|
||||||
|
_show_floating_text("AVOIDED", Color.GREEN)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Apply trap damage (affected by player's defense)
|
||||||
|
var final_damage = player.character_stats.calculate_damage(trap_damage, false, false)
|
||||||
|
|
||||||
|
if player.has_method("rpc_take_damage"):
|
||||||
|
player.rpc_take_damage(trap_damage, global_position)
|
||||||
|
|
||||||
|
print(player.name, " took ", final_damage, " trap damage")
|
||||||
1
src/scripts/trap.gd.uid
Normal file
1
src/scripts/trap.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bkp8t4mvdhtqq
|
||||||
Reference in New Issue
Block a user