try to fix some stuff

This commit is contained in:
2026-01-26 07:36:23 +01:00
parent 913e5c4418
commit dabec8a119
39 changed files with 3297 additions and 427 deletions

View File

@@ -12,8 +12,8 @@ dest_files=["res://.godot/imported/Gelhein - Evil.mp3-bc2ead9945dee5ecfc0f8aff80
[params] [params]
loop=false loop=true
loop_offset=0 loop_offset=0.0
bpm=0 bpm=0.0
beat_count=0 beat_count=0
bar_beats=4 bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://rj481ujhyho6"
path="res://.godot/imported/level_complete_03.wav-d1d7aa69f96bcf65cf182c6066013c49.sample"
[deps]
source_file="res://assets/audio/sfx/level_complete_03.wav"
dest_files=["res://.godot/imported/level_complete_03.wav-d1d7aa69f96bcf65cf182c6066013c49.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,24 @@
[remap]
importer="wav"
type="AudioStreamWAV"
uid="uid://csybukiq51h41"
path="res://.godot/imported/level_fail_02.wav-26d001ef22c866c4eb5cb80c7c2151ca.sample"
[deps]
source_file="res://assets/audio/sfx/level_fail_02.wav"
dest_files=["res://.godot/imported/level_fail_02.wav-26d001ef22c866c4eb5cb80c7c2151ca.sample"]
[params]
force/8_bit=false
force/mono=false
force/max_rate=false
force/max_rate_hz=44100
edit/trim=false
edit/normalize=false
edit/loop_mode=0
edit/loop_begin=0
edit/loop_end=-1
compress/mode=2

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://cs5ruoyq80yi4"
path="res://.godot/imported/aha.mp3-90367befe3910bf62cb2a22cf11ad3ce.mp3str"
[deps]
source_file="res://assets/audio/sfx/player/notice/aha.mp3"
dest_files=["res://.godot/imported/aha.mp3-90367befe3910bf62cb2a22cf11ad3ce.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://cbbeyrdor7nyg"
path="res://.godot/imported/lookout.mp3-0a409809ff1b9e10875c4707d9f8764e.mp3str"
[deps]
source_file="res://assets/audio/sfx/player/notice/lookout.mp3"
dest_files=["res://.godot/imported/lookout.mp3-0a409809ff1b9e10875c4707d9f8764e.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

View File

@@ -0,0 +1,19 @@
[remap]
importer="mp3"
type="AudioStreamMP3"
uid="uid://bew7ciiygabaj"
path="res://.godot/imported/whatdowehavehere.mp3-efdb2c278c76a362c249cec370681a7f.mp3str"
[deps]
source_file="res://assets/audio/sfx/player/notice/whatdowehavehere.mp3"
dest_files=["res://.godot/imported/whatdowehavehere.mp3-efdb2c278c76a362c249cec370681a7f.mp3str"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -14,6 +14,22 @@
[ext_resource type="AudioStream" uid="uid://cbvxokqp1bxar" path="res://assets/audio/sfx/enemies/goblin/raargh3.mp3" id="11_5x2ph"] [ext_resource type="AudioStream" uid="uid://cbvxokqp1bxar" path="res://assets/audio/sfx/enemies/goblin/raargh3.mp3" id="11_5x2ph"]
[ext_resource type="AudioStream" uid="uid://dscx61fdkejlt" path="res://assets/audio/sfx/enemies/goblin/ive_been_waiting_for_this.mp3" id="12_oynfq"] [ext_resource type="AudioStream" uid="uid://dscx61fdkejlt" path="res://assets/audio/sfx/enemies/goblin/ive_been_waiting_for_this.mp3" id="12_oynfq"]
[ext_resource type="AudioStream" uid="uid://ban8uv8hifsgc" path="res://assets/audio/sfx/enemies/goblin/stay_back_if_you_wanna_keep_your_head.mp3" id="13_b0veo"] [ext_resource type="AudioStream" uid="uid://ban8uv8hifsgc" path="res://assets/audio/sfx/enemies/goblin/stay_back_if_you_wanna_keep_your_head.mp3" id="13_b0veo"]
[ext_resource type="Texture2D" uid="uid://bkca7nmt4du5e" path="res://assets/gfx/Puny-Characters/ShieldOverlayer.png" id="14_shield"]
[ext_resource type="Texture2D" uid="uid://bpxxpdpow5qyl" path="res://assets/gfx/Puny-Characters/ShieldOverlayerHolding.png" id="15_shieldh"]
[ext_resource type="PackedScene" path="res://scenes/incantation.tscn" id="16_inc"]
[ext_resource type="AudioStream" uid="uid://c10ju1f6d4ed3" path="res://assets/audio/sfx/shield/activate_shield.wav" id="17_sfx"]
[ext_resource type="AudioStream" uid="uid://ly1euk0v3jxy" path="res://assets/audio/sfx/shield/shield.wav" id="18_sfx"]
[ext_resource type="AudioStream" uid="uid://c4lh535yj010h" path="res://assets/audio/sfx/shield/shield1.wav" id="19_sfx"]
[ext_resource type="AudioStream" uid="uid://ch3p57i7fvd1v" path="res://assets/audio/sfx/shield/shield2.wav" id="20_sfx"]
[ext_resource type="AudioStream" uid="uid://t0sg2rxlfech" path="res://assets/audio/sfx/shield/shield3.wav" id="21_sfx"]
[ext_resource type="AudioStream" uid="uid://b6klanrso0vvq" path="res://assets/audio/sfx/weapons/bow/bow_release_1.mp3" id="22_sfx"]
[ext_resource type="AudioStream" uid="uid://b6mwlp2ap0wbj" path="res://assets/audio/sfx/weapons/bow/bow_release2.mp3" id="23_sfx"]
[ext_resource type="AudioStream" uid="uid://d1ut5lnlch0k2" path="res://assets/audio/sfx/weapons/bow/bow_release3.mp3" id="24_sfx"]
[ext_resource type="AudioStream" uid="uid://mbtpqlb5n3gd" path="res://assets/audio/sfx/weapons/bow/bow_no_arrow.mp3" id="25_sfx"]
[ext_resource type="AudioStream" uid="uid://cgya50qrx8gms" path="res://assets/audio/sfx/weapons/bow/buckle_bow.mp3" id="26_sfx"]
[ext_resource type="AudioStream" uid="uid://bvi00vbftbgc5" path="res://assets/audio/sfx/player/ultra_run/shinespark_start.wav" id="27_sfx"]
[ext_resource type="AudioStream" uid="uid://4vulahdsj4i2" path="res://assets/audio/sfx/swoosh/throw_01.wav.mp3" id="28_sfx"]
[ext_resource type="AudioStream" uid="uid://w6yon88kjfml" path="res://assets/audio/sfx/nickes/lift_sfx.ogg" id="29_sfx"]
[sub_resource type="Gradient" id="Gradient_1"] [sub_resource type="Gradient" id="Gradient_1"]
colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0) colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0)
@@ -211,6 +227,40 @@ stream_2/stream = ExtResource("11_5x2ph")
stream_3/stream = ExtResource("12_oynfq") stream_3/stream = ExtResource("12_oynfq")
stream_4/stream = ExtResource("13_b0veo") stream_4/stream = ExtResource("13_b0veo")
[sub_resource type="ShaderMaterial" id="ShaderMaterial_shield"]
shader = ExtResource("4_r7ul0")
shader_parameter/original_0 = Color(0, 0, 0, 1)
shader_parameter/original_1 = Color(0, 0, 0, 1)
shader_parameter/original_2 = Color(0, 0, 0, 1)
shader_parameter/original_3 = Color(0, 0, 0, 1)
shader_parameter/original_4 = Color(0, 0, 0, 1)
shader_parameter/original_5 = Color(0, 0, 0, 1)
shader_parameter/original_6 = Color(0, 0, 0, 1)
shader_parameter/replace_0 = Color(0, 0, 0, 1)
shader_parameter/replace_1 = Color(0, 0, 0, 1)
shader_parameter/replace_2 = Color(0, 0, 0, 1)
shader_parameter/replace_3 = Color(0, 0, 0, 1)
shader_parameter/replace_4 = Color(0, 0, 0, 1)
shader_parameter/replace_5 = Color(0, 0, 0, 1)
shader_parameter/replace_6 = Color(0, 0, 0, 1)
shader_parameter/tint = Color(1, 1, 1, 1)
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_bow"]
playback_mode = 1
random_pitch = 1.0123794
streams_count = 3
stream_0/stream = ExtResource("22_sfx")
stream_1/stream = ExtResource("23_sfx")
stream_2/stream = ExtResource("24_sfx")
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_block"]
playback_mode = 1
streams_count = 4
stream_0/stream = ExtResource("18_sfx")
stream_1/stream = ExtResource("19_sfx")
stream_2/stream = ExtResource("20_sfx")
stream_3/stream = ExtResource("21_sfx")
[node name="EnemyHumanoid" type="CharacterBody2D" unique_id=285357386] [node name="EnemyHumanoid" type="CharacterBody2D" unique_id=285357386]
collision_layer = 2 collision_layer = 2
collision_mask = 65 collision_mask = 65
@@ -278,12 +328,28 @@ material = SubResource("ShaderMaterial_i1636")
hframes = 35 hframes = 35
vframes = 8 vframes = 8
[node name="Sprite2DShield" type="Sprite2D" parent="."]
visible = false
material = SubResource("ShaderMaterial_shield")
texture = ExtResource("14_shield")
hframes = 35
vframes = 8
[node name="Sprite2DShieldHolding" type="Sprite2D" parent="."]
visible = false
material = SubResource("ShaderMaterial_shield")
texture = ExtResource("15_shieldh")
hframes = 35
vframes = 8
[node name="Sprite2DWeapon" type="Sprite2D" parent="." unique_id=1718282928] [node name="Sprite2DWeapon" type="Sprite2D" parent="." unique_id=1718282928]
y_sort_enabled = true y_sort_enabled = true
texture = ExtResource("4") texture = ExtResource("4")
hframes = 35 hframes = 35
vframes = 8 vframes = 8
[node name="Incantation" parent="." instance=ExtResource("16_inc")]
[node name="AlertIndicator" type="Sprite2D" parent="." unique_id=1697001148] [node name="AlertIndicator" type="Sprite2D" parent="." unique_id=1697001148]
visible = false visible = false
z_index = 100 z_index = 100
@@ -330,3 +396,48 @@ attenuation = 8.57418
max_polyphony = 4 max_polyphony = 4
panning_strength = 1.04 panning_strength = 1.04
bus = &"Sfx" bus = &"Sfx"
[node name="SfxActivateShield" type="AudioStreamPlayer2D" parent="."]
stream = ExtResource("17_sfx")
volume_db = 9.695
attenuation = 1.3660401
panning_strength = 1.78
[node name="SfxBlockWithShield" type="AudioStreamPlayer2D" parent="."]
stream = SubResource("AudioStreamRandomizer_block")
volume_db = 7.254
attenuation = 1.3195078
panning_strength = 1.06
bus = &"Sfx"
[node name="SfxBowShoot" type="AudioStreamPlayer2D" parent="."]
stream = SubResource("AudioStreamRandomizer_bow")
pitch_scale = 1.33
attenuation = 6.7271657
[node name="SfxBowWithoutArrow" type="AudioStreamPlayer2D" parent="."]
stream = ExtResource("25_sfx")
max_distance = 1455.0
attenuation = 7.4642572
[node name="SfxBuckleBow" type="AudioStreamPlayer2D" parent="."]
stream = ExtResource("26_sfx")
attenuation = 7.727478
panning_strength = 1.03
[node name="SfxSpellCharge" type="AudioStreamPlayer2D" parent="."]
stream = ExtResource("27_sfx")
[node name="SfxThrow" type="AudioStreamPlayer2D" parent="."]
stream = ExtResource("28_sfx")
pitch_scale = 0.61
max_distance = 983.0
attenuation = 8.876549
panning_strength = 1.04
[node name="SfxLift" type="AudioStreamPlayer2D" parent="."]
stream = ExtResource("29_sfx")
max_distance = 1246.0
attenuation = 1.9999994
panning_strength = 1.11
bus = &"Sfx"

View File

@@ -108,16 +108,16 @@ y_sort_enabled = true
script = ExtResource("5") script = ExtResource("5")
[node name="CanvasModulate" type="CanvasModulate" parent="." unique_id=948490815] [node name="CanvasModulate" type="CanvasModulate" parent="." unique_id=948490815]
visible = false
light_mask = 1048575 light_mask = 1048575
visibility_layer = 1048575 visibility_layer = 1048575
color = Color(0.69140625, 0.69140625, 0.69140625, 1) color = Color(0.69140625, 0.69140625, 0.69140625, 1)
[node name="SfxWinds" type="AudioStreamPlayer2D" parent="." unique_id=1141138343] [node name="SfxWinds" type="AudioStreamPlayer" parent="." unique_id=1563020465]
stream = ExtResource("6_6c6v5") stream = ExtResource("6_6c6v5")
volume_db = -3.085 volume_db = -20.411
autoplay = true autoplay = true
bus = &"Sfx"
[node name="BgMusic" type="AudioStreamPlayer2D" parent="." unique_id=628820950] [node name="BgMusic" type="AudioStreamPlayer" parent="." unique_id=925983703]
stream = ExtResource("8_pdbwf") stream = ExtResource("8_pdbwf")
autoplay = true volume_db = -15.864

159
src/scenes/incantation.tscn Normal file
View File

@@ -0,0 +1,159 @@
[gd_scene format=3 uid="uid://dwxyxlqdgclwi"]
[ext_resource type="Texture2D" uid="uid://bf158atxi7ucy" path="res://assets/gfx/fx/shade_spell_effects.png" id="1_tex"]
[sub_resource type="Animation" id="Animation_reset"]
length = 0.001
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("IncantationSprite:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 1,
"values": [2037]
}
[sub_resource type="Animation" id="Animation_fire_ch"]
resource_name = "fire_charging"
length = 0.4
loop_mode = 1
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("IncantationSprite:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.03333334, 0.06666667, 0.10000002, 0.13333336, 0.16666669, 0.20000002, 0.23333335, 0.26666668, 0.30000004, 0.33333337, 0.3666667, 0.4, 0.43333337, 0.46666673, 0.5, 0.53333336),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
"update": 1,
"values": [2037, 2038, 2039, 2040, 2041, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051, 2052, 2053]
}
[sub_resource type="Animation" id="Animation_fire_rdy"]
resource_name = "fire_ready"
length = 0.6
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("IncantationSprite:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.03333334, 0.06666667, 0.10000002, 0.13333336, 0.16666669, 0.20000002, 0.23333335, 0.26666668, 0.30000004, 0.33333337, 0.3666667, 0.4, 0.43333337, 0.46666673, 0.5, 0.53333336),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
"update": 1,
"values": [2037, 2038, 2039, 2040, 2041, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051, 2052, 2053]
}
[sub_resource type="Animation" id="Animation_frost_ch"]
resource_name = "frost_charging"
length = 0.566
loop_mode = 1
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("IncantationSprite:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.03333334, 0.06666667, 0.1, 0.13333334, 0.16666667, 0.2, 0.23333333, 0.26666668, 0.3, 0.33333334, 0.36666667, 0.4, 0.43333334, 0.46666667, 0.5, 0.53333336),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
"update": 1,
"values": [3678, 3679, 3680, 3681, 3682, 3683, 3684, 3685, 3686, 3687, 3688, 3689, 3690, 3691, 3692, 3693, 3694]
}
[sub_resource type="Animation" id="Animation_frost_rdy"]
resource_name = "frost_ready"
length = 0.566
loop_mode = 1
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("IncantationSprite:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.03333334, 0.06666667, 0.1, 0.13333334, 0.16666667, 0.2, 0.23333333, 0.26666668, 0.3, 0.33333334, 0.36666667, 0.4, 0.43333334, 0.46666667, 0.5, 0.53333336),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
"update": 1,
"values": [3678, 3679, 3680, 3681, 3682, 3683, 3684, 3685, 3686, 3687, 3688, 3689, 3690, 3691, 3692, 3693, 3694]
}
[sub_resource type="Animation" id="Animation_heal_ch"]
resource_name = "healing_charging"
length = 0.5
loop_mode = 1
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("IncantationSprite:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
"update": 1,
"values": [589, 590, 591, 592, 593, 594, 595, 596, 597, 598]
}
[sub_resource type="Animation" id="Animation_heal_rdy"]
resource_name = "healing_ready"
length = 0.5
loop_mode = 1
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("IncantationSprite:frame")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45),
"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
"update": 1,
"values": [589, 590, 591, 592, 593, 594, 595, 596, 597, 598]
}
[sub_resource type="Animation" id="Animation_idle"]
resource_name = "idle"
length = 0.1
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("IncantationSprite: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="AnimationLibrary" id="AnimationLibrary_incant"]
_data = {
&"RESET": SubResource("Animation_reset"),
&"fire_charging": SubResource("Animation_fire_ch"),
&"fire_ready": SubResource("Animation_fire_rdy"),
&"frost_charging": SubResource("Animation_frost_ch"),
&"frost_ready": SubResource("Animation_frost_rdy"),
&"healing_charging": SubResource("Animation_heal_ch"),
&"healing_ready": SubResource("Animation_heal_rdy"),
&"idle": SubResource("Animation_idle")
}
[node name="Incantation" type="Node2D"]
[node name="IncantationSprite" type="Sprite2D" parent="."]
texture = ExtResource("1_tex")
hframes = 105
vframes = 79
frame = 2037
[node name="AnimationIncantation" type="AnimationPlayer" parent="."]
libraries/ = SubResource("AnimationLibrary_incant")
autoplay = &"idle"

View File

@@ -47,6 +47,9 @@
[ext_resource type="AudioStream" uid="uid://dvq72502qa46f" path="res://assets/audio/sfx/shield/denied_activate_Shield2.wav" id="45_g5jhy"] [ext_resource type="AudioStream" uid="uid://dvq72502qa46f" path="res://assets/audio/sfx/shield/denied_activate_Shield2.wav" id="45_g5jhy"]
[ext_resource type="AudioStream" uid="uid://8l0hx3sjh4ci" path="res://assets/audio/sfx/jsfxr/hitHurt (1).wav" id="46_holxr"] [ext_resource type="AudioStream" uid="uid://8l0hx3sjh4ci" path="res://assets/audio/sfx/jsfxr/hitHurt (1).wav" id="46_holxr"]
[ext_resource type="AudioStream" uid="uid://cjv4cf2kiomwo" path="res://assets/audio/sfx/jsfxr/bird_sound.wav" id="47_mx1m4"] [ext_resource type="AudioStream" uid="uid://cjv4cf2kiomwo" path="res://assets/audio/sfx/jsfxr/bird_sound.wav" id="47_mx1m4"]
[ext_resource type="AudioStream" uid="uid://cbbeyrdor7nyg" path="res://assets/audio/sfx/player/notice/lookout.mp3" id="48_6e8lb"]
[ext_resource type="AudioStream" uid="uid://cs5ruoyq80yi4" path="res://assets/audio/sfx/player/notice/aha.mp3" id="49_2gdjj"]
[ext_resource type="AudioStream" uid="uid://bew7ciiygabaj" path="res://assets/audio/sfx/player/notice/whatdowehavehere.mp3" id="50_sc3ue"]
[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)
@@ -563,6 +566,12 @@ _data = {
&"sleep": SubResource("Animation_mx1m4") &"sleep": SubResource("Animation_mx1m4")
} }
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_lxlsd"]
random_pitch = 1.1036249
streams_count = 2
stream_0/stream = ExtResource("49_2gdjj")
stream_1/stream = ExtResource("50_sc3ue")
[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
@@ -740,8 +749,9 @@ panning_strength = 1.04
[node name="SfxLift" type="AudioStreamPlayer2D" parent="." unique_id=1261167113] [node name="SfxLift" type="AudioStreamPlayer2D" parent="." unique_id=1261167113]
stream = ExtResource("28_pf23h") stream = ExtResource("28_pf23h")
max_distance = 1246.0 max_distance = 1246.0
attenuation = 6.964403 attenuation = 1.9999994
panning_strength = 1.11 panning_strength = 1.11
bus = &"Sfx"
[node name="DirectionalLight2D" type="DirectionalLight2D" parent="." unique_id=1013099358] [node name="DirectionalLight2D" type="DirectionalLight2D" parent="." unique_id=1013099358]
visible = false visible = false
@@ -802,6 +812,11 @@ bus = &"Sfx"
stream = ExtResource("45_g5jhy") stream = ExtResource("45_g5jhy")
volume_db = 9.458 volume_db = 9.458
[node name="SfxDeny" type="AudioStreamPlayer2D" parent="." unique_id=1239738372]
stream = ExtResource("45_g5jhy")
volume_db = 9.458
bus = &"Sfx"
[node name="Sprite2DStatus" type="Sprite2D" parent="." unique_id=1335748461] [node name="Sprite2DStatus" type="Sprite2D" parent="." unique_id=1335748461]
position = Vector2(0, -10) position = Vector2(0, -10)
texture = ExtResource("37_hax0n") texture = ExtResource("37_hax0n")
@@ -825,3 +840,22 @@ volume_db = -13.255
attenuation = 3.2490087 attenuation = 3.2490087
panning_strength = 1.12 panning_strength = 1.12
bus = &"Sfx" bus = &"Sfx"
[node name="SfxLookOut" type="AudioStreamPlayer2D" parent="." unique_id=1177750193]
stream = ExtResource("48_6e8lb")
volume_db = 0.881
max_distance = 1138.0
attenuation = 7.999997
panning_strength = 1.04
bus = &"Sfx"
[node name="SfxAhaa" type="AudioStreamPlayer2D" parent="." unique_id=1556952538]
stream = SubResource("AudioStreamRandomizer_lxlsd")
max_distance = 1496.0
attenuation = 6.062864
panning_strength = 1.13
bus = &"Sfx"
[node name="SfxDeny2" type="AudioStreamPlayer2D" parent="." unique_id=1127340261]
stream = ExtResource("45_g5jhy")
max_distance = 1619.0

View File

@@ -424,8 +424,8 @@ func _spawn_explosion_tile_particles():
# Direction from explosion center to this tile (outward) particles fly away from bomb # Direction from explosion center to this tile (outward) particles fly away from bomb
var to_tile = world - center var to_tile = world - center
var outward = to_tile.normalized() if to_tile.length() > 1.0 else Vector2.RIGHT.rotated(randf() * TAU) var outward = to_tile.normalized() if to_tile.length() > 1.0 else Vector2.RIGHT.rotated(randf() * TAU)
# Half the particles: 2 pieces per tile instead of 4 (indices 0 and 2) # Reduced particles: 1 piece per tile instead of 2 (use index 0)
for i in [0, 2]: var i = 0
var p = _explosion_tile_particle_scene.instantiate() as CharacterBody2D var p = _explosion_tile_particle_scene.instantiate() as CharacterBody2D
var spr = p.get_node_or_null("Sprite2D") as Sprite2D var spr = p.get_node_or_null("Sprite2D") as Sprite2D
if not spr: if not spr:

View File

@@ -1345,4 +1345,14 @@ func _sync_door_close():
is_closing = true is_closing = true
move_timer = 0.0 move_timer = 0.0
# Play close sound on clients (host plays it in _close())
if type == "GateDoor":
var sfx = get_node_or_null("SfxCloseGateDoor")
if sfx and sfx.stream:
sfx.play()
else:
var sfx = get_node_or_null("SfxDoorCloses")
if sfx and sfx.stream:
sfx.play()
LogManager.log("Door: Client received door close RPC for " + str(name) + " - starting close animation", LogManager.CATEGORY_DOOR) LogManager.log("Door: Client received door close RPC for " + str(name) + " - starting close animation", LogManager.CATEGORY_DOOR)

View File

@@ -281,7 +281,19 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
if stairs_data.is_empty(): if stairs_data.is_empty():
LogManager.log_error("DungeonGenerator: CRITICAL ERROR - Could not place stairs even with force placement!", LogManager.CATEGORY_DUNGEON) LogManager.log_error("DungeonGenerator: CRITICAL ERROR - Could not place stairs even with force placement!", LogManager.CATEGORY_DUNGEON)
# 8. Place torches in rooms (AFTER stairs, so torches don't overlap stairs) # 7.6. Place entrance in start room (only for level > 1) BEFORE placing torches
var entrance_data = {}
if level > 1:
entrance_data = _place_entrance_in_start_room(all_rooms[start_room_index], grid, tile_grid, map_size, all_doors, rng)
if entrance_data.is_empty():
LogManager.log_error("DungeonGenerator: ERROR - Failed to place entrance in start room! Room size: " + str(all_rooms[start_room_index].w) + "x" + str(all_rooms[start_room_index].h) + " Doors: " + str(all_doors.size()), LogManager.CATEGORY_DUNGEON)
# CRITICAL: Force place entrance - we MUST have an entrance for level > 1!
LogManager.log("DungeonGenerator: FORCING entrance placement in start room", LogManager.CATEGORY_DUNGEON)
entrance_data = _force_place_stairs(all_rooms[start_room_index], grid, tile_grid, map_size, all_doors, rng)
if entrance_data.is_empty():
LogManager.log_error("DungeonGenerator: CRITICAL ERROR - Could not place entrance even with force placement!", LogManager.CATEGORY_DUNGEON)
# 8. Place torches in rooms (AFTER stairs/entrance, so torches don't overlap)
var all_torches = [] var all_torches = []
for room in all_rooms: for room in all_rooms:
var room_torches = _place_torches_in_room(room, grid, all_doors, map_size, rng) var room_torches = _place_torches_in_room(room, grid, all_doors, map_size, rng)
@@ -353,6 +365,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
"interactable_objects": all_interactable_objects, "interactable_objects": all_interactable_objects,
"traps": all_traps, "traps": all_traps,
"stairs": stairs_data, "stairs": stairs_data,
"entrance": entrance_data,
"blocking_doors": blocking_doors, "blocking_doors": blocking_doors,
"grid": grid, "grid": grid,
"tile_grid": tile_grid, "tile_grid": tile_grid,
@@ -1257,7 +1270,8 @@ func _place_enemies_in_room(room: Dictionary, grid: Array, map_size: Vector2i, r
"res://scenes/enemy_rat.tscn", "res://scenes/enemy_rat.tscn",
"res://scenes/enemy_humanoid.tscn", "res://scenes/enemy_humanoid.tscn",
"res://scenes/enemy_slime.tscn", "res://scenes/enemy_slime.tscn",
"res://scenes/enemy_bat.tscn" "res://scenes/enemy_bat.tscn",
"res://scenes/enemy_hand.tscn"
] ]
# Find valid floor positions in the room (excluding walls) # Find valid floor positions in the room (excluding walls)
@@ -1298,6 +1312,8 @@ func _place_enemies_in_room(room: Dictionary, grid: Array, map_size: Vector2i, r
move_speed = rng.randf_range(18.0, 25.0) # Slimes: very slow (reduced from 30-40) move_speed = rng.randf_range(18.0, 25.0) # Slimes: very slow (reduced from 30-40)
elif enemy_type.ends_with("rat.tscn"): elif enemy_type.ends_with("rat.tscn"):
move_speed = rng.randf_range(40.0, 50.0) # Rats: slow move_speed = rng.randf_range(40.0, 50.0) # Rats: slow
elif enemy_type.ends_with("hand.tscn"):
move_speed = rng.randf_range(25.0, 32.0) # Hands: slow (reduced from 55.0 to 28.0 in script)
else: else:
move_speed = rng.randf_range(50.0, 80.0) # Other enemies (humanoids): faster move_speed = rng.randf_range(50.0, 80.0) # Other enemies (humanoids): faster
@@ -1606,6 +1622,15 @@ func _place_stairs_in_exit_room(exit_room: Dictionary, grid: Array, tile_grid: A
return stairs_data return stairs_data
func _place_entrance_in_start_room(start_room: Dictionary, grid: Array, tile_grid: Array, map_size: Vector2i, all_doors: Array, rng: RandomNumberGenerator) -> Dictionary:
# Place entrance in the start room (similar to stairs but for entrance)
# Entrance is rendered like stairs but with different purpose
# Returns entrance data with position and size for Area2D creation
LogManager.log("DungeonGenerator: Placing entrance in start room: " + str(start_room.x) + "," + str(start_room.y) + " size: " + str(start_room.w) + "x" + str(start_room.h) + " doors: " + str(all_doors.size()), LogManager.CATEGORY_DUNGEON)
# Reuse the stairs placement logic (entrance looks like stairs)
return _place_stairs_in_exit_room(start_room, grid, tile_grid, map_size, all_doors, rng)
func _force_place_stairs(exit_room: Dictionary, grid: Array, tile_grid: Array, map_size: Vector2i, all_doors: Array, _rng: RandomNumberGenerator) -> Dictionary: func _force_place_stairs(exit_room: Dictionary, grid: Array, tile_grid: Array, map_size: Vector2i, all_doors: Array, _rng: RandomNumberGenerator) -> Dictionary:
# Force place stairs in exit room - used as fallback when normal placement fails # Force place stairs in exit room - used as fallback when normal placement fails
# Still tries to avoid door overlaps, but will place stairs even if room is small # Still tries to avoid door overlaps, but will place stairs even if room is small
@@ -2093,13 +2118,20 @@ func _is_valid_interactable_position(world_pos: Vector2, all_doors: Array, all_e
return true return true
func _find_rooms_before_door(door: Dictionary, start_room: Dictionary, _all_rooms: Array, all_doors: Array) -> Array: func _find_rooms_before_door(door: Dictionary, start_room: Dictionary, _all_rooms: Array, all_doors: Array, other_blocking_doors: Array = []) -> Array:
# Find rooms that are reachable from start WITHOUT going through this door # Find rooms that are reachable from start WITHOUT going through this door
# This is used to place keys before KeyDoors # This is used to place keys before KeyDoors
# other_blocking_doors: Array of other doors that should also be treated as blocking (e.g., other key doors)
var rooms_before_door = [] var rooms_before_door = []
var visited = [start_room] var visited = [start_room]
var queue = [start_room] var queue = [start_room]
# Create a set of all blocking doors (this door + other blocking doors)
var blocking_doors_set = [door]
for blocking_door in other_blocking_doors:
if blocking_door != door:
blocking_doors_set.append(blocking_door)
while queue.size() > 0: while queue.size() > 0:
var current = queue.pop_front() var current = queue.pop_front()
@@ -2109,10 +2141,16 @@ func _find_rooms_before_door(door: Dictionary, start_room: Dictionary, _all_room
if not rooms_before_door.has(current): if not rooms_before_door.has(current):
rooms_before_door.append(current) rooms_before_door.append(current)
# Check all doors connected to current room (except the blocked door) # Check all doors connected to current room (except blocking doors)
for d in all_doors: for d in all_doors:
if d == door: # Skip if this door is in the blocking set
continue # Skip the blocked door var is_blocking = false
for blocking_door in blocking_doors_set:
if d == blocking_door:
is_blocking = true
break
if is_blocking:
continue # Skip blocking doors
var next_room = null var next_room = null
if d.room1 == current: if d.room1 == current:
@@ -2504,6 +2542,8 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
key_doors_to_create.append(door) key_doors_to_create.append(door)
# STEP 4: Create KeyDoors with keys placed BEFORE the keydoor # STEP 4: Create KeyDoors with keys placed BEFORE the keydoor
# First pass: collect all key doors to determine blocking relationships
var key_door_data_list = []
for door in key_doors_to_create: for door in key_doors_to_create:
# Determine direction # Determine direction
var direction = "" var direction = ""
@@ -2556,8 +2596,37 @@ func _place_blocking_doors(all_rooms: Array, all_doors: Array, grid: Array, map_
var door_room2 = door.room2 if "room2" in door else null var door_room2 = door.room2 if "room2" in door else null
var blocking_room = door_room2 if door_room2 != null else door_room1 var blocking_room = door_room2 if door_room2 != null else door_room1
# Store door data for second pass
key_door_data_list.append({
"door": door,
"direction": direction,
"door_world_x": door_world_x,
"door_world_y": door_world_y,
"middle_tile_x": middle_tile_x,
"middle_tile_y": middle_tile_y,
"blocking_room": blocking_room
})
# Second pass: find rooms before each door, accounting for other key doors
for i in range(key_door_data_list.size()):
var key_door_info = key_door_data_list[i]
var door = key_door_info.door
var direction = key_door_info.direction
var door_world_x = key_door_info.door_world_x
var door_world_y = key_door_info.door_world_y
var middle_tile_x = key_door_info.middle_tile_x
var middle_tile_y = key_door_info.middle_tile_y
var blocking_room = key_door_info.blocking_room
# Collect other key doors (all key doors except this one)
var other_key_doors = []
for j in range(key_door_data_list.size()):
if i != j:
other_key_doors.append(key_door_data_list[j].door)
# Find rooms reachable BEFORE this door (for key placement) # Find rooms reachable BEFORE this door (for key placement)
var rooms_before_door = _find_rooms_before_door(door, start_room, all_rooms, all_doors) # Pass other key doors so they're also treated as blocking
var rooms_before_door = _find_rooms_before_door(door, start_room, all_rooms, all_doors, other_key_doors)
# Pick a room for the key (must be reachable before the door) # Pick a room for the key (must be reachable before the door)
var key_room = null var key_room = null

View File

@@ -458,6 +458,16 @@ func rpc_take_damage(amount: float, from_position: Vector2, is_critical: bool =
if is_multiplayer_authority(): if is_multiplayer_authority():
take_damage(amount, from_position, is_critical, is_burn_damage, apply_burn_debuff) take_damage(amount, from_position, is_critical, is_burn_damage, apply_burn_debuff)
@rpc("any_peer", "reliable")
func rpc_heal_enemy(amount: float, allow_overheal: bool = false):
# RPC version - only process on server/authority
if is_multiplayer_authority():
if character_stats:
character_stats.heal(amount, allow_overheal)
# Sync current_health for backwards compatibility
current_health = character_stats.hp
LogManager.log(str(name) + " healed for " + str(amount) + " HP! Health: " + str(current_health) + "/" + str(character_stats.maxhp), LogManager.CATEGORY_ENEMY)
func _show_damage_number(amount: float, from_position: Vector2, is_critical: bool = false, is_miss: bool = false, is_dodged: bool = false): func _show_damage_number(amount: float, from_position: Vector2, is_critical: bool = false, is_miss: bool = false, is_dodged: bool = false):
# Show damage number (red/orange for crits, cyan for dodge, gray for miss, using dmg_numbers.png font) above enemy # Show damage number (red/orange for crits, cyan for dodge, gray for miss, using dmg_numbers.png font) above enemy
# Show even if amount is 0 for MISS/DODGED # Show even if amount is 0 for MISS/DODGED

View File

@@ -11,20 +11,27 @@ var players_in_interest: Array[Node] = []
var grabbed_player: Node = null var grabbed_player: Node = null
var random_move_dir: Vector2 = Vector2.ZERO var random_move_dir: Vector2 = Vector2.ZERO
var random_move_timer: float = 0.0 var random_move_timer: float = 0.0
var grab_cooldown_timer: float = 0.0
const RANDOM_MOVE_INTERVAL: float = 1.2 const RANDOM_MOVE_INTERVAL: float = 1.2
const SNATCH_DURATION: float = 0.4 const SNATCH_DURATION: float = 0.4
const SNATCH_DAMAGE: float = 12.0 const SNATCH_DAMAGE: float = 12.0
const GRAB_COOLDOWN: float = 2.0 # Cooldown after grabbing before can grab again
const TILE_SIZE: int = 16
const TILE_STRIDE: int = 17 # 16 + separation 1
@onready var emerge_area: Area2D = $EmergeArea @onready var emerge_area: Area2D = $EmergeArea
@onready var grab_area: Area2D = $GrabPlayerArea @onready var grab_area: Area2D = $GrabPlayerArea
@onready var interest_area: Area2D = $PlayerInterestArea @onready var interest_area: Area2D = $PlayerInterestArea
@onready var anim_player: AnimationPlayer = $AnimationPlayer @onready var anim_player: AnimationPlayer = $AnimationPlayer
@onready var hand_sprite: Sprite2D = $Sprite2D
var _tile_particle_scene: PackedScene = null
var blood_scene: PackedScene = preload("res://scenes/blood_clot.tscn")
func _ready() -> void: func _ready() -> void:
super._ready() super._ready()
max_health = 25.0 max_health = 25.0
current_health = max_health current_health = max_health
move_speed = 55.0 move_speed = 28.0 # Reduced from 55.0 - much slower
damage = SNATCH_DAMAGE damage = SNATCH_DAMAGE
exp_reward = 8.0 exp_reward = 8.0
collision_layer = 2 collision_layer = 2
@@ -72,15 +79,148 @@ func _die() -> void:
v.rpc_released_from_enemy_hand.rpc_id(pid) v.rpc_released_from_enemy_hand.rpc_id(pid)
else: else:
v.rpc_released_from_enemy_hand.rpc() v.rpc_released_from_enemy_hand.rpc()
# Spawn blood clots (similar to player/enemy death)
_spawn_blood_clots()
# Split hand into 4 pieces that fly in all directions
_spawn_hand_pieces()
super._die() super._die()
func _spawn_blood_clots():
# Spawn blood clots when enemy hand dies (similar to player/enemy death)
if not is_multiplayer_authority():
return
if not blood_scene:
return
var parent = get_parent()
if not parent:
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
parent = game_world.get_node_or_null("Entities")
if not parent:
return
# Spawn 12 blood clots (same as player/enemy)
for i in 12:
var angle = randf_range(0, TAU)
var speed = randf_range(50, 100)
var initial_velocityZ = randf_range(50, 90)
var b = blood_scene.instantiate() as CharacterBody2D
b.scale = Vector2(randf_range(0.3, 2), randf_range(0.3, 2))
b.global_position = global_position
# Set initial velocities
var direction = Vector2.from_angle(angle)
b.velocity = direction * speed
b.velocityZ = initial_velocityZ
parent.call_deferred("add_child", b)
func _spawn_hand_pieces():
# Split enemy hand into 4 pieces that fly in all directions (like tile particles)
if not is_multiplayer_authority():
return
if not hand_sprite or not hand_sprite.texture:
return
if not _tile_particle_scene:
_tile_particle_scene = load("res://scenes/explosion_tile_particle.tscn") as PackedScene
if not _tile_particle_scene:
return
var parent = get_parent()
if not parent:
var game_world = get_tree().get_first_node_in_group("game_world")
if game_world:
parent = game_world.get_node_or_null("Entities")
if not parent:
return
# Get current sprite frame
var current_frame = hand_sprite.frame if hand_sprite else 0
var hframes = hand_sprite.hframes if hand_sprite else 4
var vframes = hand_sprite.vframes if hand_sprite else 4
# Calculate frame position in texture
var frame_x = current_frame % hframes
var frame_y = int(float(current_frame) / float(hframes))
# Get texture size and calculate frame size
var tex = hand_sprite.texture
var frame_width = float(tex.get_width()) / float(hframes)
var frame_height = float(tex.get_height()) / float(vframes)
# Calculate the 4 quadrants of the current frame
var half_width = frame_width / 2.0
var half_height = frame_height / 2.0
var base_x = frame_x * frame_width
var base_y = frame_y * frame_height
# 4 quadrants: top-left, top-right, bottom-left, bottom-right
var regions = [
Rect2(base_x, base_y, half_width, half_height), # Top-left
Rect2(base_x + half_width, base_y, half_width, half_height), # Top-right
Rect2(base_x, base_y + half_height, half_width, half_height), # Bottom-left
Rect2(base_x + half_width, base_y + half_height, half_width, half_height) # Bottom-right
]
# 4 directions: up-left, up-right, down-left, down-right
var directions = [
Vector2(-1, -1).normalized(), # Up-left
Vector2(1, -1).normalized(), # Up-right
Vector2(-1, 1).normalized(), # Down-left
Vector2(1, 1).normalized() # Down-right
]
# Spawn 4 pieces
for i in range(4):
var p = _tile_particle_scene.instantiate() as CharacterBody2D
var piece_sprite = p.get_node_or_null("Sprite2D") as Sprite2D
if not piece_sprite:
p.queue_free()
continue
# Use the hand texture and region
piece_sprite.texture = tex
piece_sprite.region_enabled = true
piece_sprite.region_rect = regions[i]
# Position at hand location
p.global_position = global_position
# Fly in the direction for this piece
var direction = directions[i]
var speed = randf_range(200.0, 300.0) # Fast enough to see them fly
p.velocity = direction * speed
p.angular_velocity = randf_range(-10.0, 10.0)
p.position_z = 0.0
p.velocity_z = randf_range(80.0, 140.0) # Upward burst, then gravity
# Use call_deferred to avoid physics query flush errors
parent.call_deferred("add_child", p)
func _ai_behavior(delta: float) -> void: func _ai_behavior(delta: float) -> void:
# Update grab cooldown timer
if grab_cooldown_timer > 0.0:
grab_cooldown_timer -= delta
if state == HandState.HIDDEN or state == HandState.EMERGING: if state == HandState.HIDDEN or state == HandState.EMERGING:
velocity = Vector2.ZERO velocity = Vector2.ZERO
return return
if state == HandState.GRABBING: if state == HandState.GRABBING:
velocity = Vector2.ZERO velocity = Vector2.ZERO
# Update grabbed player position to follow hand (slightly above)
if grabbed_player and is_instance_valid(grabbed_player):
var target_pos = global_position + Vector2(0, -12) # Slightly above the hand
# Smoothly move player to hand position (only on authority)
if is_multiplayer_authority():
grabbed_player.global_position = grabbed_player.global_position.lerp(target_pos, delta * 8.0)
return return
# IDLE: move toward player if in interest, else random # IDLE: move toward player if in interest, else random
@@ -120,6 +260,13 @@ func _on_emerge_area_body_entered(body: Node2D) -> void:
if anim_player and anim_player.has_animation("emerge"): if anim_player and anim_player.has_animation("emerge"):
anim_player.play("emerge") anim_player.play("emerge")
# Spawn tile particles when emerging
_spawn_emerge_tile_particles()
# Sync visibility to all clients
if multiplayer.has_multiplayer_peer() and is_inside_tree():
_sync_hand_emerged.rpc()
func _on_animation_finished(anim_name: StringName) -> void: func _on_animation_finished(anim_name: StringName) -> void:
if anim_name == "emerge": if anim_name == "emerge":
@@ -152,6 +299,8 @@ func _on_grab_player_area_body_entered(body: Node2D) -> void:
return return
if grabbed_player != null: if grabbed_player != null:
return return
if grab_cooldown_timer > 0.0:
return # Still on cooldown from previous grab
if not is_multiplayer_authority(): if not is_multiplayer_authority():
return return
@@ -174,12 +323,25 @@ func _on_grab_player_area_body_entered(body: Node2D) -> void:
get_tree().create_timer(SNATCH_DURATION).timeout.connect(_finish_snatch) get_tree().create_timer(SNATCH_DURATION).timeout.connect(_finish_snatch)
@rpc("authority", "reliable")
func _sync_hand_emerged():
# Sync hand emergence visibility to clients
if is_multiplayer_authority():
return # Authority already handled it locally
if state == HandState.HIDDEN:
state = HandState.EMERGING
modulate.a = 1.0
if anim_player and anim_player.has_animation("emerge"):
anim_player.play("emerge")
func _finish_snatch() -> void: func _finish_snatch() -> void:
if not is_instance_valid(self): if not is_instance_valid(self):
return return
var victim = grabbed_player var victim = grabbed_player
grabbed_player = null grabbed_player = null
state = HandState.IDLE state = HandState.IDLE
grab_cooldown_timer = GRAB_COOLDOWN # Start cooldown after grab
if anim_player and anim_player.has_animation("idle"): if anim_player and anim_player.has_animation("idle"):
anim_player.play("idle") anim_player.play("idle")
if not is_instance_valid(victim): if not is_instance_valid(victim):
@@ -208,6 +370,78 @@ func _finish_snatch() -> void:
victim.rpc_released_from_enemy_hand.rpc() victim.rpc_released_from_enemy_hand.rpc()
func _spawn_emerge_tile_particles():
# Spawn tile particles from the tile the hand is emerging from (similar to bomb explosion)
var game_world = get_tree().get_first_node_in_group("game_world")
if not game_world:
return
var layer = game_world.get_node_or_null("Environment/DungeonLayer0")
if not layer or not layer is TileMapLayer:
return
if not _tile_particle_scene:
_tile_particle_scene = load("res://scenes/explosion_tile_particle.tscn") as PackedScene
if not _tile_particle_scene:
return
var tex = load("res://assets/gfx/RPG DUNGEON VOL 3.png") as Texture2D
if not tex:
return
var center = global_position
var layer_pos = center - layer.global_position
var center_cell = layer.local_to_map(layer_pos)
var parent = get_parent()
if not parent:
parent = game_world.get_node_or_null("Entities")
if not parent:
return
# Get the tile at the hand's position
var cell = center_cell
if layer.get_cell_source_id(cell) < 0:
return # No tile at this position
var atlas = layer.get_cell_atlas_coords(cell)
var world = layer.map_to_local(cell) + layer.global_position
var bx = atlas.x * TILE_STRIDE
var by = atlas.y * TILE_STRIDE
var h = 8.0 # TILE_SIZE / 2
var regions = [
Rect2(bx, by, h, h),
Rect2(bx + h, by, h, h),
Rect2(bx, by + h, h, h),
Rect2(bx + h, by + h, h, h)
]
# Spawn 2-3 particles from the tile, flying outward in random directions
var num_particles = randi_range(2, 3)
for i in range(num_particles):
var p = _tile_particle_scene.instantiate() as CharacterBody2D
var spr = p.get_node_or_null("Sprite2D") as Sprite2D
if not spr:
p.queue_free()
continue
spr.texture = tex
spr.region_enabled = true
# Randomly pick one of the 4 tile quadrants
var region_idx = randi() % 4
spr.region_rect = regions[region_idx]
p.global_position = world
# Particles fly outward in random directions (less intense than bomb)
var angle = randf() * TAU
var d = Vector2(cos(angle), sin(angle))
var speed = randf_range(150.0, 250.0) # Slower than bomb explosion
p.velocity = d * speed
p.angular_velocity = randf_range(-8.0, 8.0)
p.position_z = 0.0
p.velocity_z = randf_range(60.0, 120.0) # Upward burst, then gravity
# Use call_deferred to avoid physics query flush errors
parent.call_deferred("add_child", p)
func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, dir: int = 0, frame: int = 0, anim: String = "", frame_num: int = 0, state_value: int = -1) -> void: func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, dir: int = 0, frame: int = 0, anim: String = "", frame_num: int = 0, state_value: int = -1) -> void:
super._sync_position(pos, vel, z_pos, dir, frame, anim, frame_num, state_value) super._sync_position(pos, vel, z_pos, dir, frame, anim, frame_num, state_value)
# Client: keep AnimationPlayer in sync with state if we add state sync later # Client: keep AnimationPlayer in sync with state if we add state sync later

View File

@@ -25,8 +25,14 @@ enum HumanoidType {
@onready var sprite_eyelashes = $Sprite2DEyeLashes @onready var sprite_eyelashes = $Sprite2DEyeLashes
@onready var sprite_addons = $Sprite2DAddons @onready var sprite_addons = $Sprite2DAddons
@onready var sprite_headgear = $Sprite2DHeadgear @onready var sprite_headgear = $Sprite2DHeadgear
@onready var sprite_shield = $Sprite2DShield
@onready var sprite_shield_holding = $Sprite2DShieldHolding
@onready var sprite_weapon = $Sprite2DWeapon @onready var sprite_weapon = $Sprite2DWeapon
# Incantation (spell casting visual)
@onready var incantation_sprite = $Incantation/IncantationSprite
@onready var animation_incantation = $Incantation/AnimationIncantation
# Attack system # Attack system
var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn") var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn")
var can_attack: bool = true var can_attack: bool = true
@@ -37,8 +43,38 @@ var base_attack_charge_time: float = 0.4 # Base charge time before attack
var dex: int = 10 # Dexterity stat (affects attack speed) var dex: int = 10 # Dexterity stat (affects attack speed)
var blood_scene = preload("res://scenes/blood_clot.tscn") var blood_scene = preload("res://scenes/blood_clot.tscn")
# Bow charge visual effect (pulsing)
var bow_charge_tint: Color = Color(1.2, 1.2, 1.2, 1.0) # White tint when fully charged
var bow_charge_tint_pulse_time: float = 0.0 # Timer for pulsing tint animation
var bow_charge_tint_pulse_speed_charged: float = 20.0 # Speed of tint pulse animation when fully charged
var original_sprite_tints: Dictionary = {} # Store original tint values for restoration
# Loadout (player-like abilities) — some humanoids have bow, bomb, spell, shield, lift/throw
var has_bow: bool = false
var arrows_left: int = 0
var has_bomb: bool = false
var bombs_left: int = 0
var spell_type: String = "" # "flames" | "frost" | "healing" | ""
var has_shield: bool = false
var shield_block_chance: float = 0.0
var is_blocking: bool = false # Whether enemy is actively blocking with shield
var shield_block_speed_multiplier: float = 0.5 # Movement speed multiplier when blocking
var shield_block_timer: float = 0.0 # Timer for how long to keep blocking
var shield_block_duration: float = 1.5 # How long to block after raising shield
var can_lift_throw: bool = false
var spell_cooldown_timer: float = 0.0
var bomb_cooldown_timer: float = 0.0
var lift_throw_cooldown_timer: float = 0.0
var ranged_decision_timer: float = 0.0 # Roll for bow/bomb/spell every 0.4s
var attack_arrow_scene: PackedScene = preload("res://scenes/attack_arrow.tscn")
var attack_bomb_scene: PackedScene = preload("res://scenes/attack_bomb.tscn")
var flame_spell_scene: PackedScene = preload("res://scenes/attack_spell_flame.tscn")
var frostspike_spell_scene: PackedScene = preload("res://scenes/attack_spell_frostspike.tscn")
var interactable_object_scene: PackedScene = preload("res://scenes/interactable_object.tscn")
var held_bomb_object: Node = null # Bomb object held above enemy's head before throwing
# AI state # AI state
enum AIState {IDLE, WANDERING, NOTICED, CHASING, ATTACKING, GROUPING} enum AIState {IDLE, WANDERING, NOTICED, CHASING, ATTACKING, GROUPING, BOW_CHARGING, THROWING_BOMB, CASTING_SPELL, LIFTING}
var ai_state: AIState = AIState.IDLE var ai_state: AIState = AIState.IDLE
var state_timer: float = 0.0 var state_timer: float = 0.0
var group_target: Node = null # Other humanoid to group with var group_target: Node = null # Other humanoid to group with
@@ -60,6 +96,14 @@ var lost_target_duration: float = 1.0 # Time before forgetting player
# Sound effects # Sound effects
@onready var sfx_die = $SfxDie @onready var sfx_die = $SfxDie
@onready var sfx_alert_found_player = $SfxAlertFoundPlayer @onready var sfx_alert_found_player = $SfxAlertFoundPlayer
@onready var sfx_activate_shield = $SfxActivateShield
@onready var sfx_block_with_shield = $SfxBlockWithShield
@onready var sfx_bow_shoot = $SfxBowShoot
@onready var sfx_bow_without_arrow = $SfxBowWithoutArrow
@onready var sfx_buckle_bow = $SfxBuckleBow
@onready var sfx_spell_charge = $SfxSpellCharge
@onready var sfx_throw = $SfxThrow
@onready var sfx_lift = $SfxLift
# Animation system (same as player) # Animation system (same as player)
const ANIMATIONS = { const ANIMATIONS = {
@@ -216,6 +260,30 @@ func _ready():
# Undead types (e.g. skeleton) take damage from healing spell # Undead types (e.g. skeleton) take damage from healing spell
is_undead = (humanoid_type == HumanoidType.SKELETON) is_undead = (humanoid_type == HumanoidType.SKELETON)
# Assign loadout (bow, bomb, spell, shield, lift/throw) — variety, not experts
_assign_loadout()
# Adjust headgear for spell casters (must be after loadout assignment)
if spell_type != "":
# Spell caster: remove non-magician headgear and replace with magician headgear or none
if sprite_headgear and sprite_headgear.texture:
# Check if current headgear is NOT a magician headgear
var current_path = sprite_headgear.texture.resource_path
var is_magician_headgear = "Basic Mage" in current_path or current_path.ends_with("Headband.png")
if not is_magician_headgear:
# Remove non-magician headgear
sprite_headgear.texture = null
# 50% chance to add magician headgear
if appearance_rng.randf() < 0.5:
_load_magician_headgear()
else:
# No headgear currently - 50% chance to add magician headgear
if appearance_rng.randf() < 0.5:
_load_magician_headgear()
# Update shield visibility based on loadout
_update_shield_visibility()
# Start in idle state # Start in idle state
ai_state = AIState.IDLE ai_state = AIState.IDLE
state_timer = 2.0 state_timer = 2.0
@@ -547,6 +615,8 @@ func _load_random_equipment():
_load_random_gloves() _load_random_gloves()
# Random headgear (Layer 6 - Headgears) # Random headgear (Layer 6 - Headgears)
# Note: Spell casters will have their headgear adjusted after loadout assignment
# For now, load random headgear (will be replaced if spell caster)
if appearance_rng.randf() < 0.5: # 50% chance to have headgear if appearance_rng.randf() < 0.5: # 50% chance to have headgear
_load_random_headgear() _load_random_headgear()
@@ -649,6 +719,28 @@ func _load_random_gloves():
sprite_armour.hframes = 35 sprite_armour.hframes = 35
sprite_armour.vframes = 8 sprite_armour.vframes = 8
func _load_magician_headgear():
# Load magician headgear for spell casters (Hat, Wizard's Hat, Mage Hat, etc.)
if not sprite_headgear:
return
# Available magician headgears (Basic Mage category)
var magician_headgears = [
"EsperHatBlue.png",
"HighMageHatCyan.png",
"MageHatRed.png",
"SorcererHoodCyan.png"
]
var selected_headgear = magician_headgears[appearance_rng.randi() % magician_headgears.size()]
var headgear_path = "res://assets/gfx/Puny-Characters/Layer 6 - Headgears/Basic Mage/" + selected_headgear
var headgear_texture = load(headgear_path)
if headgear_texture:
sprite_headgear.texture = headgear_texture
sprite_headgear.hframes = 35
sprite_headgear.vframes = 8
func _load_random_headgear(): func _load_random_headgear():
if not sprite_headgear: if not sprite_headgear:
return return
@@ -856,6 +948,68 @@ func _setup_stats():
if collision_shape and collision_shape.shape: if collision_shape and collision_shape.shape:
collision_shape.shape.radius = aggro_range collision_shape.shape.radius = aggro_range
func _assign_loadout():
# Assign bow, bomb, spell, shield, lift/throw — variety, not everyone has everything. Use RNG for consistency.
if not appearance_rng:
return
# Bow: ~22% get bow, 35 arrows
if appearance_rng.randf() < 0.22:
has_bow = true
arrows_left = appearance_rng.randi_range(3, 5)
# Bomb: ~14% get 12 bombs
if appearance_rng.randf() < 0.14:
has_bomb = true
bombs_left = appearance_rng.randi_range(1, 2)
# Spell: ~20% — flames 12%, frost 6%, healing 2%
# Note: Enemies use spell_type strings (not tome items like players)
# "flames" = Tome of Flames spell, "frost" = Tome of Frostspike spell, "healing" = Tome of Healing spell
var spell_roll = appearance_rng.randf()
if spell_roll < 0.12:
spell_type = "flames"
LogManager.log(str(name) + " assigned flames spell (tome spell enemy)", LogManager.CATEGORY_ENEMY)
elif spell_roll < 0.18:
spell_type = "frost"
LogManager.log(str(name) + " assigned frost spell (tome spell enemy)", LogManager.CATEGORY_ENEMY)
elif spell_roll < 0.20:
spell_type = "healing"
LogManager.log(str(name) + " assigned healing spell (tome spell enemy)", LogManager.CATEGORY_ENEMY)
# Shield: ~24% get shield, block chance 0.120.22
if appearance_rng.randf() < 0.24:
has_shield = true
shield_block_chance = 0.12 + appearance_rng.randf() * 0.10
# Lift/throw: ~12% can grab and throw liftable objects when aggro
if appearance_rng.randf() < 0.12:
can_lift_throw = true
func _add_aim_error(dir: Vector2, max_degrees: float) -> Vector2:
if dir.length_squared() < 0.001:
return dir
var deg = randf_range(-max_degrees, max_degrees)
return dir.rotated(deg_to_rad(deg))
func _get_nearby_liftable() -> Node:
var gw = get_tree().get_first_node_in_group("game_world")
if not gw:
return null
var entities = gw.get_node_or_null("Entities")
if not entities:
return null
const MAX_DIST: float = 52.0
var best: Node = null
var best_d: float = MAX_DIST
for c in entities.get_children():
if not c.has_method("can_be_grabbed") or not c.has_method("can_be_lifted"):
continue
if not c.can_be_grabbed() or not c.can_be_lifted():
continue
if "is_broken" in c and c.is_broken:
continue
var d = global_position.distance_to(c.global_position)
if d < best_d:
best_d = d
best = c
return best
func _physics_process(delta): func _physics_process(delta):
# Always update animation (even when dead, for death animation) # Always update animation (even when dead, for death animation)
_update_animation(delta) _update_animation(delta)
@@ -881,9 +1035,29 @@ func _physics_process(delta):
if attack_timer > 0: if attack_timer > 0:
attack_timer -= delta attack_timer -= delta
if attack_timer <= 0: if attack_timer <= 0:
# Attack cooldown finished - reset attack flags
can_attack = true can_attack = true
is_attacking = false is_attacking = false
if spell_cooldown_timer > 0:
spell_cooldown_timer -= delta
if bomb_cooldown_timer > 0:
bomb_cooldown_timer -= delta
if lift_throw_cooldown_timer > 0:
lift_throw_cooldown_timer -= delta
if ranged_decision_timer > 0:
ranged_decision_timer -= delta
if shield_block_timer > 0:
shield_block_timer -= delta
if shield_block_timer <= 0:
is_blocking = false
_update_shield_visibility()
# Update bow charge pulse timer when charging bow
if ai_state == AIState.BOW_CHARGING and is_charging_attack:
bow_charge_tint_pulse_time += delta * bow_charge_tint_pulse_speed_charged
_apply_bow_charge_tint()
elif ai_state != AIState.BOW_CHARGING:
# Clear bow charge tint when not charging
_clear_bow_charge_tint()
# Handle knockback # Handle knockback
if is_knocked_back: if is_knocked_back:
@@ -943,6 +1117,14 @@ func _ai_behavior(delta):
_attacking_behavior(delta) _attacking_behavior(delta)
AIState.GROUPING: AIState.GROUPING:
_grouping_behavior(delta) _grouping_behavior(delta)
AIState.BOW_CHARGING:
_bow_charging_behavior(delta)
AIState.THROWING_BOMB:
_throwing_bomb_behavior(delta)
AIState.CASTING_SPELL:
_casting_spell_behavior(delta)
AIState.LIFTING:
_lifting_behavior(delta)
# Update lost target timer # Update lost target timer
if target_player: if target_player:
@@ -1030,7 +1212,7 @@ func _noticed_behavior(_delta):
ai_state = AIState.CHASING ai_state = AIState.CHASING
state_timer = 5.0 state_timer = 5.0
func _chasing_behavior(_delta): func _chasing_behavior(delta_arg):
if not target_player or not is_instance_valid(target_player) or target_player.is_dead: if not target_player or not is_instance_valid(target_player) or target_player.is_dead:
_hide_alert_indicators() _hide_alert_indicators()
ai_state = AIState.IDLE ai_state = AIState.IDLE
@@ -1039,55 +1221,154 @@ func _chasing_behavior(_delta):
var dist = global_position.distance_to(target_player.global_position) var dist = global_position.distance_to(target_player.global_position)
# Check if player left aggro range (handled by Area2D and timer in _ai_behavior)
# But still check if player is in vision for immediate reaction
if dist > aggro_range: if dist > aggro_range:
# Player left aggro range - timer will handle forgetting
# Don't immediately switch state, let timer handle it
pass pass
# Check if player is still in vision
if not _is_player_in_vision(target_player): if not _is_player_in_vision(target_player):
# Lost sight of player - go back to patrolling
ai_state = AIState.WANDERING ai_state = AIState.WANDERING
state_timer = 2.0 state_timer = 2.0
return return
# Calculate direction to player
var to_player = (target_player.global_position - global_position).normalized() var to_player = (target_player.global_position - global_position).normalized()
# Attack if close enough (start charging attack) # --- Shield blocking: raise shield when player is close and attacking ---
if has_shield and shield_block_chance > 0:
# Check if player is attacking (recently attacked or in melee range)
var player_is_attacking = false
if dist < 60.0: # Close enough that player might attack
# Check if player is facing us and might be attacking
if "is_attacking" in target_player and target_player.is_attacking:
player_is_attacking = true
# Also raise shield if player is very close (within attack range)
if dist < 50.0:
player_is_attacking = true
# Raise shield if player is attacking or very close
if player_is_attacking and not is_blocking:
is_blocking = true
shield_block_timer = shield_block_duration
_update_shield_visibility()
# Play shield activation sound
if sfx_activate_shield:
sfx_activate_shield.play()
elif not player_is_attacking and is_blocking and shield_block_timer <= 0:
# Lower shield if player is not attacking and timer expired
is_blocking = false
_update_shield_visibility()
# --- Lift/throw: try grab nearby liftable when aggro ---
if can_lift_throw and lift_throw_cooldown_timer <= 0:
var liftable = _get_nearby_liftable()
if liftable:
ai_state = AIState.LIFTING
state_timer = 2.0
velocity = Vector2.ZERO
current_direction = _get_direction_from_vector((liftable.global_position - global_position).normalized())
set_meta("_lift_target", liftable)
return
# --- Ranged (bow / bomb / spell): roll periodically, poor aim / mistakes ---
ranged_decision_timer -= delta_arg
if ranged_decision_timer <= 0:
ranged_decision_timer = 0.4
# 10% mistake: skip ranged even when we would use it
if randf() < 0.10:
pass
elif has_bow and arrows_left > 0 and can_attack and dist >= 72 and dist <= 190:
if randf() < 0.18:
ai_state = AIState.BOW_CHARGING
is_charging_attack = true
attack_charge_time = base_attack_charge_time * 1.2
velocity = Vector2.ZERO
current_direction = _get_direction_from_vector(to_player)
bow_charge_tint_pulse_time = 0.0 # Reset pulse timer
return
elif has_bomb and bombs_left > 0 and bomb_cooldown_timer <= 0 and dist >= 48 and dist <= 130:
if randf() < 0.12:
ai_state = AIState.THROWING_BOMB
state_timer = 2.0 # Increased timer: 1.0s to hold bomb visible, 1.0s for throw animation
velocity = Vector2.ZERO
current_direction = _get_direction_from_vector(to_player)
# Create bomb object above enemy's head
_create_held_bomb_object()
return
elif spell_type != "" and spell_cooldown_timer <= 0 and dist >= 56 and dist <= 145:
if randf() < 0.10:
ai_state = AIState.CASTING_SPELL
state_timer = 1.4
velocity = Vector2.ZERO
current_direction = _get_direction_from_vector(to_player)
return
# --- Melee: close enough to attack ---
if dist < 45.0 and can_attack and not is_charging_attack: if dist < 45.0 and can_attack and not is_charging_attack:
# Lower shield when attacking (can't block while attacking)
if is_blocking:
is_blocking = false
shield_block_timer = 0.0
_update_shield_visibility()
ai_state = AIState.ATTACKING ai_state = AIState.ATTACKING
is_charging_attack = true is_charging_attack = true
attack_charge_time = base_attack_charge_time attack_charge_time = base_attack_charge_time
velocity = Vector2.ZERO # Stop moving velocity = Vector2.ZERO
current_direction = _get_direction_from_vector(to_player) # Face player current_direction = _get_direction_from_vector(to_player)
return return
# Chase player (get close enough to attack) var desired_distance = 45.0
var desired_distance = 45.0 # Stop this far from player (attack range)
# Apply speed multiplier if blocking
var speed_mult = 1.0
if is_blocking:
speed_mult = shield_block_speed_multiplier
if dist > desired_distance: if dist > desired_distance:
# Still too far - chase player # Still too far - chase player
velocity = to_player * move_speed * 0.8 # Reduce chase speed to 80% (was 100%) velocity = to_player * move_speed * 0.8 * speed_mult # Apply blocking speed reduction
else: else:
# Close enough to attack - but only stop if we can attack soon # Close enough to attack - but only stop if we can attack soon
# If attack is on cooldown, keep following at reduced speed to maintain distance # If attack is on cooldown, keep following at reduced speed to maintain distance
if can_attack: if can_attack:
# Can attack - stop and wait for attack opportunity # Can attack - stop and wait for attack opportunity (unless blocking, then move slowly)
if is_blocking:
# When blocking, move slowly to maintain position
velocity = to_player * move_speed * 0.2 * speed_mult
else:
velocity = Vector2.ZERO velocity = Vector2.ZERO
else: else:
# Attack on cooldown - keep moving slowly to maintain position # Attack on cooldown - keep moving slowly to maintain position
# Move slightly away if too close, or maintain distance # Move slightly away if too close, or maintain distance
if dist < desired_distance * 0.8: if dist < desired_distance * 0.8:
# Too close - back away slightly # Too close - back away slightly
velocity = -to_player * move_speed * 0.3 velocity = -to_player * move_speed * 0.3 * speed_mult
else:
# Good distance - just face player (or move slowly if blocking)
if is_blocking:
velocity = to_player * move_speed * 0.2 * speed_mult
else: else:
# Good distance - just face player
velocity = Vector2.ZERO velocity = Vector2.ZERO
current_direction = _get_direction_from_vector(to_player) current_direction = _get_direction_from_vector(to_player)
# Set animation based on movement # Set animation based on movement and whether holding something
var is_holding_object = (held_bomb_object != null and is_instance_valid(held_bomb_object))
# When blocking, use hold animations (shield is held up)
if is_blocking:
if velocity.length() > 0.1:
if current_animation != "RUN_HOLD" and current_animation != "SWORD" and current_animation != "DAMAGE" and current_animation != "THROW":
_set_animation("RUN_HOLD")
else:
if current_animation != "IDLE_HOLD" and current_animation != "SWORD" and current_animation != "DAMAGE" and current_animation != "THROW":
_set_animation("IDLE_HOLD")
elif is_holding_object:
# Holding bomb or object - use hold animations
if velocity.length() > 0.1:
if current_animation != "RUN_HOLD" and current_animation != "SWORD" and current_animation != "DAMAGE" and current_animation != "THROW":
_set_animation("RUN_HOLD")
else:
if current_animation != "IDLE_HOLD" and current_animation != "SWORD" and current_animation != "DAMAGE" and current_animation != "THROW":
_set_animation("IDLE_HOLD")
else:
# Not holding anything - use normal animations
if velocity.length() > 0.1: if velocity.length() > 0.1:
if current_animation != "RUN" and current_animation != "SWORD" and current_animation != "DAMAGE": if current_animation != "RUN" and current_animation != "SWORD" and current_animation != "DAMAGE":
_set_animation("RUN") _set_animation("RUN")
@@ -1167,6 +1448,287 @@ func _grouping_behavior(_delta):
ai_state = AIState.IDLE ai_state = AIState.IDLE
state_timer = 2.0 state_timer = 2.0
func _get_attack_direction_vector() -> Vector2:
match current_direction:
Direction.RIGHT: return Vector2.RIGHT
Direction.DOWN_RIGHT: return Vector2(1, 1).normalized()
Direction.DOWN: return Vector2.DOWN
Direction.DOWN_LEFT: return Vector2(-1, 1).normalized()
Direction.LEFT: return Vector2.LEFT
Direction.UP_LEFT: return Vector2(-1, -1).normalized()
Direction.UP: return Vector2.UP
Direction.UP_RIGHT: return Vector2(1, -1).normalized()
return Vector2.DOWN
func _bow_charging_behavior(delta):
velocity = Vector2.ZERO
if not target_player or not is_instance_valid(target_player) or target_player.is_dead:
ai_state = AIState.CHASING
state_timer = 2.0
return
var to_player = (target_player.global_position - global_position).normalized()
current_direction = _get_direction_from_vector(to_player)
if is_charging_attack:
attack_charge_time -= delta
if attack_charge_time <= 0:
is_charging_attack = false
_perform_bow_attack()
else:
if current_animation != "BOW" and current_animation != "DAMAGE":
_set_animation("BOW")
return
ai_state = AIState.CHASING
state_timer = 2.0
func _perform_bow_attack():
if not is_multiplayer_authority():
return
if arrows_left <= 0 or not attack_arrow_scene:
ai_state = AIState.CHASING
_clear_bow_charge_tint()
return
# Fire multiple arrows in a volley (2-3 arrows)
var num_arrows = randi_range(2, 3)
num_arrows = min(num_arrows, arrows_left) # Don't fire more than available
var base_dir = _get_attack_direction_vector()
var charge_pct = 0.65 + randf() * 0.25
# Fire arrows with slight spread
for i in range(num_arrows):
var dir = base_dir
# Add spread to arrows (cone pattern)
var spread_angle = deg_to_rad(randf_range(-12.0, 12.0)) # ±12 degrees spread
dir = dir.rotated(spread_angle)
# Add additional aim error
dir = _add_aim_error(dir, randf_range(16.0, 28.0))
# Slight delay between arrows for visual effect
if i > 0:
await get_tree().create_timer(0.08 * i).timeout
var arr = attack_arrow_scene.instantiate()
var par = get_parent()
if par:
par.add_child(arr)
arr.shoot(dir, global_position, self, charge_pct)
arrows_left -= num_arrows
can_attack = false
attack_timer = attack_cooldown * 1.4
state_timer = attack_cooldown * 1.2
_set_animation("BOW")
_clear_bow_charge_tint()
ai_state = AIState.CHASING
func _throwing_bomb_behavior(delta):
state_timer -= delta
velocity = Vector2.ZERO
if not target_player or not is_instance_valid(target_player) or target_player.is_dead:
# Clean up held bomb if target is lost
_cleanup_held_bomb()
ai_state = AIState.CHASING
state_timer = 2.0
return
# Update held bomb position above enemy's head
if held_bomb_object and is_instance_valid(held_bomb_object):
# Position bomb above enemy's head (offset upward)
var head_offset = Vector2(0, -20) # Above the head
held_bomb_object.global_position = global_position + head_offset
# Make sure bomb is visible and on top
if held_bomb_object.has_node("Sprite2D"):
held_bomb_object.get_node("Sprite2D").visible = true
held_bomb_object.z_index = 10 # Above enemy sprites
# Face the player while holding bomb
current_direction = _get_direction_from_vector((target_player.global_position - global_position).normalized())
# First phase: Hold bomb above head (show it for at least 1.0 second)
if state_timer > 1.0:
# Use IDLE_HOLD animation while holding bomb (enemy is stationary)
if current_animation != "IDLE_HOLD" and current_animation != "DAMAGE":
_set_animation("IDLE_HOLD")
return
# Second phase: Throw the bomb (after 1.0 second of holding)
if state_timer <= 1.0 and state_timer > 0.0:
# Use THROW animation when actually throwing
if current_animation != "THROW" and current_animation != "DAMAGE":
_set_animation("THROW")
return
# Third phase: Actually throw the bomb (when timer reaches 0)
if state_timer <= 0.0 and bombs_left > 0 and is_multiplayer_authority():
_throw_held_bomb()
# Return to chasing after throw
ai_state = AIState.CHASING
state_timer = 2.0
func _create_held_bomb_object():
# Create a bomb object that will be held above enemy's head before throwing
if not is_multiplayer_authority():
return
if not interactable_object_scene:
push_error("ERROR: Could not load interactable_object scene!")
return
# Clean up any existing held bomb
_cleanup_held_bomb()
# Spawn bomb object above enemy's head
var entities_node = get_parent()
if not entities_node:
entities_node = get_tree().get_first_node_in_group("game_world").get_node_or_null("Entities")
if not entities_node:
push_error("ERROR: Could not find Entities node!")
return
var bomb_obj = interactable_object_scene.instantiate()
bomb_obj.name = "EnemyHeldBomb_" + str(Time.get_ticks_msec())
bomb_obj.global_position = global_position + Vector2(0, -20) # Above head
bomb_obj.z_index = 10 # Above enemy sprites
# Set multiplayer authority
if multiplayer.has_multiplayer_peer():
bomb_obj.set_multiplayer_authority(get_multiplayer_authority())
entities_node.add_child(bomb_obj)
# Setup as bomb object
bomb_obj.setup_bomb()
# Disable collision so it doesn't interfere
bomb_obj.set_collision_layer_value(2, false)
bomb_obj.set_collision_mask_value(1, false)
bomb_obj.set_collision_mask_value(2, false)
bomb_obj.set_collision_mask_value(7, true) # Keep wall collision
# Make sure sprite is visible
if bomb_obj.has_node("Sprite2D"):
bomb_obj.get_node("Sprite2D").visible = true
held_bomb_object = bomb_obj
print(name, " created held bomb object above head")
func _throw_held_bomb():
# Throw the bomb that's been held above enemy's head
if not held_bomb_object or not is_instance_valid(held_bomb_object):
# Fallback: create bomb directly if held bomb is missing
if bombs_left > 0 and attack_bomb_scene:
bombs_left -= 1
bomb_cooldown_timer = 8.0
var fallback_offset = Vector2(randf_range(-22, 22), randf_range(-22, 22))
var fallback_target_pos = target_player.global_position + fallback_offset
var fallback_throw_dir = (fallback_target_pos - global_position).normalized()
var fallback_throw_force = fallback_throw_dir * randf_range(180, 260)
var bomb = attack_bomb_scene.instantiate()
var par = get_parent()
if par:
par.add_child(bomb)
bomb.global_position = global_position + Vector2(0, -20) # From above head
bomb.setup(bomb.global_position, self, fallback_throw_force, true)
return
if bombs_left <= 0:
_cleanup_held_bomb()
return
bombs_left -= 1
bomb_cooldown_timer = 8.0
# Calculate throw direction and force
var throw_offset = Vector2(randf_range(-22, 22), randf_range(-22, 22))
var throw_target_pos = target_player.global_position + throw_offset
var throw_dir = (throw_target_pos - global_position).normalized()
var throw_force = throw_dir * randf_range(180, 260)
# Convert held bomb object to thrown bomb projectile
held_bomb_object._convert_to_bomb_projectile(self, throw_force)
# Clear reference (object will be freed by _convert_to_bomb_projectile)
held_bomb_object = null
print(name, " threw bomb from above head!")
func _cleanup_held_bomb():
# Clean up held bomb object if it exists
if held_bomb_object and is_instance_valid(held_bomb_object):
held_bomb_object.queue_free()
held_bomb_object = null
func _casting_spell_behavior(delta):
state_timer -= delta
velocity = Vector2.ZERO
if not target_player or not is_instance_valid(target_player) or target_player.is_dead:
ai_state = AIState.CHASING
return
if state_timer > 0.8:
current_direction = _get_direction_from_vector((target_player.global_position - global_position).normalized())
if current_animation != "CONJURE" and current_animation != "DAMAGE":
_set_animation("CONJURE")
return
if state_timer <= 0.8 and is_multiplayer_authority():
spell_cooldown_timer = 7.0
var gw = get_tree().get_first_node_in_group("game_world")
var base_target = target_player.global_position
var offset = Vector2(randf_range(-20, 20), randf_range(-20, 20))
var desired = base_target + offset
var valid = Vector2.ZERO
if gw and gw.has_method("_get_valid_spell_target_for_enemy"):
valid = gw._get_valid_spell_target_for_enemy(global_position, desired, get_rid())
if valid == Vector2.ZERO:
valid = global_position + (desired - global_position).normalized() * 80.0
var dmg = damage * 0.8 + character_stats.baseStats.int * 0.3 if character_stats else damage
if spell_type == "flames" and flame_spell_scene:
var s = flame_spell_scene.instantiate()
var par = get_parent()
if par:
par.add_child(s)
s.setup(valid, self, dmg)
elif spell_type == "frost" and frostspike_spell_scene:
var s = frostspike_spell_scene.instantiate()
var par = get_parent()
if par:
par.add_child(s)
s.setup(valid, self, dmg, false)
elif spell_type == "healing":
pass
ai_state = AIState.CHASING
state_timer = 2.0
func _lifting_behavior(delta):
state_timer -= delta
velocity = Vector2.ZERO
var lift_target = get_meta("_lift_target") if has_meta("_lift_target") else null
if not lift_target or not is_instance_valid(lift_target):
remove_meta("_lift_target")
ai_state = AIState.CHASING
lift_throw_cooldown_timer = 6.0
return
if state_timer > 1.2:
current_direction = _get_direction_from_vector((lift_target.global_position - global_position).normalized())
if current_animation != "LIFT" and current_animation != "DAMAGE":
_set_animation("LIFT")
return
if state_timer <= 1.2 and is_multiplayer_authority():
remove_meta("_lift_target")
lift_throw_cooldown_timer = 10.0
if lift_target.has_method("can_be_grabbed") and lift_target.can_be_grabbed() and lift_target.has_method("on_grabbed"):
lift_target.on_grabbed(self)
lift_target.global_position = global_position + Vector2(0, -8)
var to_player = (target_player.global_position - global_position).normalized() if target_player else Vector2.DOWN
to_player = _add_aim_error(to_player, 22.0)
var force = to_player * randf_range(180, 280)
if lift_target.has_method("on_thrown"):
lift_target.on_thrown(self, force)
ai_state = AIState.CHASING
state_timer = 2.0
func _find_group_target(): func _find_group_target():
# Find nearby humanoid enemies to group with # Find nearby humanoid enemies to group with
var humanoids = get_tree().get_nodes_in_group("enemy") var humanoids = get_tree().get_nodes_in_group("enemy")
@@ -1369,6 +1931,10 @@ 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
if sprite_shield:
sprite_shield.frame = frame_index
if sprite_shield_holding:
sprite_shield_holding.frame = frame_index
if sprite_weapon: if sprite_weapon:
sprite_weapon.frame = frame_index sprite_weapon.frame = frame_index
@@ -1443,12 +2009,132 @@ func _update_client_visuals():
var y_offset = - position_z * 0.5 var y_offset = - position_z * 0.5
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair, for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons, sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
sprite_headgear, sprite_weapon]: sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon]:
if sprite_layer: if sprite_layer:
sprite_layer.position.y = y_offset sprite_layer.position.y = y_offset
# Animation is updated in _update_animation which is called every frame # Animation is updated in _update_animation which is called every frame
func _apply_bow_charge_tint():
# Apply pulsing white tint to all sprite layers when charging bow (similar to player)
if ai_state != AIState.BOW_CHARGING or not is_charging_attack:
return
var sprite_layers = [
{"sprite": sprite_body, "name": "body"},
{"sprite": sprite_boots, "name": "boots"},
{"sprite": sprite_armour, "name": "armour"},
{"sprite": sprite_facial_hair, "name": "facial_hair"},
{"sprite": sprite_hair, "name": "hair"},
{"sprite": sprite_eyes, "name": "eyes"},
{"sprite": sprite_eyelashes, "name": "eyelashes"},
{"sprite": sprite_addons, "name": "addons"},
{"sprite": sprite_headgear, "name": "headgear"},
{"sprite": sprite_shield, "name": "shield"},
{"sprite": sprite_shield_holding, "name": "shield_holding"},
{"sprite": sprite_weapon, "name": "weapon"}
]
# Calculate pulse value (0.0 to 1.0) using sine wave
var pulse_value = (sin(bow_charge_tint_pulse_time) + 1.0) / 2.0 # 0.0 to 1.0
for sprite_data in sprite_layers:
var sprite_layer = sprite_data.sprite
var sprite_name = sprite_data.name
if not sprite_layer or not is_instance_valid(sprite_layer):
continue
if sprite_layer.material and sprite_layer.material is ShaderMaterial:
var shader_material = sprite_layer.material as ShaderMaterial
# Store original tint if not already stored
var tint_key = str(get_instance_id()) + "_bow_" + sprite_name
if not tint_key in original_sprite_tints:
var original_tint_param = shader_material.get_shader_parameter("tint")
if original_tint_param is Vector4:
original_sprite_tints[tint_key] = Color(original_tint_param.x, original_tint_param.y, original_tint_param.z, original_tint_param.w)
elif original_tint_param is Color:
original_sprite_tints[tint_key] = original_tint_param
else:
original_sprite_tints[tint_key] = Color.WHITE
# Get original tint
var original_tint = original_sprite_tints.get(tint_key, Color.WHITE)
# Calculate fully charged tint (original * bow_charge_tint - white tint)
var full_charged_tint = Color(
original_tint.r * bow_charge_tint.r,
original_tint.g * bow_charge_tint.g,
original_tint.b * bow_charge_tint.b,
original_tint.a * bow_charge_tint.a
)
# Interpolate between original and charged tint based on pulse
var current_tint = original_tint.lerp(full_charged_tint, pulse_value * 0.5) # 50% pulse intensity
shader_material.set_shader_parameter("tint", Vector4(current_tint.r, current_tint.g, current_tint.b, current_tint.a))
func _clear_bow_charge_tint():
# Clear bow charge tint from all sprite layers
var sprite_layers = [
{"sprite": sprite_body, "name": "body"},
{"sprite": sprite_boots, "name": "boots"},
{"sprite": sprite_armour, "name": "armour"},
{"sprite": sprite_facial_hair, "name": "facial_hair"},
{"sprite": sprite_hair, "name": "hair"},
{"sprite": sprite_eyes, "name": "eyes"},
{"sprite": sprite_eyelashes, "name": "eyelashes"},
{"sprite": sprite_addons, "name": "addons"},
{"sprite": sprite_headgear, "name": "headgear"},
{"sprite": sprite_shield, "name": "shield"},
{"sprite": sprite_shield_holding, "name": "shield_holding"},
{"sprite": sprite_weapon, "name": "weapon"}
]
var keys_to_remove = []
for sprite_data in sprite_layers:
var sprite_layer = sprite_data.sprite
var sprite_name = sprite_data.name
if not sprite_layer or not is_instance_valid(sprite_layer):
continue
if sprite_layer.material and sprite_layer.material is ShaderMaterial:
var shader_material = sprite_layer.material as ShaderMaterial
var tint_key = str(get_instance_id()) + "_bow_" + sprite_name
# Restore original tint if we stored it
if tint_key in original_sprite_tints:
var original_tint = original_sprite_tints[tint_key]
shader_material.set_shader_parameter("tint", Vector4(original_tint.r, original_tint.g, original_tint.b, original_tint.a))
keys_to_remove.append(tint_key)
# Clear stored tints
for key in keys_to_remove:
original_sprite_tints.erase(key)
# Reset pulse timer
bow_charge_tint_pulse_time = 0.0
func _update_shield_visibility() -> void:
# Update shield sprite visibility based on whether enemy has a shield and is blocking
if not sprite_shield or not sprite_shield_holding:
return
if not has_shield:
sprite_shield.visible = false
sprite_shield_holding.visible = false
return
# Show holding sprite when actively blocking, regular sprite otherwise
if is_blocking:
sprite_shield.visible = false
sprite_shield_holding.visible = true
else:
sprite_shield.visible = true
sprite_shield_holding.visible = false
func _flash_damage(): func _flash_damage():
# Flash all sprite layers red (override base class which uses single sprite) # Flash all sprite layers red (override base class which uses single sprite)
# But don't flash if dead or about to die - just play die animation # But don't flash if dead or about to die - just play die animation
@@ -1457,26 +2143,48 @@ func _flash_damage():
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair, for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons, sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
sprite_headgear, sprite_weapon]: sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon]:
if sprite_layer: if sprite_layer:
var tween = create_tween() var tween = create_tween()
tween.tween_property(sprite_layer, "modulate", Color.RED, 0.1) tween.tween_property(sprite_layer, "modulate", Color.RED, 0.1)
tween.tween_property(sprite_layer, "modulate", Color.WHITE, 0.1) tween.tween_property(sprite_layer, "modulate", Color.WHITE, 0.1)
func take_damage(amount: float, from_position: Vector2, is_critical: bool = false, is_burn_damage: bool = false, apply_burn_debuff: bool = false):
if has_shield and shield_block_chance > 0 and from_position != Vector2.ZERO and not is_burn_damage:
var to_attacker = (from_position - global_position).normalized()
var facing = _get_attack_direction_vector()
# Check if attack is coming from the direction we're facing (blocking direction)
if to_attacker.dot(facing) > 0.5 and randf() < shield_block_chance:
# Successfully blocked - reduce damage
amount = amount * 0.5
# Raise shield if not already blocking
if not is_blocking:
is_blocking = true
shield_block_timer = shield_block_duration
_update_shield_visibility()
if sfx_activate_shield:
sfx_activate_shield.play()
# Play block sound
if sfx_block_with_shield:
sfx_block_with_shield.play()
# Face the attacker
current_direction = _get_direction_from_vector(to_attacker) as Direction
else:
# Attack not blocked, but raise shield anyway if we have one (defensive reaction)
if not is_blocking:
is_blocking = true
shield_block_timer = shield_block_duration * 0.8 # Shorter duration for reactive blocking
_update_shield_visibility()
if sfx_activate_shield:
sfx_activate_shield.play()
super.take_damage(amount, from_position, is_critical, is_burn_damage, apply_burn_debuff)
func _on_take_damage(attacker_position: Vector2 = Vector2.ZERO): func _on_take_damage(attacker_position: Vector2 = Vector2.ZERO):
# CRITICAL: Don't play damage animation if already dead
# This prevents damage sync from overriding death animation on clients
if is_dead: if is_dead:
return return
# Override to play damage animation and face attacker (same as player)
_set_animation("DAMAGE") _set_animation("DAMAGE")
# Face the attacker (if attacker position is provided)
if attacker_position != Vector2.ZERO: if attacker_position != Vector2.ZERO:
# Calculate direction FROM attacker TO victim
var direction_from_attacker = (global_position - attacker_position).normalized() var direction_from_attacker = (global_position - attacker_position).normalized()
# Face the attacker (opposite of direction from attacker)
current_direction = _get_direction_from_vector(-direction_from_attacker) as Direction current_direction = _get_direction_from_vector(-direction_from_attacker) as Direction
func _play_death_animation(): func _play_death_animation():
@@ -1520,7 +2228,7 @@ func _play_death_animation():
fade_tween.set_parallel(true) fade_tween.set_parallel(true)
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair, for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons, sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
sprite_headgear, sprite_weapon, shadow]: sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon, shadow]:
if sprite_layer: if sprite_layer:
fade_tween.tween_property(sprite_layer, "modulate:a", 0.0, 0.5) fade_tween.tween_property(sprite_layer, "modulate:a", 0.0, 0.5)

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ extends CanvasLayer
# Ingame HUD - Displays player health, level, time, and boss health # Ingame HUD - Displays player health, level, time, and boss health
var label_life: Label = null var label_life: Label = null
var texture_progress_bar_hp: TextureProgressBar = null var progress_bar_hp: ProgressBar = null
var label_hp_value: Label = null var label_hp_value: Label = null
var vbox_mp: VBoxContainer = null var vbox_mp: VBoxContainer = null
var progress_bar_mp: ProgressBar = null var progress_bar_mp: ProgressBar = null
@@ -38,7 +38,6 @@ func _ready():
# Find nodes safely (using get_node_or_null to avoid crashes) # Find nodes safely (using get_node_or_null to avoid crashes)
label_life = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerLIFE/LabelLife") label_life = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerLIFE/LabelLife")
texture_progress_bar_hp = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerLIFE/TextureProgressBarHP")
label_keys = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerKeys/LabelKeys") label_keys = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerKeys/LabelKeys")
label_keys_value = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerKeys/HBoxContainer/LabelKeysValue") label_keys_value = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerKeys/HBoxContainer/LabelKeysValue")
label_level = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerLevel/LabelLevel") label_level = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerLevel/LabelLevel")
@@ -232,6 +231,23 @@ func _setup_hp_mp_ui() -> void:
var hbox = get_node_or_null("UpperLeft/HBoxContainer") var hbox = get_node_or_null("UpperLeft/HBoxContainer")
if not life_vbox or not hbox: if not life_vbox or not hbox:
return return
var old_heart_bar = life_vbox.get_node_or_null("TextureProgressBarHP")
if old_heart_bar:
life_vbox.remove_child(old_heart_bar)
old_heart_bar.queue_free()
# HP bar (inventory-style ProgressBar, same as inventory - no hearts)
progress_bar_hp = ProgressBar.new()
progress_bar_hp.custom_minimum_size = Vector2(100, 12)
progress_bar_hp.show_percentage = false
var bg_hp = StyleBoxFlat.new()
bg_hp.bg_color = Color(0.2, 0.2, 0.2, 0.8)
bg_hp.set_border_width_all(1)
bg_hp.border_color = Color(0.4, 0.4, 0.4)
progress_bar_hp.add_theme_stylebox_override("background", bg_hp)
var fill_hp = StyleBoxFlat.new()
fill_hp.bg_color = Color(0.85, 0.2, 0.2)
progress_bar_hp.add_theme_stylebox_override("fill", fill_hp)
life_vbox.add_child(progress_bar_hp)
# HP value label (curr/max, like inventory) # HP value label (curr/max, like inventory)
label_hp_value = Label.new() label_hp_value = Label.new()
label_hp_value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER label_hp_value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
@@ -280,26 +296,21 @@ func _setup_hp_mp_ui() -> void:
hbox.move_child(vbox_mp, 1) hbox.move_child(vbox_mp, 1)
func _update_player_health(): func _update_player_health():
if not local_player or not texture_progress_bar_hp: if not local_player or not progress_bar_hp:
return return
var health = 0 var health = 0.0
var max_health = 100 var max_health = 100.0
# Try to get health from character_stats first (property always exists in player.gd)
if local_player.character_stats: if local_player.character_stats:
health = local_player.character_stats.hp health = local_player.character_stats.hp
max_health = local_player.character_stats.maxhp max_health = local_player.character_stats.maxhp
else: else:
# Fallback to direct properties (these are getters in player.gd, always available)
health = local_player.current_health health = local_player.current_health
max_health = local_player.max_health max_health = local_player.max_health
# Update progress bar progress_bar_hp.max_value = max(1.0, max_health)
texture_progress_bar_hp.max_value = max_health progress_bar_hp.value = health
texture_progress_bar_hp.value = health
# HP value label (like inventory)
if label_hp_value: if label_hp_value:
label_hp_value.text = str(int(health)) + "/" + str(int(max_health)) label_hp_value.text = str(int(health)) + "/" + str(int(max_health))

View File

@@ -84,13 +84,13 @@ var level_up_stat_buttons: Array = [] # Buttons for STR, DEX, INT, END, WIS, LCK
var level_up_stat_container: HBoxContainer = null var level_up_stat_container: HBoxContainer = null
var selected_level_up_stat_index: int = -1 var selected_level_up_stat_index: int = -1
const STAT_DESCRIPTIONS: Dictionary = { const STAT_DESCRIPTIONS: Dictionary = {
"str": "STR: Physical damage, carry capacity.", "str": "STR (Strength): Increases melee and bow damage. Raises carry capacity so you can hold more items before becoming encumbered.",
"dex": "DEX: Dodge, hit chance, move & attack speed.", "dex": "DEX (Dexterity): Improves dodge chance and hit chance. Makes you move and attack faster.",
"int": "INT: Spell damage, mana, sight.", "int": "INT (Intelligence): Boosts spell damage (flames, frost, heal). Increases max mana and vision range.",
"end": "END: Max HP.", "end": "END (Endurance): Increases max HP. Each point raises your maximum health.",
"wis": "WIS: Mana regen, resistances.", "wis": "WIS (Wisdom): Improves mana regeneration and resistances to certain effects.",
"lck": "LCK: Critical hit chance.", "lck": "LCK (Luck): Increases critical hit chance. Critical hits deal bonus damage and partially ignore defense.",
"per": "PER: Trap detection, perception." "per": "PER (Perception): Improves trap detection and perception. Helps you spot hazards and secrets."
} }
# Equipment slot buttons # Equipment slot buttons
@@ -331,6 +331,9 @@ func _setup_level_up_ui() -> void:
btn.text = stat_name.to_upper() btn.text = stat_name.to_upper()
btn.custom_minimum_size = Vector2(32, 24) btn.custom_minimum_size = Vector2(32, 24)
btn.flat = true btn.flat = true
btn.add_theme_color_override("font_color", Color(0.85, 0.85, 0.85))
btn.add_theme_color_override("font_hover_color", Color(0.4, 1.0, 0.5))
btn.add_theme_color_override("font_focus_color", Color(0.4, 1.0, 0.5))
if ResourceLoader.exists("res://assets/fonts/standard_font.png"): if ResourceLoader.exists("res://assets/fonts/standard_font.png"):
var fr = load("res://assets/fonts/standard_font.png") var fr = load("res://assets/fonts/standard_font.png")
if fr: if fr:
@@ -341,6 +344,7 @@ func _setup_level_up_ui() -> void:
btn.mouse_entered.connect(_on_level_up_stat_hover_entered.bind(stat_name)) btn.mouse_entered.connect(_on_level_up_stat_hover_entered.bind(stat_name))
btn.mouse_exited.connect(_on_level_up_stat_hover_exited) btn.mouse_exited.connect(_on_level_up_stat_hover_exited)
btn.gui_input.connect(_on_level_up_stat_gui_input.bind(stat_name, btn)) btn.gui_input.connect(_on_level_up_stat_gui_input.bind(stat_name, btn))
btn.focus_entered.connect(_on_level_up_stat_focus_entered.bind(stat_name))
level_up_stat_container.add_child(btn) level_up_stat_container.add_child(btn)
level_up_stat_buttons.append(btn) level_up_stat_buttons.append(btn)
stats_panel.add_child(level_up_stat_container) stats_panel.add_child(level_up_stat_container)
@@ -349,6 +353,8 @@ func _setup_level_up_ui() -> void:
func _on_level_up_stat_pressed(stat_name: String) -> void: func _on_level_up_stat_pressed(stat_name: String) -> void:
if not local_player or not local_player.character_stats: if not local_player or not local_player.character_stats:
return return
if not _can_use_inventory():
return
if local_player.character_stats.allocate_stat_point(stat_name): if local_player.character_stats.allocate_stat_point(stat_name):
if sfx_armour: if sfx_armour:
sfx_armour.play() sfx_armour.play()
@@ -375,10 +381,23 @@ func _on_level_up_stat_hover_entered(stat_name: String) -> void:
info_label.text = STAT_DESCRIPTIONS[stat_name] info_label.text = STAT_DESCRIPTIONS[stat_name]
func _on_level_up_stat_hover_exited() -> void: func _on_level_up_stat_hover_exited() -> void:
if info_label: if not info_label:
return
var fc = get_viewport().gui_get_focus_owner()
if level_up_stat_container and fc and is_instance_valid(level_up_stat_container) and fc.get_parent() == level_up_stat_container:
var idx = level_up_stat_buttons.find(fc)
if idx >= 0 and idx < CharacterStats.LEVEL_UP_STAT_NAMES.size():
var sn = CharacterStats.LEVEL_UP_STAT_NAMES[idx]
if sn in STAT_DESCRIPTIONS:
info_label.text = STAT_DESCRIPTIONS[sn]
return
_update_info_panel() _update_info_panel()
func _on_level_up_stat_gui_input(event: InputEvent, stat_name: String, btn: Button) -> void: func _on_level_up_stat_focus_entered(stat_name: String) -> void:
if info_label and stat_name in STAT_DESCRIPTIONS:
info_label.text = STAT_DESCRIPTIONS[stat_name]
func _on_level_up_stat_gui_input(event: InputEvent, stat_name: String, _btn: Button) -> void:
if event is InputEventKey and event.pressed and not event.echo: if event is InputEventKey and event.pressed and not event.echo:
if event.keycode == KEY_ENTER or event.keycode == KEY_KP_ENTER or event.keycode == KEY_SPACE: if event.keycode == KEY_ENTER or event.keycode == KEY_KP_ENTER or event.keycode == KEY_SPACE:
_on_level_up_stat_pressed(stat_name) _on_level_up_stat_pressed(stat_name)
@@ -573,6 +592,16 @@ func _has_equipment_in_slot(slot_name: String) -> bool:
return false return false
return local_player.character_stats.equipment[slot_name] != null return local_player.character_stats.equipment[slot_name] != null
func _can_use_inventory() -> bool:
# Block equip/unequip/use/drop/level-up when dead
if not local_player:
return false
if "is_dead" in local_player and local_player.is_dead:
return false
if "is_processing_death" in local_player and local_player.is_processing_death:
return false
return true
func _find_next_filled_equipment_slot(start_index: int, direction: int) -> int: func _find_next_filled_equipment_slot(start_index: int, direction: int) -> int:
# Find next filled equipment slot, or -1 if none found # Find next filled equipment slot, or -1 if none found
var current_index = start_index var current_index = start_index
@@ -1110,11 +1139,22 @@ func _navigate_inventory(direction: String):
if inventory_selection_col > 0: if inventory_selection_col > 0:
inventory_selection_col -= 1 inventory_selection_col -= 1
else: else:
# Wrap to end of previous row
if inventory_selection_row > 0: if inventory_selection_row > 0:
inventory_selection_row -= 1 inventory_selection_row -= 1
var row = inventory_rows_list[inventory_selection_row] var row = inventory_rows_list[inventory_selection_row]
inventory_selection_col = row.get_child_count() - 1 inventory_selection_col = row.get_child_count() - 1
elif local_player and local_player.character_stats and local_player.character_stats.pending_level_up and local_player.character_stats.pending_stat_points > 0 and level_up_stat_buttons.size() > 0:
selected_type = "level_up_stat"
selected_level_up_stat_index = 0
selected_slot = ""
selected_item = null
level_up_stat_buttons[0].call_deferred("grab_focus")
if info_label and CharacterStats.LEVEL_UP_STAT_NAMES.size() > 0:
var sn = CharacterStats.LEVEL_UP_STAT_NAMES[0]
if sn in STAT_DESCRIPTIONS:
info_label.text = STAT_DESCRIPTIONS[sn]
_update_selection_rectangle()
return
"right": "right":
if inventory_selection_row < inventory_rows_list.size(): if inventory_selection_row < inventory_rows_list.size():
var row = inventory_rows_list[inventory_selection_row] var row = inventory_rows_list[inventory_selection_row]
@@ -1182,7 +1222,7 @@ func _navigate_equipment(direction: String):
if next_index >= 0: if next_index >= 0:
equipment_selection_index = next_index equipment_selection_index = next_index
"up": "up":
# Find next filled slot in row above (same column) # Find next filled slot in row above (same column), or go to stats
var current_row: int = floor(equipment_selection_index / 3.0) var current_row: int = floor(equipment_selection_index / 3.0)
var current_col = equipment_selection_index % 3 var current_col = equipment_selection_index % 3
if current_row > 0: if current_row > 0:
@@ -1191,11 +1231,20 @@ func _navigate_equipment(direction: String):
if _has_equipment_in_slot(target_slot): if _has_equipment_in_slot(target_slot):
equipment_selection_index = target_index equipment_selection_index = target_index
else: else:
# Skip to next filled slot in that row
var next_index = _find_next_filled_equipment_slot(target_index - 1, 1) var next_index = _find_next_filled_equipment_slot(target_index - 1, 1)
if next_index >= 0 and next_index < 3: # Make sure it's in row 0 if next_index >= 0 and next_index < 3:
equipment_selection_index = next_index equipment_selection_index = next_index
# Can't go up from equipment (already at top) elif local_player and local_player.character_stats and local_player.character_stats.pending_level_up and local_player.character_stats.pending_stat_points > 0 and level_up_stat_buttons.size() > 0:
selected_type = "level_up_stat"
selected_level_up_stat_index = 0
selected_slot = ""
selected_item = null
level_up_stat_buttons[0].call_deferred("grab_focus")
if info_label and CharacterStats.LEVEL_UP_STAT_NAMES.size() > 0:
var sn = CharacterStats.LEVEL_UP_STAT_NAMES[0]
if sn in STAT_DESCRIPTIONS:
info_label.text = STAT_DESCRIPTIONS[sn]
return
"down": "down":
# Find next filled slot in row below (same column), or move to inventory # Find next filled slot in row below (same column), or move to inventory
var current_row: int = floor(equipment_selection_index / 3.0) var current_row: int = floor(equipment_selection_index / 3.0)
@@ -1274,8 +1323,20 @@ func _navigate_level_up_stats(direction: String) -> void:
var sn = CharacterStats.LEVEL_UP_STAT_NAMES[selected_level_up_stat_index] var sn = CharacterStats.LEVEL_UP_STAT_NAMES[selected_level_up_stat_index]
if sn in STAT_DESCRIPTIONS: if sn in STAT_DESCRIPTIONS:
info_label.text = STAT_DESCRIPTIONS[sn] info_label.text = STAT_DESCRIPTIONS[sn]
"up", "down": "up":
pass pass
"down":
selected_type = "equipment"
selected_level_up_stat_index = -1
var next_index = _find_next_filled_equipment_slot(-1, 1)
if next_index >= 0:
equipment_selection_index = next_index
selected_slot = equipment_slots_list[next_index]
selected_item = local_player.character_stats.equipment[selected_slot] if local_player and local_player.character_stats else null
_update_selection_from_navigation()
_update_selection_rectangle()
_update_info_panel()
return
# Don't call _update_info_panel - we've set stat description above # Don't call _update_info_panel - we've set stat description above
func _on_inventory_item_pressed(item: Item): func _on_inventory_item_pressed(item: Item):
@@ -1361,18 +1422,17 @@ func _input(event):
if not is_open: if not is_open:
return return
# Arrow key navigation # Arrow key navigation (use ui_left/right/up/down so keybindings work)
if event is InputEventKey and event.pressed and not event.echo:
var direction = "" var direction = ""
if event.keycode == KEY_LEFT: var skip_repeat = event is InputEventKey and event.echo
if not skip_repeat and event.is_action_pressed("ui_left"):
direction = "left" direction = "left"
elif event.keycode == KEY_RIGHT: elif not skip_repeat and event.is_action_pressed("ui_right"):
direction = "right" direction = "right"
elif event.keycode == KEY_UP: elif not skip_repeat and event.is_action_pressed("ui_up"):
direction = "up" direction = "up"
elif event.keycode == KEY_DOWN: elif not skip_repeat and event.is_action_pressed("ui_down"):
direction = "down" direction = "down"
if direction != "": if direction != "":
if selected_type == "level_up_stat": if selected_type == "level_up_stat":
_navigate_level_up_stats(direction) _navigate_level_up_stats(direction)
@@ -1408,6 +1468,8 @@ func _input(event):
func _handle_f_key(): func _handle_f_key():
if not local_player or not local_player.character_stats: if not local_player or not local_player.character_stats:
return return
if not _can_use_inventory():
return
var char_stats = local_player.character_stats var char_stats = local_player.character_stats
@@ -1567,6 +1629,8 @@ func _handle_f_key():
func _use_consumable_item(item: Item): func _use_consumable_item(item: Item):
if not local_player or not local_player.character_stats: if not local_player or not local_player.character_stats:
return return
if not _can_use_inventory():
return
var char_stats = local_player.character_stats var char_stats = local_player.character_stats
@@ -1641,6 +1705,8 @@ func _use_consumable_item(item: Item):
func _handle_e_key(): func _handle_e_key():
if not local_player or not local_player.character_stats: if not local_player or not local_player.character_stats:
return return
if not _can_use_inventory():
return
if selected_type != "item" or not selected_item: if selected_type != "item" or not selected_item:
return return

View File

@@ -816,7 +816,7 @@ func _sync_show_floating_text(loot_type_value: int, text: String, color_value: C
# Show floating text on client # Show floating text on client
_show_floating_text(player, text, color_value, 0.5, 0.5, item_texture, sprite_hframes, sprite_vframes, sprite_frame_value) _show_floating_text(player, text, color_value, 0.5, 0.5, item_texture, sprite_hframes, sprite_vframes, sprite_frame_value)
func _show_floating_text(player: Node, text: String, color: Color, show_time: float = 0.5, fade_time: float = 0.5, item_texture: Texture2D = null, sprite_hframes: int = 1, sprite_vframes: int = 1, sprite_frame: int = 0, item = null): func _show_floating_text(player: Node, text: String, color: Color, show_time: float = 0.5, fade_time: float = 0.5, item_texture: Texture2D = null, sprite_hframes: int = 1, sprite_vframes: int = 1, sprite_frame: int = 0, item_param = null):
# Create floating text and item graphic above player's head # Create floating text and item graphic above player's head
# Shows for show_time seconds, then fades out over fade_time seconds # Shows for show_time seconds, then fades out over fade_time seconds
var floating_text_scene = preload("res://scenes/floating_text.tscn") var floating_text_scene = preload("res://scenes/floating_text.tscn")
@@ -826,4 +826,4 @@ func _show_floating_text(player: Node, text: String, color: Color, show_time: fl
if parent: if parent:
parent.add_child(floating_text) parent.add_child(floating_text)
floating_text.global_position = Vector2(player.global_position.x, player.global_position.y - 20) floating_text.global_position = Vector2(player.global_position.x, player.global_position.y - 20)
floating_text.setup(text, color, show_time, fade_time, item_texture, sprite_hframes, sprite_vframes, sprite_frame, item) floating_text.setup(text, color, show_time, fade_time, item_texture, sprite_hframes, sprite_vframes, sprite_frame, item_param)

View File

@@ -752,7 +752,7 @@ func _attempt_reconnect():
return return
# Check if we're already connected to Matchbox (host might have reconnected and we're waiting for peer ID) # Check if we're already connected to Matchbox (host might have reconnected and we're waiting for peer ID)
if matchbox_client and matchbox_client.has("is_network_connected") and matchbox_client.is_network_connected: if matchbox_client and ("is_network_connected" in matchbox_client) and matchbox_client.is_network_connected:
log_print("NetworkManager: Already connected to Matchbox, waiting for host to assign peer ID...") log_print("NetworkManager: Already connected to Matchbox, waiting for host to assign peer ID...")
# Cancel reconnection attempt - we're already connected, just waiting for host # Cancel reconnection attempt - we're already connected, just waiting for host
reconnection_attempting = false reconnection_attempting = false

View File

@@ -39,9 +39,10 @@ func _update_size() -> void:
return return
var rect = vp.get_visible_rect() var rect = vp.get_visible_rect()
set_anchors_preset(Control.PRESET_FULL_RECT) set_anchors_preset(Control.PRESET_FULL_RECT)
position = rect.position # Use set_deferred for size-related properties to avoid anchor override warnings
size = rect.size set_deferred("position", rect.position)
custom_minimum_size = rect.size set_deferred("size", rect.size)
set_deferred("custom_minimum_size", rect.size)
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
_indicators.clear() _indicators.clear()

File diff suppressed because it is too large Load Diff

View File

@@ -31,18 +31,24 @@ func _on_body_entered(body: Node2D):
if body and body.is_in_group("player") and not body.is_dead: if body and body.is_in_group("player") and not body.is_dead:
print("Stairs: Player entered stairs! Player: ", body.name) print("Stairs: Player entered stairs! Player: ", body.name)
# Notify the player to show alert and play sound
if body and is_instance_valid(body) and body.has_method("_on_exit_found"):
body._on_exit_found()
# Play stairs sound effect # Play stairs sound effect
if sfx_stairs and sfx_stairs.stream: if sfx_stairs and sfx_stairs.stream:
sfx_stairs.play() sfx_stairs.play()
# Only trigger on server/authority
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer():
print("Stairs: Server detected, calling game_world")
var game_world = get_tree().get_first_node_in_group("game_world") var game_world = get_tree().get_first_node_in_group("game_world")
if game_world: if not game_world:
print("Stairs: Game world found, calling _on_player_reached_stairs") print("Stairs: ERROR - Game world not found!")
return
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer():
print("Stairs: Server detected, calling _on_player_reached_stairs")
game_world._on_player_reached_stairs(body) game_world._on_player_reached_stairs(body)
else: else:
print("Stairs: ERROR - Game world not found!") # Client: notify server so level complete triggers (host may not detect joiner via sync)
else: var peer_id = body.get_multiplayer_authority() if body.has_method("get_multiplayer_authority") else 0
print("Stairs: Not server, ignoring") if peer_id > 0 and game_world.has_method("_request_player_reached_stairs"):
print("Stairs: Client notifying server that player ", body.name, " (peer ", peer_id, ") reached stairs")
game_world._request_player_reached_stairs.rpc_id(1, peer_id)

View File

@@ -139,6 +139,10 @@ func _detect_trap(detecting_player: Node) -> void:
# Make trap visible # Make trap visible
sprite.modulate.a = 1.0 sprite.modulate.a = 1.0
# Notify the detecting player to show alert and play sound
if detecting_player and is_instance_valid(detecting_player) and detecting_player.has_method("_on_trap_detected"):
detecting_player._on_trap_detected()
# Sync detection to all clients (including server with call_local) # Sync detection to all clients (including server with call_local)
# CRITICAL: Validate trap is still valid before sending RPC # CRITICAL: Validate trap is still valid before sending RPC
# Use GameWorld RPC to avoid node path issues # Use GameWorld RPC to avoid node path issues
@@ -260,15 +264,18 @@ func _complete_disarm() -> void:
# Change trap visual to show it's disarmed (optional - could fade out or change color) # 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) sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
# Sync disarm to all clients # Sync disarm to all clients (including host when joiner disarms)
# CRITICAL: Validate trap is still valid before sending RPC
# Use GameWorld RPC to avoid node path issues
if multiplayer.has_multiplayer_peer() and is_inside_tree() and is_instance_valid(self): if multiplayer.has_multiplayer_peer() and is_inside_tree() and is_instance_valid(self):
if multiplayer.is_server():
# Use GameWorld RPC with trap name instead of path
var game_world = get_tree().get_first_node_in_group("game_world") var game_world = get_tree().get_first_node_in_group("game_world")
if game_world and game_world.has_method("_sync_trap_state_by_name"): if game_world:
if multiplayer.is_server():
# Host disarmed: broadcast to clients
if game_world.has_method("_sync_trap_state_by_name"):
game_world._sync_trap_state_by_name.rpc(name, true, true) # detected=true, disarmed=true game_world._sync_trap_state_by_name.rpc(name, true, true) # detected=true, disarmed=true
else:
# Joiner disarmed: request host to apply locally and broadcast to all
if game_world.has_method("_request_trap_disarm"):
game_world._request_trap_disarm.rpc_id(1, name)
print("Trap disarmed!") print("Trap disarmed!")