try to fix some stuff
@@ -12,8 +12,8 @@ dest_files=["res://.godot/imported/Gelhein - Evil.mp3-bc2ead9945dee5ecfc0f8aff80
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
loop=true
|
||||
loop_offset=0.0
|
||||
bpm=0.0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
|
||||
BIN
src/assets/audio/sfx/level_complete_03.wav
Normal file
24
src/assets/audio/sfx/level_complete_03.wav.import
Normal 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
|
||||
BIN
src/assets/audio/sfx/level_fail_02.wav
Normal file
24
src/assets/audio/sfx/level_fail_02.wav.import
Normal 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
|
||||
BIN
src/assets/audio/sfx/player/notice/aha.mp3
Normal file
19
src/assets/audio/sfx/player/notice/aha.mp3.import
Normal 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
|
||||
BIN
src/assets/audio/sfx/player/notice/lookout.mp3
Normal file
19
src/assets/audio/sfx/player/notice/lookout.mp3.import
Normal 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
|
||||
BIN
src/assets/audio/sfx/player/notice/whatdowehavehere.mp3
Normal 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
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 6.6 KiB |
@@ -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://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="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"]
|
||||
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_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]
|
||||
collision_layer = 2
|
||||
collision_mask = 65
|
||||
@@ -278,12 +328,28 @@ material = SubResource("ShaderMaterial_i1636")
|
||||
hframes = 35
|
||||
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]
|
||||
y_sort_enabled = true
|
||||
texture = ExtResource("4")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Incantation" parent="." instance=ExtResource("16_inc")]
|
||||
|
||||
[node name="AlertIndicator" type="Sprite2D" parent="." unique_id=1697001148]
|
||||
visible = false
|
||||
z_index = 100
|
||||
@@ -330,3 +396,48 @@ attenuation = 8.57418
|
||||
max_polyphony = 4
|
||||
panning_strength = 1.04
|
||||
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"
|
||||
|
||||
@@ -108,16 +108,16 @@ y_sort_enabled = true
|
||||
script = ExtResource("5")
|
||||
|
||||
[node name="CanvasModulate" type="CanvasModulate" parent="." unique_id=948490815]
|
||||
visible = false
|
||||
light_mask = 1048575
|
||||
visibility_layer = 1048575
|
||||
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")
|
||||
volume_db = -3.085
|
||||
volume_db = -20.411
|
||||
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")
|
||||
autoplay = true
|
||||
volume_db = -15.864
|
||||
|
||||
159
src/scenes/incantation.tscn
Normal 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"
|
||||
@@ -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://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://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"]
|
||||
colors = PackedColorArray(0, 0, 0, 1, 1, 0.13732082, 0.092538536, 1)
|
||||
@@ -563,6 +566,12 @@ _data = {
|
||||
&"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]
|
||||
collision_mask = 67
|
||||
motion_mode = 1
|
||||
@@ -740,8 +749,9 @@ panning_strength = 1.04
|
||||
[node name="SfxLift" type="AudioStreamPlayer2D" parent="." unique_id=1261167113]
|
||||
stream = ExtResource("28_pf23h")
|
||||
max_distance = 1246.0
|
||||
attenuation = 6.964403
|
||||
attenuation = 1.9999994
|
||||
panning_strength = 1.11
|
||||
bus = &"Sfx"
|
||||
|
||||
[node name="DirectionalLight2D" type="DirectionalLight2D" parent="." unique_id=1013099358]
|
||||
visible = false
|
||||
@@ -802,6 +812,11 @@ bus = &"Sfx"
|
||||
stream = ExtResource("45_g5jhy")
|
||||
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]
|
||||
position = Vector2(0, -10)
|
||||
texture = ExtResource("37_hax0n")
|
||||
@@ -825,3 +840,22 @@ volume_db = -13.255
|
||||
attenuation = 3.2490087
|
||||
panning_strength = 1.12
|
||||
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
|
||||
|
||||
@@ -424,24 +424,24 @@ func _spawn_explosion_tile_particles():
|
||||
# Direction from explosion center to this tile (outward) – particles fly away from bomb
|
||||
var to_tile = world - center
|
||||
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)
|
||||
for i in [0, 2]:
|
||||
var p = _explosion_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
|
||||
spr.region_rect = regions[i]
|
||||
p.global_position = world
|
||||
var speed = randf_range(280.0, 420.0) # Much faster - fly around more
|
||||
var d = outward + Vector2(randf_range(-0.4, 0.4), randf_range(-0.4, 0.4))
|
||||
p.velocity = d.normalized() * speed
|
||||
p.angular_velocity = randf_range(-14.0, 14.0)
|
||||
p.position_z = 0.0
|
||||
p.velocity_z = randf_range(100.0, 180.0) # Upward burst, then gravity brings them down
|
||||
parent.add_child(p)
|
||||
# Reduced particles: 1 piece per tile instead of 2 (use index 0)
|
||||
var i = 0
|
||||
var p = _explosion_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
|
||||
spr.region_rect = regions[i]
|
||||
p.global_position = world
|
||||
var speed = randf_range(280.0, 420.0) # Much faster - fly around more
|
||||
var d = outward + Vector2(randf_range(-0.4, 0.4), randf_range(-0.4, 0.4))
|
||||
p.velocity = d.normalized() * speed
|
||||
p.angular_velocity = randf_range(-14.0, 14.0)
|
||||
p.position_z = 0.0
|
||||
p.velocity_z = randf_range(100.0, 180.0) # Upward burst, then gravity brings them down
|
||||
parent.add_child(p)
|
||||
|
||||
func _cause_screenshake():
|
||||
# Calculate screenshake based on distance from local players
|
||||
|
||||
@@ -1345,4 +1345,14 @@ func _sync_door_close():
|
||||
is_closing = true
|
||||
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)
|
||||
|
||||
@@ -281,7 +281,19 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
|
||||
if stairs_data.is_empty():
|
||||
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 = []
|
||||
for room in all_rooms:
|
||||
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,
|
||||
"traps": all_traps,
|
||||
"stairs": stairs_data,
|
||||
"entrance": entrance_data,
|
||||
"blocking_doors": blocking_doors,
|
||||
"grid": 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_humanoid.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)
|
||||
@@ -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)
|
||||
elif enemy_type.ends_with("rat.tscn"):
|
||||
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:
|
||||
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
|
||||
|
||||
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:
|
||||
# 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
|
||||
@@ -2093,13 +2118,20 @@ func _is_valid_interactable_position(world_pos: Vector2, all_doors: Array, all_e
|
||||
|
||||
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
|
||||
# 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 visited = [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:
|
||||
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):
|
||||
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:
|
||||
if d == door:
|
||||
continue # Skip the blocked door
|
||||
# Skip if this door is in the blocking set
|
||||
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
|
||||
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)
|
||||
|
||||
# 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:
|
||||
# Determine 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 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)
|
||||
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)
|
||||
var key_room = null
|
||||
|
||||
@@ -458,6 +458,16 @@ func rpc_take_damage(amount: float, from_position: Vector2, is_critical: bool =
|
||||
if is_multiplayer_authority():
|
||||
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):
|
||||
# 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
|
||||
|
||||
@@ -11,20 +11,27 @@ var players_in_interest: Array[Node] = []
|
||||
var grabbed_player: Node = null
|
||||
var random_move_dir: Vector2 = Vector2.ZERO
|
||||
var random_move_timer: float = 0.0
|
||||
var grab_cooldown_timer: float = 0.0
|
||||
const RANDOM_MOVE_INTERVAL: float = 1.2
|
||||
const SNATCH_DURATION: float = 0.4
|
||||
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 grab_area: Area2D = $GrabPlayerArea
|
||||
@onready var interest_area: Area2D = $PlayerInterestArea
|
||||
@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:
|
||||
super._ready()
|
||||
max_health = 25.0
|
||||
current_health = max_health
|
||||
move_speed = 55.0
|
||||
move_speed = 28.0 # Reduced from 55.0 - much slower
|
||||
damage = SNATCH_DAMAGE
|
||||
exp_reward = 8.0
|
||||
collision_layer = 2
|
||||
@@ -72,15 +79,148 @@ func _die() -> void:
|
||||
v.rpc_released_from_enemy_hand.rpc_id(pid)
|
||||
else:
|
||||
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()
|
||||
|
||||
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:
|
||||
# Update grab cooldown timer
|
||||
if grab_cooldown_timer > 0.0:
|
||||
grab_cooldown_timer -= delta
|
||||
|
||||
if state == HandState.HIDDEN or state == HandState.EMERGING:
|
||||
velocity = Vector2.ZERO
|
||||
return
|
||||
if state == HandState.GRABBING:
|
||||
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
|
||||
|
||||
# IDLE: move toward player if in interest, else random
|
||||
@@ -119,6 +259,13 @@ func _on_emerge_area_body_entered(body: Node2D) -> void:
|
||||
modulate.a = 1.0
|
||||
if anim_player and anim_player.has_animation("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:
|
||||
@@ -152,6 +299,8 @@ func _on_grab_player_area_body_entered(body: Node2D) -> void:
|
||||
return
|
||||
if grabbed_player != null:
|
||||
return
|
||||
if grab_cooldown_timer > 0.0:
|
||||
return # Still on cooldown from previous grab
|
||||
if not is_multiplayer_authority():
|
||||
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)
|
||||
|
||||
|
||||
@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:
|
||||
if not is_instance_valid(self):
|
||||
return
|
||||
var victim = grabbed_player
|
||||
grabbed_player = null
|
||||
state = HandState.IDLE
|
||||
grab_cooldown_timer = GRAB_COOLDOWN # Start cooldown after grab
|
||||
if anim_player and anim_player.has_animation("idle"):
|
||||
anim_player.play("idle")
|
||||
if not is_instance_valid(victim):
|
||||
@@ -208,6 +370,78 @@ func _finish_snatch() -> void:
|
||||
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:
|
||||
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
|
||||
|
||||
@@ -25,8 +25,14 @@ enum HumanoidType {
|
||||
@onready var sprite_eyelashes = $Sprite2DEyeLashes
|
||||
@onready var sprite_addons = $Sprite2DAddons
|
||||
@onready var sprite_headgear = $Sprite2DHeadgear
|
||||
@onready var sprite_shield = $Sprite2DShield
|
||||
@onready var sprite_shield_holding = $Sprite2DShieldHolding
|
||||
@onready var sprite_weapon = $Sprite2DWeapon
|
||||
|
||||
# Incantation (spell casting visual)
|
||||
@onready var incantation_sprite = $Incantation/IncantationSprite
|
||||
@onready var animation_incantation = $Incantation/AnimationIncantation
|
||||
|
||||
# Attack system
|
||||
var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn")
|
||||
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 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
|
||||
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 state_timer: float = 0.0
|
||||
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
|
||||
@onready var sfx_die = $SfxDie
|
||||
@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)
|
||||
const ANIMATIONS = {
|
||||
@@ -216,6 +260,30 @@ func _ready():
|
||||
# Undead types (e.g. skeleton) take damage from healing spell
|
||||
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
|
||||
ai_state = AIState.IDLE
|
||||
state_timer = 2.0
|
||||
@@ -547,6 +615,8 @@ func _load_random_equipment():
|
||||
_load_random_gloves()
|
||||
|
||||
# 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
|
||||
_load_random_headgear()
|
||||
|
||||
@@ -649,6 +719,28 @@ func _load_random_gloves():
|
||||
sprite_armour.hframes = 35
|
||||
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():
|
||||
if not sprite_headgear:
|
||||
return
|
||||
@@ -856,6 +948,68 @@ func _setup_stats():
|
||||
if collision_shape and collision_shape.shape:
|
||||
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, 3–5 arrows
|
||||
if appearance_rng.randf() < 0.22:
|
||||
has_bow = true
|
||||
arrows_left = appearance_rng.randi_range(3, 5)
|
||||
# Bomb: ~14% get 1–2 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.12–0.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):
|
||||
# Always update animation (even when dead, for death animation)
|
||||
_update_animation(delta)
|
||||
@@ -881,9 +1035,29 @@ func _physics_process(delta):
|
||||
if attack_timer > 0:
|
||||
attack_timer -= delta
|
||||
if attack_timer <= 0:
|
||||
# Attack cooldown finished - reset attack flags
|
||||
can_attack = true
|
||||
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
|
||||
if is_knocked_back:
|
||||
@@ -943,6 +1117,14 @@ func _ai_behavior(delta):
|
||||
_attacking_behavior(delta)
|
||||
AIState.GROUPING:
|
||||
_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
|
||||
if target_player:
|
||||
@@ -1030,7 +1212,7 @@ func _noticed_behavior(_delta):
|
||||
ai_state = AIState.CHASING
|
||||
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:
|
||||
_hide_alert_indicators()
|
||||
ai_state = AIState.IDLE
|
||||
@@ -1039,61 +1221,160 @@ func _chasing_behavior(_delta):
|
||||
|
||||
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:
|
||||
# Player left aggro range - timer will handle forgetting
|
||||
# Don't immediately switch state, let timer handle it
|
||||
pass
|
||||
|
||||
# Check if player is still in vision
|
||||
if not _is_player_in_vision(target_player):
|
||||
# Lost sight of player - go back to patrolling
|
||||
ai_state = AIState.WANDERING
|
||||
state_timer = 2.0
|
||||
return
|
||||
|
||||
# Calculate direction to player
|
||||
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:
|
||||
# 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
|
||||
is_charging_attack = true
|
||||
attack_charge_time = base_attack_charge_time
|
||||
velocity = Vector2.ZERO # Stop moving
|
||||
current_direction = _get_direction_from_vector(to_player) # Face player
|
||||
velocity = Vector2.ZERO
|
||||
current_direction = _get_direction_from_vector(to_player)
|
||||
return
|
||||
|
||||
# Chase player (get close enough to attack)
|
||||
var desired_distance = 45.0 # Stop this far from player (attack range)
|
||||
var desired_distance = 45.0
|
||||
|
||||
# Apply speed multiplier if blocking
|
||||
var speed_mult = 1.0
|
||||
if is_blocking:
|
||||
speed_mult = shield_block_speed_multiplier
|
||||
|
||||
if dist > desired_distance:
|
||||
# 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:
|
||||
# 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 can_attack:
|
||||
# Can attack - stop and wait for attack opportunity
|
||||
velocity = Vector2.ZERO
|
||||
# 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
|
||||
else:
|
||||
# Attack on cooldown - keep moving slowly to maintain position
|
||||
# Move slightly away if too close, or maintain distance
|
||||
if dist < desired_distance * 0.8:
|
||||
# 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
|
||||
velocity = Vector2.ZERO
|
||||
# Good distance - just face player (or move slowly if blocking)
|
||||
if is_blocking:
|
||||
velocity = to_player * move_speed * 0.2 * speed_mult
|
||||
else:
|
||||
velocity = Vector2.ZERO
|
||||
current_direction = _get_direction_from_vector(to_player)
|
||||
|
||||
# Set animation based on movement
|
||||
if velocity.length() > 0.1:
|
||||
if current_animation != "RUN" and current_animation != "SWORD" and current_animation != "DAMAGE":
|
||||
_set_animation("RUN")
|
||||
# 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:
|
||||
if current_animation != "IDLE" and current_animation != "SWORD" and current_animation != "DAMAGE":
|
||||
_set_animation("IDLE")
|
||||
# Not holding anything - use normal animations
|
||||
if velocity.length() > 0.1:
|
||||
if current_animation != "RUN" and current_animation != "SWORD" and current_animation != "DAMAGE":
|
||||
_set_animation("RUN")
|
||||
else:
|
||||
if current_animation != "IDLE" and current_animation != "SWORD" and current_animation != "DAMAGE":
|
||||
_set_animation("IDLE")
|
||||
|
||||
# Give up chasing after timer (or if player leaves vision)
|
||||
if state_timer <= 0:
|
||||
@@ -1167,6 +1448,287 @@ func _grouping_behavior(_delta):
|
||||
ai_state = AIState.IDLE
|
||||
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():
|
||||
# Find nearby humanoid enemies to group with
|
||||
var humanoids = get_tree().get_nodes_in_group("enemy")
|
||||
@@ -1369,6 +1931,10 @@ func _update_animation(delta):
|
||||
sprite_addons.frame = frame_index
|
||||
if sprite_headgear:
|
||||
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:
|
||||
sprite_weapon.frame = frame_index
|
||||
|
||||
@@ -1443,12 +2009,132 @@ func _update_client_visuals():
|
||||
var y_offset = - position_z * 0.5
|
||||
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
|
||||
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
|
||||
sprite_headgear, sprite_weapon]:
|
||||
sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon]:
|
||||
if sprite_layer:
|
||||
sprite_layer.position.y = y_offset
|
||||
|
||||
# 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():
|
||||
# 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
|
||||
@@ -1457,26 +2143,48 @@ func _flash_damage():
|
||||
|
||||
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
|
||||
sprite_hair, sprite_eyes, sprite_eyelashes, sprite_addons,
|
||||
sprite_headgear, sprite_weapon]:
|
||||
sprite_headgear, sprite_shield, sprite_shield_holding, sprite_weapon]:
|
||||
if sprite_layer:
|
||||
var tween = create_tween()
|
||||
tween.tween_property(sprite_layer, "modulate", Color.RED, 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):
|
||||
# CRITICAL: Don't play damage animation if already dead
|
||||
# This prevents damage sync from overriding death animation on clients
|
||||
if is_dead:
|
||||
return
|
||||
|
||||
# Override to play damage animation and face attacker (same as player)
|
||||
_set_animation("DAMAGE")
|
||||
|
||||
# Face the attacker (if attacker position is provided)
|
||||
if attacker_position != Vector2.ZERO:
|
||||
# Calculate direction FROM attacker TO victim
|
||||
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
|
||||
|
||||
func _play_death_animation():
|
||||
@@ -1520,7 +2228,7 @@ func _play_death_animation():
|
||||
fade_tween.set_parallel(true)
|
||||
for sprite_layer in [sprite_body, sprite_boots, sprite_armour, sprite_facial_hair,
|
||||
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:
|
||||
fade_tween.tween_property(sprite_layer, "modulate:a", 0.0, 0.5)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ extends CanvasLayer
|
||||
# Ingame HUD - Displays player health, level, time, and boss health
|
||||
|
||||
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 vbox_mp: VBoxContainer = null
|
||||
var progress_bar_mp: ProgressBar = null
|
||||
@@ -38,7 +38,6 @@ func _ready():
|
||||
|
||||
# Find nodes safely (using get_node_or_null to avoid crashes)
|
||||
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_value = get_node_or_null("UpperLeft/HBoxContainer/VBoxContainerKeys/HBoxContainer/LabelKeysValue")
|
||||
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")
|
||||
if not life_vbox or not hbox:
|
||||
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)
|
||||
label_hp_value = Label.new()
|
||||
label_hp_value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
@@ -280,26 +296,21 @@ func _setup_hp_mp_ui() -> void:
|
||||
hbox.move_child(vbox_mp, 1)
|
||||
|
||||
func _update_player_health():
|
||||
if not local_player or not texture_progress_bar_hp:
|
||||
if not local_player or not progress_bar_hp:
|
||||
return
|
||||
|
||||
var health = 0
|
||||
var max_health = 100
|
||||
var health = 0.0
|
||||
var max_health = 100.0
|
||||
|
||||
# Try to get health from character_stats first (property always exists in player.gd)
|
||||
if local_player.character_stats:
|
||||
health = local_player.character_stats.hp
|
||||
max_health = local_player.character_stats.maxhp
|
||||
else:
|
||||
# Fallback to direct properties (these are getters in player.gd, always available)
|
||||
health = local_player.current_health
|
||||
max_health = local_player.max_health
|
||||
|
||||
# Update progress bar
|
||||
texture_progress_bar_hp.max_value = max_health
|
||||
texture_progress_bar_hp.value = health
|
||||
|
||||
# HP value label (like inventory)
|
||||
progress_bar_hp.max_value = max(1.0, max_health)
|
||||
progress_bar_hp.value = health
|
||||
if label_hp_value:
|
||||
label_hp_value.text = str(int(health)) + "/" + str(int(max_health))
|
||||
|
||||
|
||||
@@ -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 selected_level_up_stat_index: int = -1
|
||||
const STAT_DESCRIPTIONS: Dictionary = {
|
||||
"str": "STR: Physical damage, carry capacity.",
|
||||
"dex": "DEX: Dodge, hit chance, move & attack speed.",
|
||||
"int": "INT: Spell damage, mana, sight.",
|
||||
"end": "END: Max HP.",
|
||||
"wis": "WIS: Mana regen, resistances.",
|
||||
"lck": "LCK: Critical hit chance.",
|
||||
"per": "PER: Trap detection, perception."
|
||||
"str": "STR (Strength): Increases melee and bow damage. Raises carry capacity so you can hold more items before becoming encumbered.",
|
||||
"dex": "DEX (Dexterity): Improves dodge chance and hit chance. Makes you move and attack faster.",
|
||||
"int": "INT (Intelligence): Boosts spell damage (flames, frost, heal). Increases max mana and vision range.",
|
||||
"end": "END (Endurance): Increases max HP. Each point raises your maximum health.",
|
||||
"wis": "WIS (Wisdom): Improves mana regeneration and resistances to certain effects.",
|
||||
"lck": "LCK (Luck): Increases critical hit chance. Critical hits deal bonus damage and partially ignore defense.",
|
||||
"per": "PER (Perception): Improves trap detection and perception. Helps you spot hazards and secrets."
|
||||
}
|
||||
|
||||
# Equipment slot buttons
|
||||
@@ -331,6 +331,9 @@ func _setup_level_up_ui() -> void:
|
||||
btn.text = stat_name.to_upper()
|
||||
btn.custom_minimum_size = Vector2(32, 24)
|
||||
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"):
|
||||
var fr = load("res://assets/fonts/standard_font.png")
|
||||
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_exited.connect(_on_level_up_stat_hover_exited)
|
||||
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_buttons.append(btn)
|
||||
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:
|
||||
if not local_player or not local_player.character_stats:
|
||||
return
|
||||
if not _can_use_inventory():
|
||||
return
|
||||
if local_player.character_stats.allocate_stat_point(stat_name):
|
||||
if sfx_armour:
|
||||
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]
|
||||
|
||||
func _on_level_up_stat_hover_exited() -> void:
|
||||
if info_label:
|
||||
_update_info_panel()
|
||||
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()
|
||||
|
||||
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.keycode == KEY_ENTER or event.keycode == KEY_KP_ENTER or event.keycode == KEY_SPACE:
|
||||
_on_level_up_stat_pressed(stat_name)
|
||||
@@ -573,6 +592,16 @@ func _has_equipment_in_slot(slot_name: String) -> bool:
|
||||
return false
|
||||
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:
|
||||
# Find next filled equipment slot, or -1 if none found
|
||||
var current_index = start_index
|
||||
@@ -1110,11 +1139,22 @@ func _navigate_inventory(direction: String):
|
||||
if inventory_selection_col > 0:
|
||||
inventory_selection_col -= 1
|
||||
else:
|
||||
# Wrap to end of previous row
|
||||
if inventory_selection_row > 0:
|
||||
inventory_selection_row -= 1
|
||||
var row = inventory_rows_list[inventory_selection_row]
|
||||
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":
|
||||
if inventory_selection_row < inventory_rows_list.size():
|
||||
var row = inventory_rows_list[inventory_selection_row]
|
||||
@@ -1182,7 +1222,7 @@ func _navigate_equipment(direction: String):
|
||||
if next_index >= 0:
|
||||
equipment_selection_index = next_index
|
||||
"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_col = equipment_selection_index % 3
|
||||
if current_row > 0:
|
||||
@@ -1191,11 +1231,20 @@ func _navigate_equipment(direction: String):
|
||||
if _has_equipment_in_slot(target_slot):
|
||||
equipment_selection_index = target_index
|
||||
else:
|
||||
# Skip to next filled slot in that row
|
||||
var next_index = _find_next_filled_equipment_slot(target_index - 1, 1)
|
||||
if next_index >= 0 and next_index < 3: # Make sure it's in row 0
|
||||
if next_index >= 0 and next_index < 3:
|
||||
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":
|
||||
# Find next filled slot in row below (same column), or move to inventory
|
||||
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]
|
||||
if sn in STAT_DESCRIPTIONS:
|
||||
info_label.text = STAT_DESCRIPTIONS[sn]
|
||||
"up", "down":
|
||||
"up":
|
||||
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
|
||||
|
||||
func _on_inventory_item_pressed(item: Item):
|
||||
@@ -1361,27 +1422,26 @@ func _input(event):
|
||||
if not is_open:
|
||||
return
|
||||
|
||||
# Arrow key navigation
|
||||
if event is InputEventKey and event.pressed and not event.echo:
|
||||
var direction = ""
|
||||
if event.keycode == KEY_LEFT:
|
||||
direction = "left"
|
||||
elif event.keycode == KEY_RIGHT:
|
||||
direction = "right"
|
||||
elif event.keycode == KEY_UP:
|
||||
direction = "up"
|
||||
elif event.keycode == KEY_DOWN:
|
||||
direction = "down"
|
||||
|
||||
if direction != "":
|
||||
if selected_type == "level_up_stat":
|
||||
_navigate_level_up_stats(direction)
|
||||
elif selected_type == "equipment":
|
||||
_navigate_equipment(direction)
|
||||
else:
|
||||
_navigate_inventory(direction)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
# Arrow key navigation (use ui_left/right/up/down so keybindings work)
|
||||
var direction = ""
|
||||
var skip_repeat = event is InputEventKey and event.echo
|
||||
if not skip_repeat and event.is_action_pressed("ui_left"):
|
||||
direction = "left"
|
||||
elif not skip_repeat and event.is_action_pressed("ui_right"):
|
||||
direction = "right"
|
||||
elif not skip_repeat and event.is_action_pressed("ui_up"):
|
||||
direction = "up"
|
||||
elif not skip_repeat and event.is_action_pressed("ui_down"):
|
||||
direction = "down"
|
||||
if direction != "":
|
||||
if selected_type == "level_up_stat":
|
||||
_navigate_level_up_stats(direction)
|
||||
elif selected_type == "equipment":
|
||||
_navigate_equipment(direction)
|
||||
else:
|
||||
_navigate_inventory(direction)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
||||
# F key: Unequip/equip items
|
||||
if event is InputEventKey and event.keycode == KEY_F and event.pressed and not event.echo:
|
||||
@@ -1408,6 +1468,8 @@ func _input(event):
|
||||
func _handle_f_key():
|
||||
if not local_player or not local_player.character_stats:
|
||||
return
|
||||
if not _can_use_inventory():
|
||||
return
|
||||
|
||||
var char_stats = local_player.character_stats
|
||||
|
||||
@@ -1567,6 +1629,8 @@ func _handle_f_key():
|
||||
func _use_consumable_item(item: Item):
|
||||
if not local_player or not local_player.character_stats:
|
||||
return
|
||||
if not _can_use_inventory():
|
||||
return
|
||||
|
||||
var char_stats = local_player.character_stats
|
||||
|
||||
@@ -1641,6 +1705,8 @@ func _use_consumable_item(item: Item):
|
||||
func _handle_e_key():
|
||||
if not local_player or not local_player.character_stats:
|
||||
return
|
||||
if not _can_use_inventory():
|
||||
return
|
||||
|
||||
if selected_type != "item" or not selected_item:
|
||||
return
|
||||
|
||||
@@ -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(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
|
||||
# Shows for show_time seconds, then fades out over fade_time seconds
|
||||
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:
|
||||
parent.add_child(floating_text)
|
||||
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)
|
||||
|
||||
@@ -752,7 +752,7 @@ func _attempt_reconnect():
|
||||
return
|
||||
|
||||
# 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...")
|
||||
# Cancel reconnection attempt - we're already connected, just waiting for host
|
||||
reconnection_attempting = false
|
||||
|
||||
@@ -39,9 +39,10 @@ func _update_size() -> void:
|
||||
return
|
||||
var rect = vp.get_visible_rect()
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
position = rect.position
|
||||
size = rect.size
|
||||
custom_minimum_size = rect.size
|
||||
# Use set_deferred for size-related properties to avoid anchor override warnings
|
||||
set_deferred("position", rect.position)
|
||||
set_deferred("size", rect.size)
|
||||
set_deferred("custom_minimum_size", rect.size)
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_indicators.clear()
|
||||
|
||||
@@ -31,18 +31,24 @@ func _on_body_entered(body: Node2D):
|
||||
if body and body.is_in_group("player") and not body.is_dead:
|
||||
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
|
||||
if sfx_stairs and sfx_stairs.stream:
|
||||
sfx_stairs.play()
|
||||
|
||||
# Only trigger on server/authority
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if not game_world:
|
||||
print("Stairs: ERROR - Game world not found!")
|
||||
return
|
||||
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")
|
||||
if game_world:
|
||||
print("Stairs: Game world found, calling _on_player_reached_stairs")
|
||||
game_world._on_player_reached_stairs(body)
|
||||
else:
|
||||
print("Stairs: ERROR - Game world not found!")
|
||||
print("Stairs: Server detected, calling _on_player_reached_stairs")
|
||||
game_world._on_player_reached_stairs(body)
|
||||
else:
|
||||
print("Stairs: Not server, ignoring")
|
||||
# Client: notify server so level complete triggers (host may not detect joiner via sync)
|
||||
var peer_id = body.get_multiplayer_authority() if body.has_method("get_multiplayer_authority") else 0
|
||||
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)
|
||||
|
||||
@@ -139,6 +139,10 @@ func _detect_trap(detecting_player: Node) -> void:
|
||||
# Make trap visible
|
||||
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)
|
||||
# CRITICAL: Validate trap is still valid before sending RPC
|
||||
# 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)
|
||||
sprite.modulate = Color(0.5, 0.5, 0.5, 0.5)
|
||||
|
||||
# Sync disarm to all clients
|
||||
# CRITICAL: Validate trap is still valid before sending RPC
|
||||
# Use GameWorld RPC to avoid node path issues
|
||||
# Sync disarm to all clients (including host when joiner disarms)
|
||||
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")
|
||||
if game_world and game_world.has_method("_sync_trap_state_by_name"):
|
||||
game_world._sync_trap_state_by_name.rpc(name, true, true) # detected=true, disarmed=true
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
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
|
||||
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!")
|
||||
|
||||
|
||||