added fallout tiles, fixed so all enemys can get affected.
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 24 KiB |
@@ -297,12 +297,16 @@ separation = Vector2i(1, 1)
|
|||||||
9:9/0 = 0
|
9:9/0 = 0
|
||||||
10:9/0 = 0
|
10:9/0 = 0
|
||||||
11:9/0 = 0
|
11:9/0 = 0
|
||||||
|
11:9/0/custom_data_0 = 1
|
||||||
12:9/0 = 0
|
12:9/0 = 0
|
||||||
|
12:9/0/custom_data_0 = 1
|
||||||
13:9/0 = 0
|
13:9/0 = 0
|
||||||
14:9/0 = 0
|
14:9/0 = 0
|
||||||
15:9/0 = 0
|
15:9/0 = 0
|
||||||
16:9/0 = 0
|
16:9/0 = 0
|
||||||
|
16:9/0/custom_data_0 = 2
|
||||||
17:9/0 = 0
|
17:9/0 = 0
|
||||||
|
17:9/0/custom_data_0 = 2
|
||||||
18:9/0 = 0
|
18:9/0 = 0
|
||||||
19:9/0 = 0
|
19:9/0 = 0
|
||||||
0:10/0 = 0
|
0:10/0 = 0
|
||||||
@@ -321,9 +325,11 @@ separation = Vector2i(1, 1)
|
|||||||
11:10/0 = 0
|
11:10/0 = 0
|
||||||
12:10/0 = 0
|
12:10/0 = 0
|
||||||
13:10/0 = 0
|
13:10/0 = 0
|
||||||
|
13:10/0/custom_data_0 = -1
|
||||||
14:10/0 = 0
|
14:10/0 = 0
|
||||||
15:10/0 = 0
|
15:10/0 = 0
|
||||||
16:10/0 = 0
|
16:10/0 = 0
|
||||||
|
16:10/0/custom_data_0 = -1
|
||||||
17:10/0 = 0
|
17:10/0 = 0
|
||||||
1:11/0 = 0
|
1:11/0 = 0
|
||||||
2:11/0 = 0
|
2:11/0 = 0
|
||||||
@@ -400,9 +406,11 @@ separation = Vector2i(1, 1)
|
|||||||
11:13/0/custom_data_0 = -1
|
11:13/0/custom_data_0 = -1
|
||||||
12:13/0 = 0
|
12:13/0 = 0
|
||||||
13:13/0 = 0
|
13:13/0 = 0
|
||||||
|
13:13/0/custom_data_0 = -1
|
||||||
14:13/0 = 0
|
14:13/0 = 0
|
||||||
15:13/0 = 0
|
15:13/0 = 0
|
||||||
16:13/0 = 0
|
16:13/0 = 0
|
||||||
|
16:13/0/custom_data_0 = -1
|
||||||
17:13/0 = 0
|
17:13/0 = 0
|
||||||
0:14/0 = 0
|
0:14/0 = 0
|
||||||
0:14/0/physics_layer_0/polygon_0/points = PackedVector2Array(8, 2.66667, 8, 8, -8, 8, -8, 2.66667)
|
0:14/0/physics_layer_0/polygon_0/points = PackedVector2Array(8, 2.66667, 8, 8, -8, 8, -8, 2.66667)
|
||||||
@@ -411,13 +419,10 @@ separation = Vector2i(1, 1)
|
|||||||
1:14/0/physics_layer_0/polygon_0/points = PackedVector2Array(8, 2.66667, 8, 8, -8, 8, -8, 2.66667)
|
1:14/0/physics_layer_0/polygon_0/points = PackedVector2Array(8, 2.66667, 8, 8, -8, 8, -8, 2.66667)
|
||||||
1:14/0/custom_data_0 = 8
|
1:14/0/custom_data_0 = 8
|
||||||
2:14/0 = 0
|
2:14/0 = 0
|
||||||
2:14/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, -2.66667, -8, -2.66667, 8, -8, 8)
|
|
||||||
2:14/0/custom_data_0 = 8
|
2:14/0/custom_data_0 = 8
|
||||||
3:14/0 = 0
|
3:14/0 = 0
|
||||||
3:14/0/physics_layer_0/polygon_0/points = PackedVector2Array(2.66667, -8, 8, -8, 8, 8, 2.66667, 8)
|
|
||||||
3:14/0/custom_data_0 = 8
|
3:14/0/custom_data_0 = 8
|
||||||
4:14/0 = 0
|
4:14/0 = 0
|
||||||
4:14/0/physics_layer_0/polygon_0/points = PackedVector2Array(8, 2.66667, 8, 8, -8, 8, -8, 2.66667)
|
|
||||||
4:14/0/custom_data_0 = 8
|
4:14/0/custom_data_0 = 8
|
||||||
5:14/0 = 0
|
5:14/0 = 0
|
||||||
5:14/0/physics_layer_0/polygon_0/points = PackedVector2Array(8, 2.66667, 8, 8, -8, 8, -8, 2.66667)
|
5:14/0/physics_layer_0/polygon_0/points = PackedVector2Array(8, 2.66667, 8, 8, -8, 8, -8, 2.66667)
|
||||||
@@ -465,6 +470,35 @@ separation = Vector2i(1, 1)
|
|||||||
14:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
14:2/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||||
14:1/0 = 0
|
14:1/0 = 0
|
||||||
14:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
14:1/0/physics_layer_0/polygon_0/points = PackedVector2Array(-8, -8, 8, -8, 8, 8, -8, 8)
|
||||||
|
14:15/0 = 0
|
||||||
|
15:15/0 = 0
|
||||||
|
16:15/0 = 0
|
||||||
|
17:15/0 = 0
|
||||||
|
18:15/0 = 0
|
||||||
|
19:15/0 = 0
|
||||||
|
20:15/0 = 0
|
||||||
|
20:16/0 = 0
|
||||||
|
21:16/0 = 0
|
||||||
|
21:15/0 = 0
|
||||||
|
16:16/0 = 0
|
||||||
|
15:16/0 = 0
|
||||||
|
15:16/0/custom_data_0 = -2
|
||||||
|
14:16/0 = 0
|
||||||
|
13:16/0 = 0
|
||||||
|
12:16/0 = 0
|
||||||
|
11:16/0 = 0
|
||||||
|
10:16/0 = 0
|
||||||
|
9:16/0 = 0
|
||||||
|
7:15/0 = 0
|
||||||
|
1:16/0 = 0
|
||||||
|
0:16/0 = 0
|
||||||
|
2:16/0 = 0
|
||||||
|
3:16/0 = 0
|
||||||
|
4:16/0 = 0
|
||||||
|
5:16/0 = 0
|
||||||
|
6:16/0 = 0
|
||||||
|
7:16/0 = 0
|
||||||
|
8:16/0 = 0
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
occlusion_layer_0/light_mask = 1
|
occlusion_layer_0/light_mask = 1
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -204,6 +204,24 @@ shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
|||||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||||
|
|
||||||
|
[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="CircleShape2D" id="CircleShape2D_1"]
|
[sub_resource type="CircleShape2D" id="CircleShape2D_1"]
|
||||||
radius = 5.0
|
radius = 5.0
|
||||||
|
|
||||||
@@ -227,23 +245,13 @@ stream_2/stream = ExtResource("11_5x2ph")
|
|||||||
stream_3/stream = ExtResource("12_oynfq")
|
stream_3/stream = ExtResource("12_oynfq")
|
||||||
stream_4/stream = ExtResource("13_b0veo")
|
stream_4/stream = ExtResource("13_b0veo")
|
||||||
|
|
||||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_shield"]
|
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_block"]
|
||||||
shader = ExtResource("4_r7ul0")
|
playback_mode = 1
|
||||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
streams_count = 4
|
||||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
stream_0/stream = ExtResource("18_sfx")
|
||||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
stream_1/stream = ExtResource("19_sfx")
|
||||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
stream_2/stream = ExtResource("20_sfx")
|
||||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
stream_3/stream = ExtResource("21_sfx")
|
||||||
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"]
|
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_bow"]
|
||||||
playback_mode = 1
|
playback_mode = 1
|
||||||
@@ -253,14 +261,6 @@ stream_0/stream = ExtResource("22_sfx")
|
|||||||
stream_1/stream = ExtResource("23_sfx")
|
stream_1/stream = ExtResource("23_sfx")
|
||||||
stream_2/stream = ExtResource("24_sfx")
|
stream_2/stream = ExtResource("24_sfx")
|
||||||
|
|
||||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_block"]
|
|
||||||
playback_mode = 1
|
|
||||||
streams_count = 4
|
|
||||||
stream_0/stream = ExtResource("18_sfx")
|
|
||||||
stream_1/stream = ExtResource("19_sfx")
|
|
||||||
stream_2/stream = ExtResource("20_sfx")
|
|
||||||
stream_3/stream = ExtResource("21_sfx")
|
|
||||||
|
|
||||||
[node name="EnemyHumanoid" type="CharacterBody2D" unique_id=285357386]
|
[node name="EnemyHumanoid" type="CharacterBody2D" unique_id=285357386]
|
||||||
collision_layer = 2
|
collision_layer = 2
|
||||||
collision_mask = 65
|
collision_mask = 65
|
||||||
@@ -269,13 +269,14 @@ script = ExtResource("1")
|
|||||||
|
|
||||||
[node name="Shadow" type="Sprite2D" parent="." unique_id=468462304]
|
[node name="Shadow" type="Sprite2D" parent="." unique_id=468462304]
|
||||||
z_index = -1
|
z_index = -1
|
||||||
position = Vector2(0, 7)
|
position = Vector2(0, 3)
|
||||||
texture = SubResource("GradientTexture2D_1")
|
texture = SubResource("GradientTexture2D_1")
|
||||||
script = ExtResource("2")
|
script = ExtResource("2")
|
||||||
|
|
||||||
[node name="Sprite2DBody" type="Sprite2D" parent="." unique_id=855871821]
|
[node name="Sprite2DBody" type="Sprite2D" parent="." unique_id=855871821]
|
||||||
y_sort_enabled = true
|
y_sort_enabled = true
|
||||||
material = SubResource("ShaderMaterial_uedn7")
|
material = SubResource("ShaderMaterial_uedn7")
|
||||||
|
position = Vector2(0, -4)
|
||||||
texture = ExtResource("3")
|
texture = ExtResource("3")
|
||||||
hframes = 35
|
hframes = 35
|
||||||
vframes = 8
|
vframes = 8
|
||||||
@@ -283,59 +284,67 @@ vframes = 8
|
|||||||
[node name="Sprite2DBoots" type="Sprite2D" parent="." unique_id=460958943]
|
[node name="Sprite2DBoots" type="Sprite2D" parent="." unique_id=460958943]
|
||||||
y_sort_enabled = true
|
y_sort_enabled = true
|
||||||
material = SubResource("ShaderMaterial_5x2ph")
|
material = SubResource("ShaderMaterial_5x2ph")
|
||||||
|
position = Vector2(0, -4)
|
||||||
hframes = 35
|
hframes = 35
|
||||||
vframes = 8
|
vframes = 8
|
||||||
|
|
||||||
[node name="Sprite2DArmour" type="Sprite2D" parent="." unique_id=6790482]
|
[node name="Sprite2DArmour" type="Sprite2D" parent="." unique_id=6790482]
|
||||||
y_sort_enabled = true
|
y_sort_enabled = true
|
||||||
material = SubResource("ShaderMaterial_r7ul0")
|
material = SubResource("ShaderMaterial_r7ul0")
|
||||||
|
position = Vector2(0, -4)
|
||||||
hframes = 35
|
hframes = 35
|
||||||
vframes = 8
|
vframes = 8
|
||||||
|
|
||||||
[node name="Sprite2DFacialHair" type="Sprite2D" parent="." unique_id=31110906]
|
[node name="Sprite2DFacialHair" type="Sprite2D" parent="." unique_id=31110906]
|
||||||
y_sort_enabled = true
|
y_sort_enabled = true
|
||||||
material = SubResource("ShaderMaterial_oynfq")
|
material = SubResource("ShaderMaterial_oynfq")
|
||||||
|
position = Vector2(0, -4)
|
||||||
hframes = 35
|
hframes = 35
|
||||||
vframes = 8
|
vframes = 8
|
||||||
|
|
||||||
[node name="Sprite2DHair" type="Sprite2D" parent="." unique_id=425592986]
|
[node name="Sprite2DHair" type="Sprite2D" parent="." unique_id=425592986]
|
||||||
y_sort_enabled = true
|
y_sort_enabled = true
|
||||||
material = SubResource("ShaderMaterial_b0veo")
|
material = SubResource("ShaderMaterial_b0veo")
|
||||||
|
position = Vector2(0, -4)
|
||||||
hframes = 35
|
hframes = 35
|
||||||
vframes = 8
|
vframes = 8
|
||||||
|
|
||||||
[node name="Sprite2DEyes" type="Sprite2D" parent="." unique_id=496437887]
|
[node name="Sprite2DEyes" type="Sprite2D" parent="." unique_id=496437887]
|
||||||
y_sort_enabled = true
|
y_sort_enabled = true
|
||||||
material = SubResource("ShaderMaterial_of8l8")
|
material = SubResource("ShaderMaterial_of8l8")
|
||||||
|
position = Vector2(0, -4)
|
||||||
hframes = 35
|
hframes = 35
|
||||||
vframes = 8
|
vframes = 8
|
||||||
|
|
||||||
[node name="Sprite2DEyeLashes" type="Sprite2D" parent="." unique_id=1799398723]
|
[node name="Sprite2DEyeLashes" type="Sprite2D" parent="." unique_id=1799398723]
|
||||||
y_sort_enabled = true
|
y_sort_enabled = true
|
||||||
material = SubResource("ShaderMaterial_ofeay")
|
material = SubResource("ShaderMaterial_ofeay")
|
||||||
|
position = Vector2(0, -4)
|
||||||
hframes = 35
|
hframes = 35
|
||||||
vframes = 8
|
vframes = 8
|
||||||
|
|
||||||
[node name="Sprite2DAddons" type="Sprite2D" parent="." unique_id=1702763725]
|
[node name="Sprite2DAddons" type="Sprite2D" parent="." unique_id=1702763725]
|
||||||
y_sort_enabled = true
|
y_sort_enabled = true
|
||||||
material = SubResource("ShaderMaterial_5a33a")
|
material = SubResource("ShaderMaterial_5a33a")
|
||||||
|
position = Vector2(0, -4)
|
||||||
hframes = 35
|
hframes = 35
|
||||||
vframes = 8
|
vframes = 8
|
||||||
|
|
||||||
[node name="Sprite2DHeadgear" type="Sprite2D" parent="." unique_id=164186416]
|
[node name="Sprite2DHeadgear" type="Sprite2D" parent="." unique_id=164186416]
|
||||||
y_sort_enabled = true
|
y_sort_enabled = true
|
||||||
material = SubResource("ShaderMaterial_i1636")
|
material = SubResource("ShaderMaterial_i1636")
|
||||||
|
position = Vector2(0, -4)
|
||||||
hframes = 35
|
hframes = 35
|
||||||
vframes = 8
|
vframes = 8
|
||||||
|
|
||||||
[node name="Sprite2DShield" type="Sprite2D" parent="."]
|
[node name="Sprite2DShield" type="Sprite2D" parent="." unique_id=470468744]
|
||||||
visible = false
|
visible = false
|
||||||
material = SubResource("ShaderMaterial_shield")
|
material = SubResource("ShaderMaterial_shield")
|
||||||
texture = ExtResource("14_shield")
|
texture = ExtResource("14_shield")
|
||||||
hframes = 35
|
hframes = 35
|
||||||
vframes = 8
|
vframes = 8
|
||||||
|
|
||||||
[node name="Sprite2DShieldHolding" type="Sprite2D" parent="."]
|
[node name="Sprite2DShieldHolding" type="Sprite2D" parent="." unique_id=1318098286]
|
||||||
visible = false
|
visible = false
|
||||||
material = SubResource("ShaderMaterial_shield")
|
material = SubResource("ShaderMaterial_shield")
|
||||||
texture = ExtResource("15_shieldh")
|
texture = ExtResource("15_shieldh")
|
||||||
@@ -344,11 +353,13 @@ vframes = 8
|
|||||||
|
|
||||||
[node name="Sprite2DWeapon" type="Sprite2D" parent="." unique_id=1718282928]
|
[node name="Sprite2DWeapon" type="Sprite2D" parent="." unique_id=1718282928]
|
||||||
y_sort_enabled = true
|
y_sort_enabled = true
|
||||||
|
position = Vector2(0, -4)
|
||||||
texture = ExtResource("4")
|
texture = ExtResource("4")
|
||||||
hframes = 35
|
hframes = 35
|
||||||
vframes = 8
|
vframes = 8
|
||||||
|
|
||||||
[node name="Incantation" parent="." instance=ExtResource("16_inc")]
|
[node name="Incantation" parent="." unique_id=441417699 instance=ExtResource("16_inc")]
|
||||||
|
position = Vector2(0, -4)
|
||||||
|
|
||||||
[node name="AlertIndicator" type="Sprite2D" parent="." unique_id=1697001148]
|
[node name="AlertIndicator" type="Sprite2D" parent="." unique_id=1697001148]
|
||||||
visible = false
|
visible = false
|
||||||
@@ -365,10 +376,10 @@ texture = ExtResource("6")
|
|||||||
hframes = 3
|
hframes = 3
|
||||||
|
|
||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=189217716]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=189217716]
|
||||||
position = Vector2(0, 4)
|
|
||||||
shape = SubResource("CircleShape2D_1")
|
shape = SubResource("CircleShape2D_1")
|
||||||
|
|
||||||
[node name="AttackArea" type="Area2D" parent="." unique_id=1923132385]
|
[node name="AttackArea" type="Area2D" parent="." unique_id=1923132385]
|
||||||
|
position = Vector2(0, -4)
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
|
|
||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="AttackArea" unique_id=1597070641]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="AttackArea" unique_id=1597070641]
|
||||||
@@ -376,12 +387,14 @@ position = Vector2(0, 4)
|
|||||||
shape = SubResource("CircleShape2D_1")
|
shape = SubResource("CircleShape2D_1")
|
||||||
|
|
||||||
[node name="AggroArea" type="Area2D" parent="." unique_id=1234567890]
|
[node name="AggroArea" type="Area2D" parent="." unique_id=1234567890]
|
||||||
|
position = Vector2(0, -4)
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
|
|
||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="AggroArea" unique_id=1286608618]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="AggroArea" unique_id=1286608618]
|
||||||
shape = SubResource("CircleShape2D_2")
|
shape = SubResource("CircleShape2D_2")
|
||||||
|
|
||||||
[node name="SfxDie" type="AudioStreamPlayer2D" parent="." unique_id=693933783]
|
[node name="SfxDie" type="AudioStreamPlayer2D" parent="." unique_id=693933783]
|
||||||
|
position = Vector2(0, -4)
|
||||||
stream = SubResource("AudioStreamRandomizer_fikv0")
|
stream = SubResource("AudioStreamRandomizer_fikv0")
|
||||||
max_distance = 930.0
|
max_distance = 930.0
|
||||||
attenuation = 8.282114
|
attenuation = 8.282114
|
||||||
@@ -390,6 +403,7 @@ panning_strength = 1.3
|
|||||||
bus = &"Sfx"
|
bus = &"Sfx"
|
||||||
|
|
||||||
[node name="SfxAlertFoundPlayer" type="AudioStreamPlayer2D" parent="." unique_id=815591859]
|
[node name="SfxAlertFoundPlayer" type="AudioStreamPlayer2D" parent="." unique_id=815591859]
|
||||||
|
position = Vector2(0, -4)
|
||||||
stream = SubResource("AudioStreamRandomizer_37mja")
|
stream = SubResource("AudioStreamRandomizer_37mja")
|
||||||
max_distance = 1146.0
|
max_distance = 1146.0
|
||||||
attenuation = 8.57418
|
attenuation = 8.57418
|
||||||
@@ -397,45 +411,53 @@ max_polyphony = 4
|
|||||||
panning_strength = 1.04
|
panning_strength = 1.04
|
||||||
bus = &"Sfx"
|
bus = &"Sfx"
|
||||||
|
|
||||||
[node name="SfxActivateShield" type="AudioStreamPlayer2D" parent="."]
|
[node name="SfxActivateShield" type="AudioStreamPlayer2D" parent="." unique_id=626374525]
|
||||||
|
position = Vector2(0, -4)
|
||||||
stream = ExtResource("17_sfx")
|
stream = ExtResource("17_sfx")
|
||||||
volume_db = 9.695
|
volume_db = 9.695
|
||||||
attenuation = 1.3660401
|
attenuation = 1.3660401
|
||||||
panning_strength = 1.78
|
panning_strength = 1.78
|
||||||
|
|
||||||
[node name="SfxBlockWithShield" type="AudioStreamPlayer2D" parent="."]
|
[node name="SfxBlockWithShield" type="AudioStreamPlayer2D" parent="." unique_id=4928520]
|
||||||
|
position = Vector2(0, -4)
|
||||||
stream = SubResource("AudioStreamRandomizer_block")
|
stream = SubResource("AudioStreamRandomizer_block")
|
||||||
volume_db = 7.254
|
volume_db = 7.254
|
||||||
attenuation = 1.3195078
|
attenuation = 1.3195078
|
||||||
panning_strength = 1.06
|
panning_strength = 1.06
|
||||||
bus = &"Sfx"
|
bus = &"Sfx"
|
||||||
|
|
||||||
[node name="SfxBowShoot" type="AudioStreamPlayer2D" parent="."]
|
[node name="SfxBowShoot" type="AudioStreamPlayer2D" parent="." unique_id=495272024]
|
||||||
|
position = Vector2(0, -4)
|
||||||
stream = SubResource("AudioStreamRandomizer_bow")
|
stream = SubResource("AudioStreamRandomizer_bow")
|
||||||
pitch_scale = 1.33
|
pitch_scale = 1.33
|
||||||
attenuation = 6.7271657
|
attenuation = 6.7271657
|
||||||
|
|
||||||
[node name="SfxBowWithoutArrow" type="AudioStreamPlayer2D" parent="."]
|
[node name="SfxBowWithoutArrow" type="AudioStreamPlayer2D" parent="." unique_id=343560488]
|
||||||
|
position = Vector2(0, -4)
|
||||||
stream = ExtResource("25_sfx")
|
stream = ExtResource("25_sfx")
|
||||||
max_distance = 1455.0
|
max_distance = 1455.0
|
||||||
attenuation = 7.4642572
|
attenuation = 7.4642572
|
||||||
|
|
||||||
[node name="SfxBuckleBow" type="AudioStreamPlayer2D" parent="."]
|
[node name="SfxBuckleBow" type="AudioStreamPlayer2D" parent="." unique_id=1114353313]
|
||||||
|
position = Vector2(0, -4)
|
||||||
stream = ExtResource("26_sfx")
|
stream = ExtResource("26_sfx")
|
||||||
attenuation = 7.727478
|
attenuation = 7.727478
|
||||||
panning_strength = 1.03
|
panning_strength = 1.03
|
||||||
|
|
||||||
[node name="SfxSpellCharge" type="AudioStreamPlayer2D" parent="."]
|
[node name="SfxSpellCharge" type="AudioStreamPlayer2D" parent="." unique_id=671028964]
|
||||||
|
position = Vector2(0, -4)
|
||||||
stream = ExtResource("27_sfx")
|
stream = ExtResource("27_sfx")
|
||||||
|
|
||||||
[node name="SfxThrow" type="AudioStreamPlayer2D" parent="."]
|
[node name="SfxThrow" type="AudioStreamPlayer2D" parent="." unique_id=92763809]
|
||||||
|
position = Vector2(0, -4)
|
||||||
stream = ExtResource("28_sfx")
|
stream = ExtResource("28_sfx")
|
||||||
pitch_scale = 0.61
|
pitch_scale = 0.61
|
||||||
max_distance = 983.0
|
max_distance = 983.0
|
||||||
attenuation = 8.876549
|
attenuation = 8.876549
|
||||||
panning_strength = 1.04
|
panning_strength = 1.04
|
||||||
|
|
||||||
[node name="SfxLift" type="AudioStreamPlayer2D" parent="."]
|
[node name="SfxLift" type="AudioStreamPlayer2D" parent="." unique_id=722569138]
|
||||||
|
position = Vector2(0, -4)
|
||||||
stream = ExtResource("29_sfx")
|
stream = ExtResource("29_sfx")
|
||||||
max_distance = 1246.0
|
max_distance = 1246.0
|
||||||
attenuation = 1.9999994
|
attenuation = 1.9999994
|
||||||
|
|||||||
3
src/scenes/fallout.tscn
Normal file
3
src/scenes/fallout.tscn
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[gd_scene format=3 uid="uid://cm4f7w8ohdi7r"]
|
||||||
|
|
||||||
|
[node name="Fallout" type="Node2D" unique_id=587574934]
|
||||||
@@ -42,6 +42,105 @@ shader_parameter/replace_13 = Color(0, 0, 0, 1)
|
|||||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||||
shader_parameter/ambient = Color(1, 1, 1, 1)
|
shader_parameter/ambient = Color(1, 1, 1, 1)
|
||||||
|
|
||||||
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_u1jpj"]
|
||||||
|
shader = ExtResource("4_bhwwd")
|
||||||
|
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/original_7 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_8 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_9 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_10 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_11 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_12 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_13 = 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/replace_7 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_8 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_9 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_10 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_11 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_12 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_13 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||||
|
shader_parameter/ambient = Color(1, 1, 1, 1)
|
||||||
|
|
||||||
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_uh34q"]
|
||||||
|
shader = ExtResource("4_bhwwd")
|
||||||
|
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/original_7 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_8 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_9 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_10 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_11 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_12 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_13 = 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/replace_7 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_8 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_9 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_10 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_11 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_12 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_13 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||||
|
shader_parameter/ambient = Color(1, 1, 1, 1)
|
||||||
|
|
||||||
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_ph1f2"]
|
||||||
|
shader = ExtResource("4_bhwwd")
|
||||||
|
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/original_7 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_8 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_9 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_10 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_11 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_12 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/original_13 = 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/replace_7 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_8 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_9 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_10 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_11 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_12 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/replace_13 = Color(0, 0, 0, 1)
|
||||||
|
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||||
|
shader_parameter/ambient = Color(1, 1, 1, 1)
|
||||||
|
|
||||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_bhwwd"]
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_bhwwd"]
|
||||||
shader = ExtResource("4_bhwwd")
|
shader = ExtResource("4_bhwwd")
|
||||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||||
@@ -92,7 +191,16 @@ z_index = -2
|
|||||||
material = SubResource("ShaderMaterial_pdbwf")
|
material = SubResource("ShaderMaterial_pdbwf")
|
||||||
tile_set = ExtResource("9")
|
tile_set = ExtResource("9")
|
||||||
|
|
||||||
|
[node name="TileMapLayerDecoratedGround" type="TileMapLayer" parent="Environment" unique_id=1839647666]
|
||||||
|
material = SubResource("ShaderMaterial_u1jpj")
|
||||||
|
tile_set = ExtResource("9")
|
||||||
|
|
||||||
|
[node name="TileMapLayerCrackedGround" type="TileMapLayer" parent="Environment" unique_id=556112467]
|
||||||
|
material = SubResource("ShaderMaterial_uh34q")
|
||||||
|
tile_set = ExtResource("9")
|
||||||
|
|
||||||
[node name="TileMapLayerMiddle" type="TileMapLayer" parent="Environment" unique_id=1063124770]
|
[node name="TileMapLayerMiddle" type="TileMapLayer" parent="Environment" unique_id=1063124770]
|
||||||
|
material = SubResource("ShaderMaterial_ph1f2")
|
||||||
tile_set = ExtResource("9")
|
tile_set = ExtResource("9")
|
||||||
|
|
||||||
[node name="TileMapLayerAbove" type="TileMapLayer" parent="Environment" unique_id=1234567892]
|
[node name="TileMapLayerAbove" type="TileMapLayer" parent="Environment" unique_id=1234567892]
|
||||||
|
|||||||
@@ -289,6 +289,9 @@ radius = 4.0
|
|||||||
[sub_resource type="CircleShape2D" id="CircleShape2D_2"]
|
[sub_resource type="CircleShape2D" id="CircleShape2D_2"]
|
||||||
radius = 8.0
|
radius = 8.0
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_quicksand"]
|
||||||
|
size = Vector2(8, 8)
|
||||||
|
|
||||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_l71n6"]
|
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_l71n6"]
|
||||||
playback_mode = 1
|
playback_mode = 1
|
||||||
random_pitch = 1.0118532
|
random_pitch = 1.0118532
|
||||||
@@ -340,6 +343,21 @@ tracks/0/keys = {
|
|||||||
"values": [2037]
|
"values": [2037]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[sub_resource type="Animation" id="Animation_6e8lb"]
|
||||||
|
resource_name = "fallout"
|
||||||
|
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.029999994),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 1,
|
||||||
|
"values": [2037]
|
||||||
|
}
|
||||||
|
|
||||||
[sub_resource type="Animation" id="Animation_j2b1d"]
|
[sub_resource type="Animation" id="Animation_j2b1d"]
|
||||||
resource_name = "fire_charging"
|
resource_name = "fire_charging"
|
||||||
length = 0.4
|
length = 0.4
|
||||||
@@ -459,6 +477,7 @@ tracks/0/keys = {
|
|||||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_2dvfe"]
|
[sub_resource type="AnimationLibrary" id="AnimationLibrary_2dvfe"]
|
||||||
_data = {
|
_data = {
|
||||||
&"RESET": SubResource("Animation_t4otl"),
|
&"RESET": SubResource("Animation_t4otl"),
|
||||||
|
&"fallout": SubResource("Animation_6e8lb"),
|
||||||
&"fire_charging": SubResource("Animation_j2b1d"),
|
&"fire_charging": SubResource("Animation_j2b1d"),
|
||||||
&"fire_ready": SubResource("Animation_cs1tg"),
|
&"fire_ready": SubResource("Animation_cs1tg"),
|
||||||
&"frost_charging": SubResource("Animation_frost_ch"),
|
&"frost_charging": SubResource("Animation_frost_ch"),
|
||||||
@@ -702,11 +721,20 @@ collision_mask = 3
|
|||||||
shape = SubResource("CircleShape2D_2")
|
shape = SubResource("CircleShape2D_2")
|
||||||
debug_color = Color(0.70196074, 0.6126261, 0.19635464, 0.41960785)
|
debug_color = Color(0.70196074, 0.6126261, 0.19635464, 0.41960785)
|
||||||
|
|
||||||
|
[node name="QuicksandArea" type="Area2D" parent="." unique_id=600000001]
|
||||||
|
position = Vector2(0, 4)
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 0
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="QuicksandArea" unique_id=600000002]
|
||||||
|
shape = SubResource("RectangleShape2D_quicksand")
|
||||||
|
debug_color = Color(0.48027503, 0, 0.70196074, 0.41960785)
|
||||||
|
|
||||||
[node name="Label" type="Label" parent="." unique_id=227628720]
|
[node name="Label" type="Label" parent="." unique_id=227628720]
|
||||||
offset_left = -10.0
|
offset_left = -10.0
|
||||||
offset_top = -15.0
|
offset_top = -15.0
|
||||||
offset_right = 10.0
|
offset_right = 10.0
|
||||||
offset_bottom = -9.0
|
offset_bottom = 8.0
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
[node name="InteractionIndicator" type="Sprite2D" parent="." unique_id=1661043470]
|
[node name="InteractionIndicator" type="Sprite2D" parent="." unique_id=1661043470]
|
||||||
@@ -732,6 +760,12 @@ volume_db = -2.537
|
|||||||
attenuation = 8.876548
|
attenuation = 8.876548
|
||||||
bus = &"Sfx"
|
bus = &"Sfx"
|
||||||
|
|
||||||
|
[node name="SfxFallout" type="AudioStreamPlayer2D" parent="." unique_id=600000003]
|
||||||
|
stream = ExtResource("19_4r5pv")
|
||||||
|
volume_db = -2.537
|
||||||
|
attenuation = 8.876548
|
||||||
|
bus = &"Sfx"
|
||||||
|
|
||||||
[node name="SfxTakeDamage" type="AudioStreamPlayer2D" parent="." unique_id=322150091]
|
[node name="SfxTakeDamage" type="AudioStreamPlayer2D" parent="." unique_id=322150091]
|
||||||
stream = SubResource("AudioStreamRandomizer_487ah")
|
stream = SubResource("AudioStreamRandomizer_487ah")
|
||||||
volume_db = -6.092
|
volume_db = -6.092
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ var blink_start_time: float = 1.0 # Start blinking 1 second before explosion
|
|||||||
var can_be_collected: bool = false
|
var can_be_collected: bool = false
|
||||||
var collection_delay: float = 0.2 # Can be collected after 0.2 seconds
|
var collection_delay: float = 0.2 # Can be collected after 0.2 seconds
|
||||||
|
|
||||||
|
# Fallout (landed in quicksand): sink and explode with no visual/damage, but sound + screenshake
|
||||||
|
var fell_in_fallout: bool = false
|
||||||
|
var fallout_sink_progress: float = 1.0
|
||||||
|
const FALLOUT_SINK_DURATION: float = 0.5
|
||||||
|
|
||||||
@onready var sprite = $Sprite2D
|
@onready var sprite = $Sprite2D
|
||||||
@onready var explosion_sprite = $ExplosionSprite
|
@onready var explosion_sprite = $ExplosionSprite
|
||||||
@onready var shadow = $Shadow
|
@onready var shadow = $Shadow
|
||||||
@@ -143,7 +148,10 @@ func _start_fuse():
|
|||||||
|
|
||||||
func _physics_process(delta):
|
func _physics_process(delta):
|
||||||
if is_exploding:
|
if is_exploding:
|
||||||
# Handle explosion animation
|
# Handle explosion animation (or immediate free if fell in fallout)
|
||||||
|
if fell_in_fallout:
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
explosion_timer += delta
|
explosion_timer += delta
|
||||||
if explosion_sprite:
|
if explosion_sprite:
|
||||||
# Play 9 frames of explosion animation at ~15 FPS
|
# Play 9 frames of explosion animation at ~15 FPS
|
||||||
@@ -157,6 +165,21 @@ func _physics_process(delta):
|
|||||||
queue_free()
|
queue_free()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Fallout sink: scale down over time (no respawn)
|
||||||
|
if fell_in_fallout:
|
||||||
|
fallout_sink_progress -= delta / FALLOUT_SINK_DURATION
|
||||||
|
if fallout_sink_progress < 0.0:
|
||||||
|
fallout_sink_progress = 0.0
|
||||||
|
scale = Vector2.ONE * max(0.0, fallout_sink_progress)
|
||||||
|
# Fuse still runs; when it hits, _explode() will do sound + screenshake only
|
||||||
|
if is_fused:
|
||||||
|
fuse_timer += delta
|
||||||
|
if fuse_timer >= fuse_duration:
|
||||||
|
_explode()
|
||||||
|
return
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
|
||||||
# Update fuse timer
|
# Update fuse timer
|
||||||
if is_fused:
|
if is_fused:
|
||||||
fuse_timer += delta
|
fuse_timer += delta
|
||||||
@@ -246,6 +269,15 @@ func _land():
|
|||||||
position_z = 0.0
|
position_z = 0.0
|
||||||
velocity_z = 0.0
|
velocity_z = 0.0
|
||||||
|
|
||||||
|
# If landed on fallout tile: sink and will explode with no visual/damage (sound + screenshake only)
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw and gw.has_method("_is_position_on_fallout_tile") and gw._is_position_on_fallout_tile(global_position):
|
||||||
|
fell_in_fallout = true
|
||||||
|
fallout_sink_progress = 1.0
|
||||||
|
can_be_collected = false
|
||||||
|
if collection_area:
|
||||||
|
collection_area.set_deferred("monitoring", false)
|
||||||
|
|
||||||
# Start fuse when landing
|
# Start fuse when landing
|
||||||
if not is_fused:
|
if not is_fused:
|
||||||
_start_fuse()
|
_start_fuse()
|
||||||
@@ -256,7 +288,27 @@ func _explode():
|
|||||||
|
|
||||||
is_exploding = true
|
is_exploding = true
|
||||||
|
|
||||||
# Hide bomb sprite and shadow, show explosion
|
# Stop fuse sound and particles
|
||||||
|
if has_node("SfxFuse"):
|
||||||
|
$SfxFuse.stop()
|
||||||
|
if fuse_particles:
|
||||||
|
fuse_particles.emitting = false
|
||||||
|
if fuse_light:
|
||||||
|
fuse_light.enabled = false
|
||||||
|
|
||||||
|
# Fell in fallout: no explosion visual, no damage, but sound + screenshake
|
||||||
|
if fell_in_fallout:
|
||||||
|
if has_node("SfxExplosion"):
|
||||||
|
$SfxExplosion.play()
|
||||||
|
_cause_screenshake()
|
||||||
|
if bomb_area:
|
||||||
|
bomb_area.set_deferred("monitoring", false)
|
||||||
|
if collection_area:
|
||||||
|
collection_area.set_deferred("monitoring", false)
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Normal explosion
|
||||||
if sprite:
|
if sprite:
|
||||||
sprite.visible = false
|
sprite.visible = false
|
||||||
if shadow:
|
if shadow:
|
||||||
@@ -267,38 +319,27 @@ func _explode():
|
|||||||
explosion_frame = 0
|
explosion_frame = 0
|
||||||
explosion_timer = 0.0
|
explosion_timer = 0.0
|
||||||
|
|
||||||
# Stop fuse sound and particles
|
|
||||||
if has_node("SfxFuse"):
|
|
||||||
$SfxFuse.stop()
|
|
||||||
if fuse_particles:
|
|
||||||
fuse_particles.emitting = false
|
|
||||||
|
|
||||||
# Disable fuse light, enable explosion light
|
|
||||||
if fuse_light:
|
|
||||||
fuse_light.enabled = false
|
|
||||||
if explosion_light:
|
if explosion_light:
|
||||||
explosion_light.enabled = true
|
explosion_light.enabled = true
|
||||||
# Fade out explosion light over time
|
|
||||||
var tween = create_tween()
|
var tween = create_tween()
|
||||||
tween.tween_property(explosion_light, "energy", 0.0, 0.3)
|
tween.tween_property(explosion_light, "energy", 0.0, 0.3)
|
||||||
tween.tween_callback(func(): if explosion_light: explosion_light.enabled = false)
|
tween.tween_callback(func(): if explosion_light: explosion_light.enabled = false)
|
||||||
|
|
||||||
# Play explosion sound
|
|
||||||
if has_node("SfxExplosion"):
|
if has_node("SfxExplosion"):
|
||||||
$SfxExplosion.play()
|
$SfxExplosion.play()
|
||||||
|
|
||||||
# Deal area damage
|
|
||||||
_deal_explosion_damage()
|
_deal_explosion_damage()
|
||||||
|
|
||||||
# Cause screenshake
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw and gw.has_method("break_cracked_tiles_in_radius"):
|
||||||
|
gw.break_cracked_tiles_in_radius(global_position, damage_radius / 2.0)
|
||||||
|
|
||||||
_cause_screenshake()
|
_cause_screenshake()
|
||||||
|
|
||||||
# Spawn tile debris particles (4 pieces per affected tile, bounce, fade)
|
|
||||||
_spawn_explosion_tile_particles()
|
_spawn_explosion_tile_particles()
|
||||||
if has_node("SfxDebrisFromParticles"):
|
if has_node("SfxDebrisFromParticles"):
|
||||||
$SfxDebrisFromParticles.play()
|
$SfxDebrisFromParticles.play()
|
||||||
|
|
||||||
# Disable collision
|
|
||||||
if bomb_area:
|
if bomb_area:
|
||||||
bomb_area.set_deferred("monitoring", false)
|
bomb_area.set_deferred("monitoring", false)
|
||||||
if collection_area:
|
if collection_area:
|
||||||
|
|||||||
@@ -53,12 +53,6 @@ const WALL_BOTTOM_RIGHT_BOTTOM_RIGHT = Vector2i(4, 4) # Bottom-right corner, bot
|
|||||||
const WALL_BOTTOM_UPPER = Vector2i(2, 3) # Bottom wall, upper part
|
const WALL_BOTTOM_UPPER = Vector2i(2, 3) # Bottom wall, upper part
|
||||||
const WALL_BOTTOM_LOWER = Vector2i(2, 4) # Bottom wall, lower part
|
const WALL_BOTTOM_LOWER = Vector2i(2, 4) # Bottom wall, lower part
|
||||||
|
|
||||||
# Inner wall tiles (for non-rectangular rooms)
|
|
||||||
const INNER_WALL_TOP_LEFT = Vector2i(1, 6)
|
|
||||||
const INNER_WALL_TOP_RIGHT = Vector2i(3, 6)
|
|
||||||
const INNER_WALL_BOTTOM_LEFT = Vector2i(1, 8)
|
|
||||||
const INNER_WALL_BOTTOM_RIGHT = Vector2i(3, 8)
|
|
||||||
|
|
||||||
# Door tiles
|
# Door tiles
|
||||||
const DOOR_UP_START = Vector2i(7, 0) # 3x2 large
|
const DOOR_UP_START = Vector2i(7, 0) # 3x2 large
|
||||||
const DOOR_LEFT_START = Vector2i(5, 2) # 2x3 large
|
const DOOR_LEFT_START = Vector2i(5, 2) # 2x3 large
|
||||||
@@ -71,8 +65,35 @@ const STAIRS_LEFT_START = Vector2i(5, 2) # 2x3 large, middle tile is (5,1) inste
|
|||||||
const STAIRS_RIGHT_START = Vector2i(10, 2) # 2x3 large, middle tile is (11,1) instead of (11,3)
|
const STAIRS_RIGHT_START = Vector2i(10, 2) # 2x3 large, middle tile is (11,1) instead of (11,3)
|
||||||
const STAIRS_DOWN_START = Vector2i(7, 5) # 3x2 large, middle tile is (6,6) instead of (8,6)
|
const STAIRS_DOWN_START = Vector2i(7, 5) # 3x2 large, middle tile is (6,6) instead of (8,6)
|
||||||
|
|
||||||
# Ground/floor tiles (random selection)
|
# Base floor tile always on DungeonLayer0 (standard floor)
|
||||||
const FLOOR_TILES = [Vector2i(9, 8), Vector2i(14, 8), Vector2i(6, 11)]
|
const FLOOR_BASE = Vector2i(0, 15)
|
||||||
|
# Decorated ground tiles (TileMapLayerDecoratedGround): (16,16) = most common (blank), then (1,15)-(21,15), (0,16)-(14,16)
|
||||||
|
const FLOOR_DECORATED_PLAIN = Vector2i(16, 16) # Blank/non-decorated (0,0 has collision)
|
||||||
|
const FLOOR_DECORATED_RANGE_1_START = Vector2i(1, 15)
|
||||||
|
const FLOOR_DECORATED_RANGE_1_END = Vector2i(21, 15)
|
||||||
|
const FLOOR_DECORATED_RANGE_2_START = Vector2i(0, 16)
|
||||||
|
const FLOOR_DECORATED_RANGE_2_END = Vector2i(14, 16)
|
||||||
|
# Cracked ground tile (TileMapLayerCrackedGround) - terrain -2
|
||||||
|
const CRACKED_TILE = Vector2i(15, 16)
|
||||||
|
|
||||||
|
# Fallout: solo floor tiles (cracked/worn) - use center for all single fallout floor tiles
|
||||||
|
const FALLOUT_CENTER = Vector2i(10, 12) # 274
|
||||||
|
|
||||||
|
# Fallout inner wall edges (for larger holes - boundary between hole wall and room floor)
|
||||||
|
const FALLOUT_INNER_UP_LEFT = Vector2i(9, 11) # 251
|
||||||
|
const FALLOUT_INNER_UP = Vector2i(10, 11) # 252
|
||||||
|
const FALLOUT_INNER_UP_RIGHT = Vector2i(11, 11) # 253
|
||||||
|
const FALLOUT_INNER_RIGHT = Vector2i(11, 12) # 275
|
||||||
|
const FALLOUT_INNER_DOWN_RIGHT = Vector2i(11, 13) # 297
|
||||||
|
const FALLOUT_INNER_DOWN = Vector2i(10, 13) # 296
|
||||||
|
const FALLOUT_INNER_DOWN_LEFT = Vector2i(9, 13) # 295
|
||||||
|
const FALLOUT_INNER_LEFT = Vector2i(9, 12) # 273
|
||||||
|
|
||||||
|
# Fallout inner corners (for larger holes)
|
||||||
|
const FALLOUT_CORNER_INNER_UP_LEFT = Vector2i(13, 10) # 233
|
||||||
|
const FALLOUT_CORNER_INNER_UP_RIGHT = Vector2i(16, 10) # 236
|
||||||
|
const FALLOUT_CORNER_INNER_DOWN_LEFT = Vector2i(13, 13) # 299
|
||||||
|
const FALLOUT_CORNER_INNER_DOWN_RIGHT = Vector2i(16, 13) # 302
|
||||||
|
|
||||||
# Room generation parameters
|
# Room generation parameters
|
||||||
# Minimum room size is 3x3 floor tiles, but rooms need 2-tile walls on each side
|
# Minimum room size is 3x3 floor tiles, but rooms need 2-tile walls on each side
|
||||||
@@ -100,13 +121,19 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
|
|||||||
|
|
||||||
# Initialize grid (0 = wall, 1 = floor, 2 = door, 3 = corridor)
|
# Initialize grid (0 = wall, 1 = floor, 2 = door, 3 = corridor)
|
||||||
var grid = []
|
var grid = []
|
||||||
var tile_grid = [] # Actual tile coordinates (Vector2i) for rendering
|
var tile_grid = [] # Actual tile coordinates (Vector2i) for DungeonLayer0
|
||||||
|
var decorated_tile_grid = [] # TileMapLayerDecoratedGround: random (0,0) or (1,15)-(21,15), (0,16)-(14,16)
|
||||||
|
var cracked_tile_grid = [] # TileMapLayerCrackedGround: (15,16) where true; never in doors/corridors/front
|
||||||
for x in range(map_size.x):
|
for x in range(map_size.x):
|
||||||
grid.append([])
|
grid.append([])
|
||||||
tile_grid.append([])
|
tile_grid.append([])
|
||||||
|
decorated_tile_grid.append([])
|
||||||
|
cracked_tile_grid.append([])
|
||||||
for y in range(map_size.y):
|
for y in range(map_size.y):
|
||||||
grid[x].append(0) # Start with all walls
|
grid[x].append(0) # Start with all walls
|
||||||
tile_grid[x].append(Vector2i(0, 0)) # Default wall tile (will be set properly later)
|
tile_grid[x].append(Vector2i(0, 0)) # Default wall tile (will be set properly later)
|
||||||
|
decorated_tile_grid[x].append(null) # Filled for floor/corridor only
|
||||||
|
cracked_tile_grid[x].append(false) # Filled for some floor only (not doors/corridors/front)
|
||||||
|
|
||||||
var all_rooms = []
|
var all_rooms = []
|
||||||
var all_doors = []
|
var all_doors = []
|
||||||
@@ -271,6 +298,18 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
|
|||||||
# 7. Render walls around rooms
|
# 7. Render walls around rooms
|
||||||
_render_room_walls(all_rooms, grid, tile_grid, map_size, rng)
|
_render_room_walls(all_rooms, grid, tile_grid, map_size, rng)
|
||||||
|
|
||||||
|
# 7.4. Fix inner walls and corners (holes / compound rooms)
|
||||||
|
_cleanup_compound_room_corners_and_walls(all_rooms, grid, tile_grid, map_size)
|
||||||
|
|
||||||
|
# 7.43. Fill decorated ground layer (random per floor/corridor); (0,0) most common
|
||||||
|
_fill_decorated_tile_grid(grid, decorated_tile_grid, map_size, rng)
|
||||||
|
# 7.44. Sprinkle cracked floor (15,16) on some floor; never in doors, corridors, in front of doors, or in start room
|
||||||
|
_fill_cracked_tile_grid(grid, cracked_tile_grid, map_size, all_doors, all_rooms[start_room_index], rng)
|
||||||
|
# 7.45. Sprinkle fallout tiles on some floor (cracked/worn look); never in doors, corridors, in front of doors, or in start room
|
||||||
|
_render_fallout_tiles(grid, tile_grid, map_size, all_doors, all_rooms[start_room_index], rng)
|
||||||
|
# 7.46. Keep fallout tiles free from decorated layer (no decorated tiles on top of fallout)
|
||||||
|
_clear_decorated_on_fallout(tile_grid, decorated_tile_grid, map_size)
|
||||||
|
|
||||||
# 7.5. Place stairs in exit room BEFORE placing torches (so torches don't overlap stairs)
|
# 7.5. Place stairs in exit room BEFORE placing torches (so torches don't overlap stairs)
|
||||||
var stairs_data = _place_stairs_in_exit_room(all_rooms[exit_room_index], grid, tile_grid, map_size, all_doors, rng)
|
var stairs_data = _place_stairs_in_exit_room(all_rooms[exit_room_index], grid, tile_grid, map_size, all_doors, rng)
|
||||||
if stairs_data.is_empty():
|
if stairs_data.is_empty():
|
||||||
@@ -305,9 +344,25 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
|
|||||||
var blocking_doors = blocking_doors_result.doors if blocking_doors_result.has("doors") else blocking_doors_result
|
var blocking_doors = blocking_doors_result.doors if blocking_doors_result.has("doors") else blocking_doors_result
|
||||||
var room_puzzle_data = blocking_doors_result.puzzle_data if blocking_doors_result.has("puzzle_data") else {}
|
var room_puzzle_data = blocking_doors_result.puzzle_data if blocking_doors_result.has("puzzle_data") else {}
|
||||||
|
|
||||||
|
# Floor switches must NEVER have cracked or fallout: clear cracked_tile_grid and replace fallout with normal floor at all switch positions
|
||||||
|
var blocking_doors_array = blocking_doors if blocking_doors is Array else blocking_doors.doors
|
||||||
|
var switch_tiles_seen = {}
|
||||||
|
for door_data in blocking_doors_array:
|
||||||
|
if "switch_tile_x" in door_data and "switch_tile_y" in door_data:
|
||||||
|
var sx = door_data.switch_tile_x
|
||||||
|
var sy = door_data.switch_tile_y
|
||||||
|
var key = str(sx) + "," + str(sy)
|
||||||
|
if not key in switch_tiles_seen:
|
||||||
|
switch_tiles_seen[key] = true
|
||||||
|
if sx >= 0 and sx < map_size.x and sy >= 0 and sy < map_size.y:
|
||||||
|
cracked_tile_grid[sx][sy] = false
|
||||||
|
if _is_fallout_atlas(tile_grid[sx][sy]):
|
||||||
|
tile_grid[sx][sy] = FLOOR_BASE
|
||||||
|
LogManager.log("DungeonGenerator: Replaced fallout with normal floor at switch tile (" + str(sx) + "," + str(sy) + ")", LogManager.CATEGORY_DUNGEON)
|
||||||
|
LogManager.log("DungeonGenerator: Cleared cracked at switch tile (" + str(sx) + "," + str(sy) + ") - switches cannot have cracked ground", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
# Extract rooms with monster spawner puzzles (these should NOT have pre-spawned enemies)
|
# Extract rooms with monster spawner puzzles (these should NOT have pre-spawned enemies)
|
||||||
var rooms_with_spawner_puzzles = []
|
var rooms_with_spawner_puzzles = []
|
||||||
var blocking_doors_array = blocking_doors if blocking_doors is Array else blocking_doors.doors
|
|
||||||
for door_data in blocking_doors_array:
|
for door_data in blocking_doors_array:
|
||||||
if "puzzle_type" in door_data and door_data.puzzle_type == "enemy":
|
if "puzzle_type" in door_data and door_data.puzzle_type == "enemy":
|
||||||
if "blocking_room" in door_data and not door_data.blocking_room.is_empty():
|
if "blocking_room" in door_data and not door_data.blocking_room.is_empty():
|
||||||
@@ -348,7 +403,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
|
|||||||
var room = all_rooms[i]
|
var room = all_rooms[i]
|
||||||
# Skip start room and exit room
|
# Skip start room and exit room
|
||||||
if i != start_room_index and i != exit_room_index:
|
if i != start_room_index and i != exit_room_index:
|
||||||
var room_objects = _place_interactable_objects_in_room(room, grid, map_size, all_doors, all_enemies, rng, room_puzzle_data)
|
var room_objects = _place_interactable_objects_in_room(room, grid, tile_grid, map_size, all_doors, all_enemies, rng, room_puzzle_data)
|
||||||
all_interactable_objects.append_array(room_objects)
|
all_interactable_objects.append_array(room_objects)
|
||||||
|
|
||||||
# 9.6. Place traps (1-2 per level, excluding start and exit rooms)
|
# 9.6. Place traps (1-2 per level, excluding start and exit rooms)
|
||||||
@@ -369,21 +424,20 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
|
|||||||
"blocking_doors": blocking_doors,
|
"blocking_doors": blocking_doors,
|
||||||
"grid": grid,
|
"grid": grid,
|
||||||
"tile_grid": tile_grid,
|
"tile_grid": tile_grid,
|
||||||
|
"decorated_tile_grid": decorated_tile_grid,
|
||||||
|
"cracked_tile_grid": cracked_tile_grid,
|
||||||
"map_size": map_size,
|
"map_size": map_size,
|
||||||
"start_room": all_rooms[start_room_index],
|
"start_room": all_rooms[start_room_index],
|
||||||
"exit_room": all_rooms[exit_room_index]
|
"exit_room": all_rooms[exit_room_index]
|
||||||
}
|
}
|
||||||
|
|
||||||
func _set_floor(room: Dictionary, grid: Array, tile_grid: Array, map_size: Vector2i, rng: RandomNumberGenerator):
|
func _set_floor(room: Dictionary, grid: Array, tile_grid: Array, map_size: Vector2i, _rng: RandomNumberGenerator):
|
||||||
# Set floor tiles for the room (interior only, walls will be set separately)
|
# Set floor tiles for the room (interior only); base layer always (0,15)
|
||||||
# Leave 2 tile border for walls (walls are 2 tiles tall)
|
|
||||||
for x in range(room.x + 2, room.x + room.w - 2):
|
for x in range(room.x + 2, room.x + room.w - 2):
|
||||||
for y in range(room.y + 2, room.y + room.h - 2):
|
for y in range(room.y + 2, room.y + room.h - 2):
|
||||||
if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y:
|
if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y:
|
||||||
grid[x][y] = 1 # Floor
|
grid[x][y] = 1 # Floor
|
||||||
# Random floor tile variation
|
tile_grid[x][y] = FLOOR_BASE
|
||||||
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
|
|
||||||
tile_grid[x][y] = floor_tile
|
|
||||||
|
|
||||||
func _try_place_room_near(source_room: Dictionary, grid: Array, map_size: Vector2i, rng: RandomNumberGenerator) -> Dictionary:
|
func _try_place_room_near(source_room: Dictionary, grid: Array, map_size: Vector2i, rng: RandomNumberGenerator) -> Dictionary:
|
||||||
var attempts = 20
|
var attempts = 20
|
||||||
@@ -547,13 +601,11 @@ func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid:
|
|||||||
if corridor_intersects_other_room.call(corridor_start_x, corridor_y, corridor_end_x, corridor_y, true):
|
if corridor_intersects_other_room.call(corridor_start_x, corridor_y, corridor_end_x, corridor_y, true):
|
||||||
return {} # Corridor would pass through another room, skip this connection
|
return {} # Corridor would pass through another room, skip this connection
|
||||||
|
|
||||||
# Create corridor (1 tile wide) - use floor tiles
|
# Create corridor (1 tile wide) - base floor (0,15)
|
||||||
# Corridor is between the rooms, after the door
|
|
||||||
for x in range(wall_x + 1, wall_x + corridor_length + 1): # Corridor starts after the wall
|
for x in range(wall_x + 1, wall_x + corridor_length + 1): # Corridor starts after the wall
|
||||||
if x >= 0 and x < map_size.x and door_y + 1 >= 0 and door_y + 1 < map_size.y:
|
if x >= 0 and x < map_size.x and door_y + 1 >= 0 and door_y + 1 < map_size.y:
|
||||||
grid[x][door_y + 1] = 3 # Corridor (middle row of door)
|
grid[x][door_y + 1] = 3 # Corridor (middle row of door)
|
||||||
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
|
tile_grid[x][door_y + 1] = FLOOR_BASE
|
||||||
tile_grid[x][door_y + 1] = floor_tile
|
|
||||||
|
|
||||||
# Create door on RIGHT wall of left room (2x3 tiles - 2 wide, 3 tall)
|
# Create door on RIGHT wall of left room (2x3 tiles - 2 wide, 3 tall)
|
||||||
# Door is placed ON the wall, replacing the 2-tile wide wall
|
# Door is placed ON the wall, replacing the 2-tile wide wall
|
||||||
@@ -572,8 +624,7 @@ func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid:
|
|||||||
tile_grid[x][y] = door_tile_start + Vector2i(door_dx, door_dy)
|
tile_grid[x][y] = door_tile_start + Vector2i(door_dx, door_dy)
|
||||||
if door_dx == 0 and door_dy == 1:
|
if door_dx == 0 and door_dy == 1:
|
||||||
grid[x][y] = 1 # Floor
|
grid[x][y] = 1 # Floor
|
||||||
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
|
tile_grid[x][y] = FLOOR_BASE
|
||||||
tile_grid[x][y] = floor_tile
|
|
||||||
|
|
||||||
|
|
||||||
# Also create door on LEFT wall of right room (if there's a gap)
|
# Also create door on LEFT wall of right room (if there's a gap)
|
||||||
@@ -591,8 +642,7 @@ func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid:
|
|||||||
tile_grid[x][y] = right_door_tile_start + Vector2i(door_dx, door_dy)
|
tile_grid[x][y] = right_door_tile_start + Vector2i(door_dx, door_dy)
|
||||||
if door_dx == 1 and door_dy == 1:
|
if door_dx == 1 and door_dy == 1:
|
||||||
grid[x][y] = 1 # Floor
|
grid[x][y] = 1 # Floor
|
||||||
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
|
tile_grid[x][y] = FLOOR_BASE
|
||||||
tile_grid[x][y] = floor_tile
|
|
||||||
|
|
||||||
# CRITICAL: room1 = room the door is ON (left room for horizontal doors)
|
# CRITICAL: room1 = room the door is ON (left room for horizontal doors)
|
||||||
# room2 = room the door leads TO (right room for horizontal doors)
|
# room2 = room the door leads TO (right room for horizontal doors)
|
||||||
@@ -639,13 +689,11 @@ func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid:
|
|||||||
if corridor_intersects_other_room.call(corridor_x, corridor_start_y, corridor_x, corridor_end_y, false):
|
if corridor_intersects_other_room.call(corridor_x, corridor_start_y, corridor_x, corridor_end_y, false):
|
||||||
return {} # Corridor would pass through another room, skip this connection
|
return {} # Corridor would pass through another room, skip this connection
|
||||||
|
|
||||||
# Create corridor (1 tile wide) - use floor tiles
|
# Create corridor (1 tile wide) - base floor (0,15)
|
||||||
# Corridor is between the rooms, after the door
|
|
||||||
for y in range(wall_y + 1, wall_y + corridor_length + 1): # Corridor starts after the wall
|
for y in range(wall_y + 1, wall_y + corridor_length + 1): # Corridor starts after the wall
|
||||||
if door_x + 1 >= 0 and door_x + 1 < map_size.x and y >= 0 and y < map_size.y:
|
if door_x + 1 >= 0 and door_x + 1 < map_size.x and y >= 0 and y < map_size.y:
|
||||||
grid[door_x + 1][y] = 3 # Corridor (middle column of door)
|
grid[door_x + 1][y] = 3 # Corridor (middle column of door)
|
||||||
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
|
tile_grid[door_x + 1][y] = FLOOR_BASE
|
||||||
tile_grid[door_x + 1][y] = floor_tile
|
|
||||||
|
|
||||||
# Create door on BOTTOM wall of top room (3x2 tiles - 3 wide, 2 tall)
|
# Create door on BOTTOM wall of top room (3x2 tiles - 3 wide, 2 tall)
|
||||||
# Door is placed ON the wall, replacing the 2-tile tall wall
|
# Door is placed ON the wall, replacing the 2-tile tall wall
|
||||||
@@ -664,8 +712,7 @@ func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid:
|
|||||||
tile_grid[x][y] = door_tile_start + Vector2i(door_dx, door_dy)
|
tile_grid[x][y] = door_tile_start + Vector2i(door_dx, door_dy)
|
||||||
if door_dx == 1 and door_dy == 0:
|
if door_dx == 1 and door_dy == 0:
|
||||||
grid[x][y] = 1 # Floor
|
grid[x][y] = 1 # Floor
|
||||||
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
|
tile_grid[x][y] = FLOOR_BASE
|
||||||
tile_grid[x][y] = floor_tile
|
|
||||||
|
|
||||||
# Also create door on TOP wall of bottom room (if there's a gap)
|
# Also create door on TOP wall of bottom room (if there's a gap)
|
||||||
if corridor_length > 0:
|
if corridor_length > 0:
|
||||||
@@ -682,8 +729,7 @@ func _create_corridor_between_rooms(room1: Dictionary, room2: Dictionary, grid:
|
|||||||
tile_grid[x][y] = bottom_door_tile_start + Vector2i(door_dx, door_dy)
|
tile_grid[x][y] = bottom_door_tile_start + Vector2i(door_dx, door_dy)
|
||||||
if door_dx == 1 and door_dy == 1:
|
if door_dx == 1 and door_dy == 1:
|
||||||
grid[x][y] = 1 # Floor
|
grid[x][y] = 1 # Floor
|
||||||
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
|
tile_grid[x][y] = FLOOR_BASE
|
||||||
tile_grid[x][y] = floor_tile
|
|
||||||
|
|
||||||
# CRITICAL: room1 = room the door is ON (top room for vertical doors)
|
# CRITICAL: room1 = room the door is ON (top room for vertical doors)
|
||||||
# room2 = room the door leads TO (bottom room for vertical doors)
|
# room2 = room the door leads TO (bottom room for vertical doors)
|
||||||
@@ -1037,7 +1083,7 @@ func _add_hole_to_room(room: Dictionary, grid: Array, tile_grid: Array, map_size
|
|||||||
var hole_x = rng.randi_range(floor_min_x, max_x)
|
var hole_x = rng.randi_range(floor_min_x, max_x)
|
||||||
var hole_y = rng.randi_range(floor_min_y, max_y)
|
var hole_y = rng.randi_range(floor_min_y, max_y)
|
||||||
|
|
||||||
# Create hole (back to wall) - use inner wall tiles
|
# Create hole (back to wall) - use fallout inner corner/edge tiles; cleanup pass will refine edges
|
||||||
# Only create hole if the position is currently a floor tile
|
# Only create hole if the position is currently a floor tile
|
||||||
for x in range(hole_x, hole_x + hole_size):
|
for x in range(hole_x, hole_x + hole_size):
|
||||||
for y in range(hole_y, hole_y + hole_size):
|
for y in range(hole_y, hole_y + hole_size):
|
||||||
@@ -1045,18 +1091,17 @@ func _add_hole_to_room(room: Dictionary, grid: Array, tile_grid: Array, map_size
|
|||||||
# Only create hole if it's currently a floor tile
|
# Only create hole if it's currently a floor tile
|
||||||
if grid[x][y] == 1: # Floor
|
if grid[x][y] == 1: # Floor
|
||||||
grid[x][y] = 0 # Wall
|
grid[x][y] = 0 # Wall
|
||||||
# Use inner wall tiles for holes
|
# Fallout corner tiles for hole corners; rest get center (cleanup will set edges)
|
||||||
if x == hole_x and y == hole_y:
|
if x == hole_x and y == hole_y:
|
||||||
tile_grid[x][y] = INNER_WALL_TOP_LEFT
|
tile_grid[x][y] = FALLOUT_CORNER_INNER_UP_LEFT
|
||||||
elif x == hole_x + hole_size - 1 and y == hole_y:
|
elif x == hole_x + hole_size - 1 and y == hole_y:
|
||||||
tile_grid[x][y] = INNER_WALL_TOP_RIGHT
|
tile_grid[x][y] = FALLOUT_CORNER_INNER_UP_RIGHT
|
||||||
elif x == hole_x and y == hole_y + hole_size - 1:
|
elif x == hole_x and y == hole_y + hole_size - 1:
|
||||||
tile_grid[x][y] = INNER_WALL_BOTTOM_LEFT
|
tile_grid[x][y] = FALLOUT_CORNER_INNER_DOWN_LEFT
|
||||||
elif x == hole_x + hole_size - 1 and y == hole_y + hole_size - 1:
|
elif x == hole_x + hole_size - 1 and y == hole_y + hole_size - 1:
|
||||||
tile_grid[x][y] = INNER_WALL_BOTTOM_RIGHT
|
tile_grid[x][y] = FALLOUT_CORNER_INNER_DOWN_RIGHT
|
||||||
else:
|
else:
|
||||||
# Use default wall tile for interior of hole
|
tile_grid[x][y] = FALLOUT_CENTER
|
||||||
tile_grid[x][y] = WALL_TOP_UPPER
|
|
||||||
|
|
||||||
func _find_farthest_room(all_rooms: Array, start_index: int) -> int:
|
func _find_farthest_room(all_rooms: Array, start_index: int) -> int:
|
||||||
var start_room = all_rooms[start_index]
|
var start_room = all_rooms[start_index]
|
||||||
@@ -1250,6 +1295,200 @@ func _render_room_walls(all_rooms: Array, grid: Array, tile_grid: Array, map_siz
|
|||||||
if grid[right_x_right][y] != 2: # Not a door (don't overwrite door tiles)
|
if grid[right_x_right][y] != 2: # Not a door (don't overwrite door tiles)
|
||||||
tile_grid[right_x_right][y] = WALL_RIGHT_RIGHT
|
tile_grid[right_x_right][y] = WALL_RIGHT_RIGHT
|
||||||
|
|
||||||
|
func _cleanup_compound_room_corners_and_walls(all_rooms: Array, grid: Array, tile_grid: Array, map_size: Vector2i):
|
||||||
|
# Fix inner walls and corners for rooms that have holes (compound shapes).
|
||||||
|
# Use fallout inner/corner tiles for hole boundaries (larger holes get proper corners and edges).
|
||||||
|
for room in all_rooms:
|
||||||
|
var rx = room.x
|
||||||
|
var ry = room.y
|
||||||
|
var rw = room.w
|
||||||
|
var rh = room.h
|
||||||
|
# Inner area: skip outer 2-tile perimeter (walls); only consider wall cells inside the room
|
||||||
|
for x in range(rx + 2, rx + rw - 2):
|
||||||
|
for y in range(ry + 2, ry + rh - 2):
|
||||||
|
if x < 0 or x >= map_size.x or y < 0 or y >= map_size.y:
|
||||||
|
continue
|
||||||
|
if grid[x][y] != 0:
|
||||||
|
continue # Not a wall (e.g. floor, door, corridor)
|
||||||
|
# Wall cell inside room - check 4-neighbors for floor (1) or corridor (3)
|
||||||
|
var floor_left = (x - 1 >= 0 and (grid[x - 1][y] == 1 or grid[x - 1][y] == 3))
|
||||||
|
var floor_right = (x + 1 < map_size.x and (grid[x + 1][y] == 1 or grid[x + 1][y] == 3))
|
||||||
|
var floor_up = (y - 1 >= 0 and (grid[x][y - 1] == 1 or grid[x][y - 1] == 3))
|
||||||
|
var floor_down = (y + 1 < map_size.y and (grid[x][y + 1] == 1 or grid[x][y + 1] == 3))
|
||||||
|
# Inner corner: two perpendicular floor neighbors → fallout corner tiles
|
||||||
|
if floor_right and floor_down:
|
||||||
|
tile_grid[x][y] = FALLOUT_CORNER_INNER_UP_LEFT
|
||||||
|
elif floor_left and floor_down:
|
||||||
|
tile_grid[x][y] = FALLOUT_CORNER_INNER_UP_RIGHT
|
||||||
|
elif floor_right and floor_up:
|
||||||
|
tile_grid[x][y] = FALLOUT_CORNER_INNER_DOWN_LEFT
|
||||||
|
elif floor_left and floor_up:
|
||||||
|
tile_grid[x][y] = FALLOUT_CORNER_INNER_DOWN_RIGHT
|
||||||
|
# Inner edge: single floor neighbor → fallout inner edge tiles
|
||||||
|
elif floor_down:
|
||||||
|
tile_grid[x][y] = FALLOUT_INNER_UP
|
||||||
|
elif floor_up:
|
||||||
|
tile_grid[x][y] = FALLOUT_INNER_DOWN
|
||||||
|
elif floor_right:
|
||||||
|
tile_grid[x][y] = FALLOUT_INNER_LEFT
|
||||||
|
elif floor_left:
|
||||||
|
tile_grid[x][y] = FALLOUT_INNER_RIGHT
|
||||||
|
# Wall with no floor neighbor (hole interior) → fallout center
|
||||||
|
else:
|
||||||
|
tile_grid[x][y] = FALLOUT_CENTER
|
||||||
|
|
||||||
|
func _is_wall_at(grid: Array, map_size: Vector2i, x: int, y: int) -> bool:
|
||||||
|
if x < 0 or x >= map_size.x or y < 0 or y >= map_size.y:
|
||||||
|
return false
|
||||||
|
return grid[x][y] == 0 or grid[x][y] == 2 # wall or door
|
||||||
|
|
||||||
|
func _get_fallout_tile_for_floor(grid: Array, map_size: Vector2i, x: int, y: int) -> Vector2i:
|
||||||
|
# Choose fallout tile based on adjacent walls (up/right/down/left). Default: fallout_center.
|
||||||
|
var w_up = _is_wall_at(grid, map_size, x, y - 1)
|
||||||
|
var w_right = _is_wall_at(grid, map_size, x + 1, y)
|
||||||
|
var w_down = _is_wall_at(grid, map_size, x, y + 1)
|
||||||
|
var w_left = _is_wall_at(grid, map_size, x - 1, y)
|
||||||
|
# Corner tiles: exactly 2 walls (numpad 1/3/7/9) — check first so they take precedence
|
||||||
|
if w_down and w_left and not w_up and not w_right:
|
||||||
|
return FALLOUT_CORNER_INNER_UP_RIGHT # numpad 1: down+left
|
||||||
|
if w_down and w_right and not w_up and not w_left:
|
||||||
|
return FALLOUT_CORNER_INNER_UP_LEFT # numpad 3: down+right
|
||||||
|
if w_up and w_left and not w_down and not w_right:
|
||||||
|
return FALLOUT_CORNER_INNER_DOWN_RIGHT # numpad 7: up+left
|
||||||
|
if w_up and w_right and not w_down and not w_left:
|
||||||
|
return FALLOUT_CORNER_INNER_DOWN_LEFT # numpad 9: up+right
|
||||||
|
# Inner two-wall (wall up+left, up+right, down+right, down+left) — 3 or 4 walls
|
||||||
|
if w_up and w_left:
|
||||||
|
return FALLOUT_INNER_UP_LEFT
|
||||||
|
if w_up and w_right:
|
||||||
|
return FALLOUT_INNER_UP_RIGHT
|
||||||
|
if w_down and w_right:
|
||||||
|
return FALLOUT_INNER_DOWN_RIGHT
|
||||||
|
if w_down and w_left:
|
||||||
|
return FALLOUT_INNER_DOWN_LEFT
|
||||||
|
# Single wall (edge only)
|
||||||
|
if w_up and not w_right and not w_down and not w_left:
|
||||||
|
return FALLOUT_INNER_UP
|
||||||
|
if w_right and not w_up and not w_down and not w_left:
|
||||||
|
return FALLOUT_INNER_RIGHT
|
||||||
|
if w_down and not w_up and not w_left and not w_right:
|
||||||
|
return FALLOUT_INNER_DOWN
|
||||||
|
if w_left and not w_up and not w_right and not w_down:
|
||||||
|
return FALLOUT_INNER_LEFT
|
||||||
|
return FALLOUT_CENTER
|
||||||
|
|
||||||
|
func _pick_decorated_tile(rng: RandomNumberGenerator) -> Vector2i:
|
||||||
|
# (16,16) = most common (plain/non-decorated); then (1,15)-(21,15), (0,16)-(14,16)
|
||||||
|
if rng.randf() < 0.5:
|
||||||
|
return FLOOR_DECORATED_PLAIN
|
||||||
|
var roll = rng.randi() % 36 # 21 + 15 = 36 other options
|
||||||
|
if roll < 21:
|
||||||
|
return FLOOR_DECORATED_RANGE_1_START + Vector2i(roll, 0)
|
||||||
|
return FLOOR_DECORATED_RANGE_2_START + Vector2i(roll - 21, 0)
|
||||||
|
|
||||||
|
func _fill_decorated_tile_grid(grid: Array, decorated_tile_grid: Array, map_size: Vector2i, rng: RandomNumberGenerator):
|
||||||
|
for x in range(map_size.x):
|
||||||
|
for y in range(map_size.y):
|
||||||
|
if grid[x][y] == 1 or grid[x][y] == 3: # Floor or corridor only (not doors)
|
||||||
|
decorated_tile_grid[x][y] = _pick_decorated_tile(rng)
|
||||||
|
|
||||||
|
func _is_tile_in_room_interior(tx: int, ty: int, room: Dictionary) -> bool:
|
||||||
|
# Room interior excludes 2-tile walls: room.x+2..room.x+room.w-2, room.y+2..room.y+room.h-2
|
||||||
|
if room.is_empty():
|
||||||
|
return false
|
||||||
|
return tx >= room.x + 2 and tx < room.x + room.w - 2 and ty >= room.y + 2 and ty < room.y + room.h - 2
|
||||||
|
|
||||||
|
func _fill_cracked_tile_grid(grid: Array, cracked_tile_grid: Array, map_size: Vector2i, all_doors: Array, start_room: Dictionary, rng: RandomNumberGenerator):
|
||||||
|
const CHANCE = 0.05 # Not so common
|
||||||
|
for x in range(map_size.x):
|
||||||
|
for y in range(map_size.y):
|
||||||
|
if grid[x][y] != 1:
|
||||||
|
continue # Only floor (never corridors, doors)
|
||||||
|
if _is_tile_blocked_for_fallout(x, y, grid, map_size, all_doors):
|
||||||
|
continue
|
||||||
|
if not start_room.is_empty() and _is_tile_in_room_interior(x, y, start_room):
|
||||||
|
continue # No cracked tiles in start room
|
||||||
|
if rng.randf() >= CHANCE:
|
||||||
|
continue
|
||||||
|
cracked_tile_grid[x][y] = true
|
||||||
|
|
||||||
|
func _is_tile_blocked_for_fallout(x: int, y: int, _grid: Array, _map_size: Vector2i, all_doors: Array) -> bool:
|
||||||
|
# True if (x,y) must NOT get a fallout tile: inside a door (either side), or directly in front of a door
|
||||||
|
# Each connection has TWO door frames (room1 side and room2 side); we must exclude both
|
||||||
|
for door in all_doors:
|
||||||
|
var dx: int = door.x
|
||||||
|
var dy: int = door.y
|
||||||
|
var room2 = door.get("room2", null)
|
||||||
|
if door.get("dir", "E") == "E":
|
||||||
|
# Horizontal: door frame is 2 wide, 3 tall
|
||||||
|
# Room1 (left) door: (dx, dy) to (dx+2, dy+3)
|
||||||
|
if x >= dx and x < dx + 2 and y >= dy and y < dy + 3:
|
||||||
|
return true
|
||||||
|
# Room2 (right) door: (room2.x, dy) to (room2.x+2, dy+3)
|
||||||
|
if room2 is Dictionary:
|
||||||
|
var r2x: int = room2.x
|
||||||
|
if x >= r2x and x < r2x + 2 and y >= dy and y < dy + 3:
|
||||||
|
return true
|
||||||
|
# Directly in front of door (one tile into room from door)
|
||||||
|
if x == dx - 1 and y >= dy and y < dy + 3:
|
||||||
|
return true
|
||||||
|
if x == dx + 2 and y >= dy and y < dy + 3:
|
||||||
|
return true
|
||||||
|
if room2 is Dictionary:
|
||||||
|
var r2x: int = room2.x
|
||||||
|
if x == r2x - 1 and y >= dy and y < dy + 3:
|
||||||
|
return true
|
||||||
|
if x == r2x + 2 and y >= dy and y < dy + 3:
|
||||||
|
return true
|
||||||
|
else:
|
||||||
|
# Vertical (S): door frame is 3 wide, 2 tall
|
||||||
|
# Room1 (top) door: (dx, dy) to (dx+3, dy+2)
|
||||||
|
if x >= dx and x < dx + 3 and y >= dy and y < dy + 2:
|
||||||
|
return true
|
||||||
|
# Room2 (bottom) door: (dx, room2.y) to (dx+3, room2.y+2)
|
||||||
|
if room2 is Dictionary:
|
||||||
|
var r2y: int = room2.y
|
||||||
|
if x >= dx and x < dx + 3 and y >= r2y and y < r2y + 2:
|
||||||
|
return true
|
||||||
|
if x >= dx and x < dx + 3 and y == dy - 1:
|
||||||
|
return true
|
||||||
|
if x >= dx and x < dx + 3 and y == dy + 2:
|
||||||
|
return true
|
||||||
|
if room2 is Dictionary:
|
||||||
|
var r2y: int = room2.y
|
||||||
|
if x >= dx and x < dx + 3 and y == r2y - 1:
|
||||||
|
return true
|
||||||
|
if x >= dx and x < dx + 3 and y == r2y + 2:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
func _render_fallout_tiles(grid: Array, tile_grid: Array, map_size: Vector2i, all_doors: Array, start_room: Dictionary, rng: RandomNumberGenerator):
|
||||||
|
# Replace a small fraction of floor tiles with fallout; never in doors, corridors, in front of doors, or in start room
|
||||||
|
const CHANCE = 0.08
|
||||||
|
for x in range(map_size.x):
|
||||||
|
for y in range(map_size.y):
|
||||||
|
if grid[x][y] != 1:
|
||||||
|
continue # Only floor (corridors are 3, door frame is 2)
|
||||||
|
if _is_tile_blocked_for_fallout(x, y, grid, map_size, all_doors):
|
||||||
|
continue
|
||||||
|
if not start_room.is_empty() and _is_tile_in_room_interior(x, y, start_room):
|
||||||
|
continue # No fallout tiles in start room
|
||||||
|
if rng.randf() >= CHANCE:
|
||||||
|
continue
|
||||||
|
tile_grid[x][y] = _get_fallout_tile_for_floor(grid, map_size, x, y)
|
||||||
|
|
||||||
|
func _is_fallout_atlas(tile: Vector2i) -> bool:
|
||||||
|
return tile == FALLOUT_CENTER \
|
||||||
|
or tile == FALLOUT_INNER_UP_LEFT or tile == FALLOUT_INNER_UP or tile == FALLOUT_INNER_UP_RIGHT \
|
||||||
|
or tile == FALLOUT_INNER_RIGHT or tile == FALLOUT_INNER_DOWN_RIGHT or tile == FALLOUT_INNER_DOWN or tile == FALLOUT_INNER_DOWN_LEFT or tile == FALLOUT_INNER_LEFT \
|
||||||
|
or tile == FALLOUT_CORNER_INNER_UP_LEFT or tile == FALLOUT_CORNER_INNER_UP_RIGHT or tile == FALLOUT_CORNER_INNER_DOWN_LEFT or tile == FALLOUT_CORNER_INNER_DOWN_RIGHT
|
||||||
|
|
||||||
|
func _clear_decorated_on_fallout(tile_grid: Array, decorated_tile_grid: Array, map_size: Vector2i):
|
||||||
|
for x in range(map_size.x):
|
||||||
|
for y in range(map_size.y):
|
||||||
|
if _is_fallout_atlas(tile_grid[x][y]):
|
||||||
|
decorated_tile_grid[x][y] = null
|
||||||
|
|
||||||
func _place_enemies_in_room(room: Dictionary, grid: Array, map_size: Vector2i, rng: RandomNumberGenerator, level: int = 1) -> Array:
|
func _place_enemies_in_room(room: Dictionary, grid: Array, map_size: Vector2i, rng: RandomNumberGenerator, level: int = 1) -> Array:
|
||||||
# Place enemies in a room, scaled by level
|
# Place enemies in a room, scaled by level
|
||||||
# Level 1: 0-2 enemies per room (fewer)
|
# Level 1: 0-2 enemies per room (fewer)
|
||||||
@@ -1857,7 +2096,7 @@ func _force_place_stairs(exit_room: Dictionary, grid: Array, tile_grid: Array, m
|
|||||||
LogManager.log("DungeonGenerator: Force placed " + str(stairs_dir) + " stairs at tile (" + str(stairs_data.x) + "," + str(stairs_data.y) + ") world pos: " + str(stairs_data.world_pos), LogManager.CATEGORY_DUNGEON)
|
LogManager.log("DungeonGenerator: Force placed " + str(stairs_dir) + " stairs at tile (" + str(stairs_data.x) + "," + str(stairs_data.y) + ") world pos: " + str(stairs_data.world_pos), LogManager.CATEGORY_DUNGEON)
|
||||||
return stairs_data
|
return stairs_data
|
||||||
|
|
||||||
func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size: Vector2i, all_doors: Array, all_enemies: Array, rng: RandomNumberGenerator, room_puzzle_data: Dictionary = {}) -> Array:
|
func _place_interactable_objects_in_room(room: Dictionary, grid: Array, tile_grid: Array, map_size: Vector2i, all_doors: Array, all_enemies: Array, rng: RandomNumberGenerator, room_puzzle_data: Dictionary = {}) -> Array:
|
||||||
# Place interactable objects in a room
|
# Place interactable objects in a room
|
||||||
# Small rooms (7-8 tiles): 0-1 objects
|
# Small rooms (7-8 tiles): 0-1 objects
|
||||||
# Medium rooms (9-10 tiles): 0-3 objects
|
# Medium rooms (9-10 tiles): 0-3 objects
|
||||||
@@ -2034,6 +2273,17 @@ func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size
|
|||||||
"room": room
|
"room": room
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# If an interactable spawns on a fallout tile, replace that tile with normal floor
|
||||||
|
for obj in objects:
|
||||||
|
if obj.has("position"):
|
||||||
|
var pos = obj.position
|
||||||
|
var tx = int(pos.x / tile_size)
|
||||||
|
var ty = int(pos.y / tile_size)
|
||||||
|
if tx >= 0 and tx < map_size.x and ty >= 0 and ty < map_size.y:
|
||||||
|
if _is_fallout_atlas(tile_grid[tx][ty]):
|
||||||
|
tile_grid[tx][ty] = FLOOR_BASE
|
||||||
|
LogManager.log("DungeonGenerator: Replaced fallout with normal floor at interactable spawn (" + str(tx) + "," + str(ty) + ")", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
return objects
|
return objects
|
||||||
|
|
||||||
func _is_valid_interactable_position(world_pos: Vector2, all_doors: Array, all_enemies: Array, room: Dictionary) -> bool:
|
func _is_valid_interactable_position(world_pos: Vector2, all_doors: Array, all_enemies: Array, room: Dictionary) -> bool:
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ var burn_damage_timer: float = 0.0 # Timer for burn damage ticks
|
|||||||
var position_z: float = 0.0
|
var position_z: float = 0.0
|
||||||
var velocity_z: float = 0.0
|
var velocity_z: float = 0.0
|
||||||
|
|
||||||
|
# Fallout (quicksand): humanoids sink like player then die; slimes rotate 45 + scale then die
|
||||||
|
var fallout_state: bool = false
|
||||||
|
var fallout_scale_progress: float = 1.0
|
||||||
|
var fallout_defeat_started: bool = false # Slime-like: simple scale/rotate then die
|
||||||
|
var died_from_fallout: bool = false # True when _die() was triggered by fallout (delay loot 0.3s)
|
||||||
|
const FALLOUT_SINK_DURATION: float = 0.5
|
||||||
|
const FALLOUT_CENTER_THRESHOLD: float = 2.0
|
||||||
|
const FALLOUT_LOOT_DELAY: float = 0.3 # Seconds after fallout death before spawning loot
|
||||||
|
|
||||||
# Animation
|
# Animation
|
||||||
enum Direction {DOWN = 0, LEFT = 1, RIGHT = 2, UP = 3, DOWN_LEFT = 4, DOWN_RIGHT = 5, UP_LEFT = 6, UP_RIGHT = 7}
|
enum Direction {DOWN = 0, LEFT = 1, RIGHT = 2, UP = 3, DOWN_LEFT = 4, DOWN_RIGHT = 5, UP_LEFT = 6, UP_RIGHT = 7}
|
||||||
var current_direction: Direction = Direction.DOWN
|
var current_direction: Direction = Direction.DOWN
|
||||||
@@ -118,6 +127,71 @@ func _physics_process(delta):
|
|||||||
burn_sprite.set_meta("burn_animation_timer", anim_timer)
|
burn_sprite.set_meta("burn_animation_timer", anim_timer)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Fallout: humanoid sinks like player (FALL anim) then dies; slime rotates 45 + scale then dies
|
||||||
|
if fallout_state:
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
fallout_scale_progress -= delta / FALLOUT_SINK_DURATION
|
||||||
|
if fallout_scale_progress <= 0.0:
|
||||||
|
died_from_fallout = true
|
||||||
|
call_deferred("_die")
|
||||||
|
return
|
||||||
|
scale = Vector2.ONE * max(0.0, fallout_scale_progress)
|
||||||
|
if has_method("_set_animation"):
|
||||||
|
_set_animation("FALL")
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
if fallout_defeat_started:
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
fallout_scale_progress -= delta / FALLOUT_SINK_DURATION
|
||||||
|
if fallout_scale_progress <= 0.0:
|
||||||
|
died_from_fallout = true
|
||||||
|
call_deferred("_die")
|
||||||
|
return
|
||||||
|
scale = Vector2.ONE * max(0.0, fallout_scale_progress)
|
||||||
|
rotation = deg_to_rad(45.0)
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only ground enemies (position_z <= 0) can fall into fallout; bat has position_z 1 and ignores it
|
||||||
|
# Humanoid: use 16x16 box check (like player) so any part on fallout triggers; drag toward center then sink
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
var on_fallout = false
|
||||||
|
if position_z <= 0.0 and gw:
|
||||||
|
if "humanoid_type" in self and gw.has_method("_is_player_box_on_fallout_tile"):
|
||||||
|
on_fallout = gw._is_player_box_on_fallout_tile(global_position, 8.0)
|
||||||
|
elif gw.has_method("_is_position_on_fallout_tile"):
|
||||||
|
on_fallout = gw._is_position_on_fallout_tile(global_position)
|
||||||
|
if on_fallout:
|
||||||
|
if "humanoid_type" in self:
|
||||||
|
# Humanoid: drag toward tile center (quicksand pull) then sink when at center
|
||||||
|
var tile_center = gw._get_closest_fallout_tile_center(global_position) if gw.has_method("_get_closest_fallout_tile_center") else (gw._get_tile_center_at(global_position) if gw.has_method("_get_tile_center_at") else global_position)
|
||||||
|
var dist_to_center = global_position.distance_to(tile_center)
|
||||||
|
if dist_to_center < FALLOUT_CENTER_THRESHOLD:
|
||||||
|
global_position = tile_center
|
||||||
|
fallout_state = true
|
||||||
|
fallout_scale_progress = 1.0
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
if has_method("_set_animation"):
|
||||||
|
_set_animation("FALL")
|
||||||
|
if has_node("SfxFallout"):
|
||||||
|
$SfxFallout.play()
|
||||||
|
else:
|
||||||
|
# Drag toward center (quicksand pull)
|
||||||
|
var dir = (tile_center - global_position).normalized()
|
||||||
|
const FALLOUT_DRAG_STRENGTH: float = 820.0
|
||||||
|
velocity = dir * FALLOUT_DRAG_STRENGTH * get_process_delta_time()
|
||||||
|
if has_method("_set_animation"):
|
||||||
|
_set_animation("RUN")
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Slime-like: rotate 45 and scale down then die
|
||||||
|
fallout_defeat_started = true
|
||||||
|
fallout_scale_progress = 1.0
|
||||||
|
rotation = deg_to_rad(45.0)
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
|
||||||
# Update attack timer
|
# Update attack timer
|
||||||
if attack_timer > 0:
|
if attack_timer > 0:
|
||||||
attack_timer -= delta
|
attack_timer -= delta
|
||||||
@@ -137,6 +211,14 @@ func _physics_process(delta):
|
|||||||
if not is_knocked_back:
|
if not is_knocked_back:
|
||||||
_ai_behavior(delta)
|
_ai_behavior(delta)
|
||||||
|
|
||||||
|
# Slime, rat, humanoid: try to avoid stepping onto fallout (position_z <= 0 = ground enemies only; bat has position_z 1)
|
||||||
|
if position_z <= 0.0 and not fallout_state and not fallout_defeat_started and velocity.length_squared() > 1.0:
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_is_position_on_fallout_tile"):
|
||||||
|
var step = velocity.normalized() * 18.0
|
||||||
|
if game_world._is_position_on_fallout_tile(global_position + step):
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
|
||||||
# Move
|
# Move
|
||||||
move_and_slide()
|
move_and_slide()
|
||||||
|
|
||||||
@@ -752,7 +834,13 @@ func _die():
|
|||||||
if game_world and game_world.has_method("_sync_exp_text_at_position") and multiplayer.has_multiplayer_peer():
|
if game_world and game_world.has_method("_sync_exp_text_at_position") and multiplayer.has_multiplayer_peer():
|
||||||
game_world._sync_exp_text_at_position.rpc(exp_per_player, global_position)
|
game_world._sync_exp_text_at_position.rpc(exp_per_player, global_position)
|
||||||
|
|
||||||
# Spawn loot immediately (before death animation)
|
# Spawn loot (immediately, or after 0.3s if died from fallout so it appears after sink)
|
||||||
|
if died_from_fallout:
|
||||||
|
get_tree().create_timer(FALLOUT_LOOT_DELAY).timeout.connect(func():
|
||||||
|
if is_instance_valid(self):
|
||||||
|
_spawn_loot()
|
||||||
|
)
|
||||||
|
else:
|
||||||
_spawn_loot()
|
_spawn_loot()
|
||||||
|
|
||||||
# Sync death to all clients (only server sends RPC)
|
# Sync death to all clients (only server sends RPC)
|
||||||
@@ -932,10 +1020,11 @@ func _spawn_loot():
|
|||||||
ItemLootHelper.spawn_item_loot(item, safe_spawn_pos, entities_node, game_world)
|
ItemLootHelper.spawn_item_loot(item, safe_spawn_pos, entities_node, game_world)
|
||||||
LogManager.log(str(name) + " ✓ dropped item #" + str(i + 1) + ": " + str(item.item_name) + " at " + str(safe_spawn_pos) + " (LCK boost: " + str(item_rarity_boost) + ")", LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " ✓ dropped item #" + str(i + 1) + ": " + str(item.item_name) + " at " + str(safe_spawn_pos) + " (LCK boost: " + str(item_rarity_boost) + ")", LogManager.CATEGORY_ENEMY)
|
||||||
else:
|
else:
|
||||||
# Spawn regular loot (coin or food)
|
# Spawn regular loot (coin or food) - start at position_z 1 to avoid falling into fallout
|
||||||
var loot = loot_scene.instantiate()
|
var loot = loot_scene.instantiate()
|
||||||
entities_node.add_child(loot)
|
entities_node.add_child(loot)
|
||||||
loot.global_position = safe_spawn_pos
|
loot.global_position = safe_spawn_pos
|
||||||
|
loot.position_z = 1.0 if "position_z" in loot else 0.0
|
||||||
loot.loot_type = loot_type
|
loot.loot_type = loot_type
|
||||||
# Set initial velocity before _ready() processes
|
# Set initial velocity before _ready() processes
|
||||||
loot.velocity = initial_velocity
|
loot.velocity = initial_velocity
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ func _ready():
|
|||||||
|
|
||||||
state_timer = idle_duration
|
state_timer = idle_duration
|
||||||
|
|
||||||
|
# Bats fly: permanent position_z 1 so they ignore fallout tiles
|
||||||
|
position_z = 1.0
|
||||||
|
|
||||||
# CRITICAL: Ensure collision mask is set correctly (walls are on layer 7 = bit 6 = 64)
|
# CRITICAL: Ensure collision mask is set correctly (walls are on layer 7 = bit 6 = 64)
|
||||||
collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
|
collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ func _ready() -> void:
|
|||||||
move_speed = 16.8 # 60% of 28.0 - slower chase/random movement
|
move_speed = 16.8 # 60% of 28.0 - slower chase/random movement
|
||||||
damage = SNATCH_DAMAGE
|
damage = SNATCH_DAMAGE
|
||||||
exp_reward = 8.0
|
exp_reward = 8.0
|
||||||
|
# Enemy hand is a special enemy: always at position_z 1 so it never falls into fallout
|
||||||
|
position_z = 1.0
|
||||||
collision_layer = 2
|
collision_layer = 2
|
||||||
collision_mask = 1 | 2 | 64
|
collision_mask = 1 | 2 | 64
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,12 @@ const ANIMATIONS = {
|
|||||||
"loop": false,
|
"loop": false,
|
||||||
"nextAnimation": null
|
"nextAnimation": null
|
||||||
},
|
},
|
||||||
|
"FALL": {
|
||||||
|
"frames": [21],
|
||||||
|
"frameDurations": [500],
|
||||||
|
"loop": true,
|
||||||
|
"nextAnimation": null
|
||||||
|
},
|
||||||
"IDLE_HOLD": {
|
"IDLE_HOLD": {
|
||||||
"frames": [25],
|
"frames": [25],
|
||||||
"frameDurations": [500],
|
"frameDurations": [500],
|
||||||
@@ -246,8 +252,8 @@ func _ready():
|
|||||||
seed_value = hash(str(spawn_position) + str(humanoid_type) + str(random_component))
|
seed_value = hash(str(spawn_position) + str(humanoid_type) + str(random_component))
|
||||||
LogManager.log(str(name) + " appearance seed (randomized): " + str(seed_value) + " at spawn position: " + str(spawn_position) + " type: " + str(humanoid_type), LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " appearance seed (randomized): " + str(seed_value) + " at spawn position: " + str(spawn_position) + " type: " + str(humanoid_type), LogManager.CATEGORY_ENEMY)
|
||||||
else:
|
else:
|
||||||
# Deterministic based on position and type only
|
# Deterministic based on position, type, and name so each enemy has unique look (name differs per spawn index)
|
||||||
seed_value = hash(str(spawn_position) + str(humanoid_type))
|
seed_value = hash(str(spawn_position) + str(humanoid_type) + str(name))
|
||||||
LogManager.log(str(name) + " appearance seed (deterministic): " + str(seed_value) + " at spawn position: " + str(spawn_position) + " type: " + str(humanoid_type), LogManager.CATEGORY_ENEMY)
|
LogManager.log(str(name) + " appearance seed (deterministic): " + str(seed_value) + " at spawn position: " + str(spawn_position) + " type: " + str(humanoid_type), LogManager.CATEGORY_ENEMY)
|
||||||
appearance_rng.seed = seed_value
|
appearance_rng.seed = seed_value
|
||||||
|
|
||||||
@@ -747,8 +753,8 @@ func _load_random_headgear():
|
|||||||
# Available headgears organized by category (using actual files found)
|
# Available headgears organized by category (using actual files found)
|
||||||
var headgear_categories = {
|
var headgear_categories = {
|
||||||
"": ["Headband.png"], # Direct files in Layer 6 - Headgears
|
"": ["Headband.png"], # Direct files in Layer 6 - Headgears
|
||||||
"Basic Assasin": [
|
"Basic Assassin": [
|
||||||
"AssasinBandanaBlack.png", "StalkerHoodBlack.png", "ThiefBandanaGreen.png"
|
"AssassinBandanaBlack.png", "StalkerHoodBlack.png", "ThiefBandanaGreen.png"
|
||||||
],
|
],
|
||||||
"Basic Mage": [
|
"Basic Mage": [
|
||||||
"EsperHatBlue.png", "HighMageHatCyan.png", "MageHatRed.png", "SorcererHoodCyan.png"
|
"EsperHatBlue.png", "HighMageHatCyan.png", "MageHatRed.png", "SorcererHoodCyan.png"
|
||||||
@@ -1138,6 +1144,15 @@ func _ai_behavior(delta):
|
|||||||
else:
|
else:
|
||||||
lost_target_timer = 0.0
|
lost_target_timer = 0.0
|
||||||
|
|
||||||
|
func _would_move_into_fallout(move_dir: Vector2, step: float = 24.0) -> bool:
|
||||||
|
if move_dir.length_squared() < 0.01:
|
||||||
|
return false
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if not gw or not gw.has_method("_is_position_on_fallout_tile"):
|
||||||
|
return false
|
||||||
|
var check_pos = global_position + move_dir.normalized() * step
|
||||||
|
return gw._is_position_on_fallout_tile(check_pos)
|
||||||
|
|
||||||
func _idle_behavior(_delta):
|
func _idle_behavior(_delta):
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
|
|
||||||
@@ -1166,13 +1181,17 @@ func _idle_behavior(_delta):
|
|||||||
|
|
||||||
func _wandering_behavior(_delta):
|
func _wandering_behavior(_delta):
|
||||||
# Patrolling at slower pace
|
# Patrolling at slower pace
|
||||||
# Pick a random direction at the start of wandering
|
# Pick a random direction at the start of wandering (avoid fallout)
|
||||||
if state_timer >= 1.9: # Pick direction at start
|
if state_timer >= 1.9: # Pick direction at start
|
||||||
var random_angle = randf() * PI * 2
|
var random_angle = randf() * PI * 2
|
||||||
var random_dir = Vector2(cos(random_angle), sin(random_angle))
|
var random_dir = Vector2(cos(random_angle), sin(random_angle))
|
||||||
|
if _would_move_into_fallout(random_dir):
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
_set_animation("IDLE")
|
||||||
|
else:
|
||||||
velocity = random_dir * move_speed * patrol_speed_multiplier # Slower when patrolling
|
velocity = random_dir * move_speed * patrol_speed_multiplier # Slower when patrolling
|
||||||
current_direction = _get_direction_from_vector(random_dir)
|
|
||||||
_set_animation("RUN")
|
_set_animation("RUN")
|
||||||
|
current_direction = _get_direction_from_vector(random_dir)
|
||||||
|
|
||||||
# Check if player enters vision while patrolling
|
# Check if player enters vision while patrolling
|
||||||
if target_player and _is_player_in_vision(target_player):
|
if target_player and _is_player_in_vision(target_player):
|
||||||
@@ -1315,6 +1334,12 @@ func _chasing_behavior(delta_arg):
|
|||||||
|
|
||||||
var desired_distance = 45.0
|
var desired_distance = 45.0
|
||||||
|
|
||||||
|
# Avoid walking into fallout (quicksand)
|
||||||
|
if _would_move_into_fallout(to_player):
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
current_direction = _get_direction_from_vector(to_player)
|
||||||
|
return
|
||||||
|
|
||||||
# Apply speed multiplier if blocking
|
# Apply speed multiplier if blocking
|
||||||
var speed_mult = 1.0
|
var speed_mult = 1.0
|
||||||
if is_blocking:
|
if is_blocking:
|
||||||
|
|||||||
@@ -42,16 +42,15 @@ func _ready():
|
|||||||
set_process(false)
|
set_process(false)
|
||||||
|
|
||||||
func _find_tilemap_layer():
|
func _find_tilemap_layer():
|
||||||
# Find tilemap layer to update switch visual
|
# Find TileMapLayerDecoratedGround to update switch visual (switches are drawn on decorated ground)
|
||||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
if game_world:
|
if game_world:
|
||||||
if "dungeon_tilemap_layer" in game_world:
|
if "dungeon_tilemap_layer_decorated" in game_world:
|
||||||
tilemap_layer = game_world.dungeon_tilemap_layer
|
tilemap_layer = game_world.dungeon_tilemap_layer_decorated
|
||||||
else:
|
else:
|
||||||
# Try to find it in Environment node
|
|
||||||
var environment = game_world.get_node_or_null("Environment")
|
var environment = game_world.get_node_or_null("Environment")
|
||||||
if environment:
|
if environment:
|
||||||
tilemap_layer = environment.get_node_or_null("DungeonLayer0")
|
tilemap_layer = environment.get_node_or_null("TileMapLayerDecoratedGround")
|
||||||
|
|
||||||
func _on_body_entered(body):
|
func _on_body_entered(body):
|
||||||
# Object entered the switch
|
# Object entered the switch
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ var fog_debug_lines: Array = []
|
|||||||
var dungeon_data: Dictionary = {}
|
var dungeon_data: Dictionary = {}
|
||||||
var dungeon_tilemap_layer: TileMapLayer = null
|
var dungeon_tilemap_layer: TileMapLayer = null
|
||||||
var dungeon_tilemap_layer_above: TileMapLayer = null
|
var dungeon_tilemap_layer_above: TileMapLayer = null
|
||||||
|
var dungeon_tilemap_layer_decorated: TileMapLayer = null
|
||||||
|
var dungeon_tilemap_layer_cracked: TileMapLayer = null
|
||||||
var current_level: int = 1
|
var current_level: int = 1
|
||||||
var dungeon_seed: int = 0
|
var dungeon_seed: int = 0
|
||||||
|
|
||||||
@@ -111,6 +113,28 @@ var clients_ready: Dictionary = {} # peer_id -> bool
|
|||||||
# Track dungeon syncs in progress (server only) - prevent multiple simultaneous syncs
|
# Track dungeon syncs in progress (server only) - prevent multiple simultaneous syncs
|
||||||
var dungeon_sync_in_progress: Dictionary = {} # peer_id -> bool
|
var dungeon_sync_in_progress: Dictionary = {} # peer_id -> bool
|
||||||
|
|
||||||
|
# Fallout tiles (quicksand): last safe tile center per player (for respawn after falling in)
|
||||||
|
var last_safe_position_by_player: Dictionary = {} # player node path or name -> Vector2
|
||||||
|
|
||||||
|
# Cracked floor: stand too long -> tile breaks and becomes fallout
|
||||||
|
var cracked_stand_timers: Dictionary = {} # "player_key|tx|ty" -> float (seconds on that tile)
|
||||||
|
const CRACKED_STAND_DURATION: float = 0.9 # Seconds standing on cracked tile before it breaks
|
||||||
|
const CRACKED_TILE_ATLAS: Vector2i = Vector2i(15, 16)
|
||||||
|
# Fallout tile atlas coords (for replacing floor when cracked tile breaks) - match dungeon_generator
|
||||||
|
const _FALLOUT_CENTER = Vector2i(10, 12)
|
||||||
|
const _FALLOUT_INNER_UP_LEFT = Vector2i(9, 11)
|
||||||
|
const _FALLOUT_INNER_UP = Vector2i(10, 11)
|
||||||
|
const _FALLOUT_INNER_UP_RIGHT = Vector2i(11, 11)
|
||||||
|
const _FALLOUT_INNER_RIGHT = Vector2i(11, 12)
|
||||||
|
const _FALLOUT_INNER_DOWN_RIGHT = Vector2i(11, 13)
|
||||||
|
const _FALLOUT_INNER_DOWN = Vector2i(10, 13)
|
||||||
|
const _FALLOUT_INNER_DOWN_LEFT = Vector2i(9, 13)
|
||||||
|
const _FALLOUT_INNER_LEFT = Vector2i(9, 12)
|
||||||
|
const _FALLOUT_CORNER_INNER_UP_LEFT = Vector2i(13, 10)
|
||||||
|
const _FALLOUT_CORNER_INNER_UP_RIGHT = Vector2i(16, 10)
|
||||||
|
const _FALLOUT_CORNER_INNER_DOWN_LEFT = Vector2i(13, 13)
|
||||||
|
const _FALLOUT_CORNER_INNER_DOWN_RIGHT = Vector2i(16, 13)
|
||||||
|
|
||||||
# Track chunk acknowledgments (server only) - for flow control
|
# Track chunk acknowledgments (server only) - for flow control
|
||||||
var dungeon_chunk_acks: Dictionary = {} # peer_id -> {chunk_idx -> bool, next_chunk_to_send -> int, chunks_data -> Array}
|
var dungeon_chunk_acks: Dictionary = {} # peer_id -> {chunk_idx -> bool, next_chunk_to_send -> int, chunks_data -> Array}
|
||||||
|
|
||||||
@@ -449,9 +473,12 @@ func _send_initial_client_sync_with_retry(peer_id: int, local_count: int, retry_
|
|||||||
_push_existing_players_state_to_client(peer_id)
|
_push_existing_players_state_to_client(peer_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sync broken interactable objects to the new client
|
# Sync broken interactable objects to the new client (immediate + delayed retry so joiner always gets it after objects exist)
|
||||||
# Wait a bit after dungeon sync to ensure objects are spawned first
|
|
||||||
call_deferred("_sync_broken_objects_to_client", peer_id)
|
call_deferred("_sync_broken_objects_to_client", peer_id)
|
||||||
|
get_tree().create_timer(2.0).timeout.connect(func():
|
||||||
|
if is_inside_tree() and multiplayer.is_server():
|
||||||
|
_sync_broken_objects_to_client(peer_id)
|
||||||
|
)
|
||||||
|
|
||||||
# Sync existing enemies (from spawners) to the new client
|
# Sync existing enemies (from spawners) to the new client
|
||||||
# Wait a bit after dungeon sync to ensure spawners are spawned first
|
# Wait a bit after dungeon sync to ensure spawners are spawned first
|
||||||
@@ -1029,6 +1056,7 @@ func _sync_loot_spawn(spawn_position: Vector2, loot_type: int, initial_velocity:
|
|||||||
loot.set_meta("loot_id", loot_id)
|
loot.set_meta("loot_id", loot_id)
|
||||||
entities_node.add_child(loot)
|
entities_node.add_child(loot)
|
||||||
loot.global_position = spawn_position
|
loot.global_position = spawn_position
|
||||||
|
loot.position_z = 1.0 if "position_z" in loot else 0.0
|
||||||
loot.loot_type = loot_type
|
loot.loot_type = loot_type
|
||||||
# Ensure key loot has a deterministic name for any legacy RPCs
|
# Ensure key loot has a deterministic name for any legacy RPCs
|
||||||
if loot_type == 4: # LootType.KEY
|
if loot_type == 4: # LootType.KEY
|
||||||
@@ -1064,6 +1092,7 @@ func _sync_item_loot_spawn(spawn_position: Vector2, item_data: Dictionary, initi
|
|||||||
loot.set_meta("loot_id", loot_id)
|
loot.set_meta("loot_id", loot_id)
|
||||||
entities_node.add_child(loot)
|
entities_node.add_child(loot)
|
||||||
loot.global_position = spawn_position
|
loot.global_position = spawn_position
|
||||||
|
loot.position_z = 1.0 if "position_z" in loot else 0.0
|
||||||
loot.loot_type = loot.LootType.ITEM
|
loot.loot_type = loot.LootType.ITEM
|
||||||
loot.item = item # Set the item instance
|
loot.item = item # Set the item instance
|
||||||
# Set initial velocity before _ready() processes
|
# Set initial velocity before _ready() processes
|
||||||
@@ -1900,6 +1929,67 @@ func _process(delta):
|
|||||||
peer_cleanup_timer = 0.0
|
peer_cleanup_timer = 0.0
|
||||||
_cleanup_disconnected_peers()
|
_cleanup_disconnected_peers()
|
||||||
|
|
||||||
|
# Cracked floor: only server (or single-player) checks stand time and breaks tiles.
|
||||||
|
# On server, check ALL players (host + joiners) so joiners can break cracked tiles too.
|
||||||
|
if dungeon_tilemap_layer_cracked and (multiplayer.is_server() or not multiplayer.has_multiplayer_peer()):
|
||||||
|
var players_to_check: Array = get_tree().get_nodes_in_group("player") if (multiplayer.is_server() and multiplayer.has_multiplayer_peer()) else (player_manager.get_local_players() if player_manager else [])
|
||||||
|
if players_to_check.is_empty() and player_manager:
|
||||||
|
players_to_check = player_manager.get_local_players()
|
||||||
|
for player in players_to_check:
|
||||||
|
if not is_instance_valid(player):
|
||||||
|
continue
|
||||||
|
var pos = player.global_position
|
||||||
|
if player.has_node("QuicksandArea"):
|
||||||
|
var qa = player.get_node("QuicksandArea")
|
||||||
|
if is_instance_valid(qa):
|
||||||
|
pos = qa.global_position
|
||||||
|
if not _is_position_on_cracked_tile(pos):
|
||||||
|
# Clear any timer for this player's other tiles
|
||||||
|
var to_remove = []
|
||||||
|
var player_key = str(player.get_path()) if player.is_inside_tree() else str(player.name)
|
||||||
|
for k in cracked_stand_timers.keys():
|
||||||
|
if k.begins_with(player_key + "|"):
|
||||||
|
to_remove.append(k)
|
||||||
|
for k in to_remove:
|
||||||
|
cracked_stand_timers.erase(k)
|
||||||
|
continue
|
||||||
|
var tile = _get_tile_coords_at_world(pos)
|
||||||
|
if tile.x < 0 or tile.y < 0:
|
||||||
|
continue
|
||||||
|
var key = (str(player.get_path()) if player.is_inside_tree() else str(player.name)) + "|" + str(tile.x) + "|" + str(tile.y)
|
||||||
|
cracked_stand_timers[key] = cracked_stand_timers.get(key, 0.0) + delta
|
||||||
|
if cracked_stand_timers[key] >= CRACKED_STAND_DURATION:
|
||||||
|
cracked_stand_timers.erase(key)
|
||||||
|
_break_cracked_tile(tile.x, tile.y)
|
||||||
|
# Slime, rat, humanoid: standing on cracked tiles can break them (they don't dodge cracked)
|
||||||
|
var enemies_to_check = get_tree().get_nodes_in_group("enemy")
|
||||||
|
for enemy in enemies_to_check:
|
||||||
|
if not is_instance_valid(enemy):
|
||||||
|
continue
|
||||||
|
if "is_dead" in enemy and enemy.is_dead:
|
||||||
|
continue
|
||||||
|
var ez = enemy.position_z if "position_z" in enemy else 0.0
|
||||||
|
if ez > 0.0:
|
||||||
|
continue
|
||||||
|
var pos = enemy.global_position
|
||||||
|
if not _is_position_on_cracked_tile(pos):
|
||||||
|
var enemy_key = (str(enemy.get_path()) if enemy.is_inside_tree() else str(enemy.name)) + "|"
|
||||||
|
var to_remove = []
|
||||||
|
for k in cracked_stand_timers.keys():
|
||||||
|
if k.begins_with(enemy_key):
|
||||||
|
to_remove.append(k)
|
||||||
|
for k in to_remove:
|
||||||
|
cracked_stand_timers.erase(k)
|
||||||
|
continue
|
||||||
|
var tile = _get_tile_coords_at_world(pos)
|
||||||
|
if tile.x < 0 or tile.y < 0:
|
||||||
|
continue
|
||||||
|
var key = (str(enemy.get_path()) if enemy.is_inside_tree() else str(enemy.name)) + "|" + str(tile.x) + "|" + str(tile.y)
|
||||||
|
cracked_stand_timers[key] = cracked_stand_timers.get(key, 0.0) + delta
|
||||||
|
if cracked_stand_timers[key] >= CRACKED_STAND_DURATION:
|
||||||
|
cracked_stand_timers.erase(key)
|
||||||
|
_break_cracked_tile(tile.x, tile.y)
|
||||||
|
|
||||||
func _check_client_buffers(current_time: float):
|
func _check_client_buffers(current_time: float):
|
||||||
# Check all client buffers and mark which ones should be skipped
|
# Check all client buffers and mark which ones should be skipped
|
||||||
if not multiplayer.multiplayer_peer is WebRTCMultiplayerPeer:
|
if not multiplayer.multiplayer_peer is WebRTCMultiplayerPeer:
|
||||||
@@ -2251,8 +2341,8 @@ func _update_mouse_cursor(delta: float):
|
|||||||
|
|
||||||
var mouse_direction = (target_world_pos - player_pos).normalized()
|
var mouse_direction = (target_world_pos - player_pos).normalized()
|
||||||
|
|
||||||
# Only update facing if mouse is far enough from player
|
# Only update facing if mouse is far enough from player (lock to down during fallout)
|
||||||
if mouse_direction.length() > 0.1:
|
if mouse_direction.length() > 0.1 and not (("fallout_state" in player) and player.fallout_state):
|
||||||
player._update_facing_from_mouse(mouse_direction)
|
player._update_facing_from_mouse(mouse_direction)
|
||||||
else:
|
else:
|
||||||
# Mouse is outside window - disable mouse control (use WASD/movement for direction)
|
# Mouse is outside window - disable mouse control (use WASD/movement for direction)
|
||||||
@@ -2408,6 +2498,267 @@ func _is_walkable_tile(tile_center: Vector2) -> bool:
|
|||||||
var v = grid[tile_x][tile_y]
|
var v = grid[tile_x][tile_y]
|
||||||
return v == 1 or v == 2 or v == 3
|
return v == 1 or v == 2 or v == 3
|
||||||
|
|
||||||
|
# Fallout (quicksand) tiles: CustomDataLayer "terrain" int value -1
|
||||||
|
const FALLOUT_TERRAIN_VALUE: int = -1
|
||||||
|
|
||||||
|
func _is_position_on_fallout_tile(world_pos: Vector2) -> bool:
|
||||||
|
if not dungeon_tilemap_layer:
|
||||||
|
return false
|
||||||
|
var tile_pos = dungeon_tilemap_layer.local_to_map(world_pos - dungeon_tilemap_layer.global_position)
|
||||||
|
var td = dungeon_tilemap_layer.get_cell_tile_data(tile_pos)
|
||||||
|
if not td:
|
||||||
|
return false
|
||||||
|
if not td.get_custom_data("terrain") is int:
|
||||||
|
return false
|
||||||
|
return td.get_custom_data("terrain") == FALLOUT_TERRAIN_VALUE
|
||||||
|
|
||||||
|
func _is_player_box_on_fallout_tile(player_center: Vector2, box_half_size: float = 8.0) -> bool:
|
||||||
|
# True if any part of the player's 16x16 box (center ± 8) is on a fallout tile (Link's Awakening style)
|
||||||
|
var corners = [
|
||||||
|
player_center,
|
||||||
|
player_center + Vector2(-box_half_size, -box_half_size),
|
||||||
|
player_center + Vector2(box_half_size, -box_half_size),
|
||||||
|
player_center + Vector2(-box_half_size, box_half_size),
|
||||||
|
player_center + Vector2(box_half_size, box_half_size),
|
||||||
|
]
|
||||||
|
for p in corners:
|
||||||
|
if _is_position_on_fallout_tile(p):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
func _get_tile_center_at(world_pos: Vector2) -> Vector2:
|
||||||
|
if not dungeon_tilemap_layer:
|
||||||
|
return world_pos
|
||||||
|
var tile_pos = dungeon_tilemap_layer.local_to_map(world_pos - dungeon_tilemap_layer.global_position)
|
||||||
|
return dungeon_tilemap_layer.map_to_local(tile_pos) + dungeon_tilemap_layer.global_position
|
||||||
|
|
||||||
|
func _get_closest_fallout_tile_center(world_pos: Vector2) -> Vector2:
|
||||||
|
if not dungeon_tilemap_layer:
|
||||||
|
return world_pos
|
||||||
|
var center_tile = dungeon_tilemap_layer.local_to_map(world_pos - dungeon_tilemap_layer.global_position)
|
||||||
|
var best_center: Vector2 = world_pos
|
||||||
|
var best_dist: float = 1e9
|
||||||
|
var search_radius = 3
|
||||||
|
for dx in range(-search_radius, search_radius + 1):
|
||||||
|
for dy in range(-search_radius, search_radius + 1):
|
||||||
|
var t = center_tile + Vector2i(dx, dy)
|
||||||
|
var tile_center = dungeon_tilemap_layer.map_to_local(t) + dungeon_tilemap_layer.global_position
|
||||||
|
var td = dungeon_tilemap_layer.get_cell_tile_data(t)
|
||||||
|
if not td:
|
||||||
|
continue
|
||||||
|
if not td.get_custom_data("terrain") is int:
|
||||||
|
continue
|
||||||
|
if td.get_custom_data("terrain") != FALLOUT_TERRAIN_VALUE:
|
||||||
|
continue
|
||||||
|
var d = world_pos.distance_squared_to(tile_center)
|
||||||
|
if d < best_dist:
|
||||||
|
best_dist = d
|
||||||
|
best_center = tile_center
|
||||||
|
return best_center
|
||||||
|
|
||||||
|
# Cracked floor: terrain -2 on TileMapLayerCrackedGround
|
||||||
|
func _is_position_on_cracked_tile(world_pos: Vector2) -> bool:
|
||||||
|
if not dungeon_tilemap_layer_cracked:
|
||||||
|
return false
|
||||||
|
var tile_pos = dungeon_tilemap_layer_cracked.local_to_map(world_pos - dungeon_tilemap_layer_cracked.global_position)
|
||||||
|
var td = dungeon_tilemap_layer_cracked.get_cell_tile_data(tile_pos)
|
||||||
|
if not td:
|
||||||
|
return false
|
||||||
|
if not td.get_custom_data("terrain") is int:
|
||||||
|
return false
|
||||||
|
return td.get_custom_data("terrain") == -2
|
||||||
|
|
||||||
|
func _get_tile_coords_at_world(world_pos: Vector2) -> Vector2i:
|
||||||
|
if not dungeon_tilemap_layer:
|
||||||
|
return Vector2i(-1, -1)
|
||||||
|
return dungeon_tilemap_layer.local_to_map(world_pos - dungeon_tilemap_layer.global_position)
|
||||||
|
|
||||||
|
func _is_wall_at_tile(tx: int, ty: int) -> bool:
|
||||||
|
if dungeon_data.is_empty() or not dungeon_data.has("grid") or not dungeon_data.has("map_size"):
|
||||||
|
return false
|
||||||
|
var grid = dungeon_data.grid
|
||||||
|
var map_size: Vector2i = dungeon_data.map_size
|
||||||
|
if tx < 0 or tx >= map_size.x or ty < 0 or ty >= map_size.y:
|
||||||
|
return false
|
||||||
|
var v = grid[tx][ty]
|
||||||
|
return v == 0 or v == 2
|
||||||
|
|
||||||
|
func _get_fallout_tile_for_floor_at(tx: int, ty: int) -> Vector2i:
|
||||||
|
var w_up = _is_wall_at_tile(tx, ty - 1)
|
||||||
|
var w_right = _is_wall_at_tile(tx + 1, ty)
|
||||||
|
var w_down = _is_wall_at_tile(tx, ty + 1)
|
||||||
|
var w_left = _is_wall_at_tile(tx - 1, ty)
|
||||||
|
if w_down and w_left and not w_up and not w_right:
|
||||||
|
return _FALLOUT_CORNER_INNER_UP_RIGHT
|
||||||
|
if w_down and w_right and not w_up and not w_left:
|
||||||
|
return _FALLOUT_CORNER_INNER_UP_LEFT
|
||||||
|
if w_up and w_left and not w_down and not w_right:
|
||||||
|
return _FALLOUT_CORNER_INNER_DOWN_RIGHT
|
||||||
|
if w_up and w_right and not w_down and not w_left:
|
||||||
|
return _FALLOUT_CORNER_INNER_DOWN_LEFT
|
||||||
|
if w_up and w_left:
|
||||||
|
return _FALLOUT_INNER_UP_LEFT
|
||||||
|
if w_up and w_right:
|
||||||
|
return _FALLOUT_INNER_UP_RIGHT
|
||||||
|
if w_down and w_right:
|
||||||
|
return _FALLOUT_INNER_DOWN_RIGHT
|
||||||
|
if w_down and w_left:
|
||||||
|
return _FALLOUT_INNER_DOWN_LEFT
|
||||||
|
if w_up and not w_right and not w_down and not w_left:
|
||||||
|
return _FALLOUT_INNER_UP
|
||||||
|
if w_right and not w_up and not w_down and not w_left:
|
||||||
|
return _FALLOUT_INNER_RIGHT
|
||||||
|
if w_down and not w_up and not w_left and not w_right:
|
||||||
|
return _FALLOUT_INNER_DOWN
|
||||||
|
if w_left and not w_up and not w_right and not w_down:
|
||||||
|
return _FALLOUT_INNER_LEFT
|
||||||
|
return _FALLOUT_CENTER
|
||||||
|
|
||||||
|
func _play_whoosh_at(world_pos: Vector2) -> void:
|
||||||
|
var whoosh = load("res://assets/audio/sfx/wizard/animevox/whoosh_1769364646131.wav") as AudioStream
|
||||||
|
if not whoosh:
|
||||||
|
return
|
||||||
|
var player = AudioStreamPlayer2D.new()
|
||||||
|
player.stream = whoosh
|
||||||
|
player.global_position = world_pos
|
||||||
|
player.bus = "Sfx"
|
||||||
|
add_child(player)
|
||||||
|
player.play()
|
||||||
|
player.finished.connect(player.queue_free)
|
||||||
|
|
||||||
|
func break_cracked_tiles_in_radius(world_center: Vector2, radius: float) -> void:
|
||||||
|
# Break any cracked tiles inside the given world-space circle. Only server performs the break.
|
||||||
|
# Clients (e.g. joiner's bomb) request the server to do it via RPC.
|
||||||
|
if not dungeon_tilemap_layer or not dungeon_tilemap_layer_cracked:
|
||||||
|
return
|
||||||
|
if multiplayer.has_multiplayer_peer() and not multiplayer.is_server():
|
||||||
|
_request_break_cracked_tiles_in_radius.rpc_id(1, world_center, radius)
|
||||||
|
return
|
||||||
|
var center_tile = _get_tile_coords_at_world(world_center)
|
||||||
|
if center_tile.x < 0 or center_tile.y < 0:
|
||||||
|
return
|
||||||
|
var tile_radius = int(ceil(radius / 16.0)) + 1
|
||||||
|
for dx in range(-tile_radius, tile_radius + 1):
|
||||||
|
for dy in range(-tile_radius, tile_radius + 1):
|
||||||
|
var tx = center_tile.x + dx
|
||||||
|
var ty = center_tile.y + dy
|
||||||
|
var tile_center = dungeon_tilemap_layer.map_to_local(Vector2i(tx, ty)) + dungeon_tilemap_layer.global_position
|
||||||
|
if world_center.distance_to(tile_center) > radius:
|
||||||
|
continue
|
||||||
|
if not _is_position_on_cracked_tile(tile_center):
|
||||||
|
continue
|
||||||
|
_break_cracked_tile(tx, ty)
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _request_break_cracked_tiles_in_radius(world_center: Vector2, radius: float) -> void:
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
break_cracked_tiles_in_radius(world_center, radius)
|
||||||
|
|
||||||
|
func _break_cracked_tile(tile_x: int, tile_y: int) -> void:
|
||||||
|
if not dungeon_tilemap_layer or dungeon_data.is_empty():
|
||||||
|
return
|
||||||
|
# Replace floor with fallout on main layer
|
||||||
|
var fallout_tile = _get_fallout_tile_for_floor_at(tile_x, tile_y)
|
||||||
|
dungeon_tilemap_layer.set_cell(Vector2i(tile_x, tile_y), 0, fallout_tile)
|
||||||
|
if dungeon_tilemap_layer_decorated:
|
||||||
|
dungeon_tilemap_layer_decorated.erase_cell(Vector2i(tile_x, tile_y))
|
||||||
|
if dungeon_tilemap_layer_cracked:
|
||||||
|
dungeon_tilemap_layer_cracked.erase_cell(Vector2i(tile_x, tile_y))
|
||||||
|
var tile_center = dungeon_tilemap_layer.map_to_local(Vector2i(tile_x, tile_y)) + dungeon_tilemap_layer.global_position
|
||||||
|
_play_whoosh_at(tile_center)
|
||||||
|
# Update dungeon_data so re-packed blob for joiners has correct floor (no separate broken list needed)
|
||||||
|
if multiplayer.is_server() and not dungeon_data.is_empty():
|
||||||
|
if dungeon_data.has("tile_grid") and tile_x >= 0 and tile_y >= 0:
|
||||||
|
var tg = dungeon_data.tile_grid
|
||||||
|
if tile_x < tg.size() and tile_y < tg[tile_x].size():
|
||||||
|
tg[tile_x][tile_y] = fallout_tile
|
||||||
|
if dungeon_data.has("decorated_tile_grid"):
|
||||||
|
var dg = dungeon_data.decorated_tile_grid
|
||||||
|
if tile_x < dg.size() and tile_y < dg[tile_x].size():
|
||||||
|
dg[tile_x][tile_y] = null
|
||||||
|
if dungeon_data.has("cracked_tile_grid"):
|
||||||
|
var cg = dungeon_data.cracked_tile_grid
|
||||||
|
if tile_x < cg.size() and tile_y < cg[tile_x].size():
|
||||||
|
cg[tile_x][tile_y] = false
|
||||||
|
if multiplayer.is_server() and multiplayer.has_multiplayer_peer():
|
||||||
|
_sync_cracked_tile_broke.rpc(tile_x, tile_y, fallout_tile.x, fallout_tile.y)
|
||||||
|
|
||||||
|
@rpc("authority", "reliable", "call_remote")
|
||||||
|
func _sync_cracked_tile_broke(tile_x: int, tile_y: int, fallout_atlas_x: int, fallout_atlas_y: int) -> void:
|
||||||
|
if multiplayer.is_server():
|
||||||
|
return
|
||||||
|
var fallout_tile = Vector2i(fallout_atlas_x, fallout_atlas_y)
|
||||||
|
if dungeon_tilemap_layer:
|
||||||
|
dungeon_tilemap_layer.set_cell(Vector2i(tile_x, tile_y), 0, fallout_tile)
|
||||||
|
if dungeon_tilemap_layer_decorated:
|
||||||
|
dungeon_tilemap_layer_decorated.erase_cell(Vector2i(tile_x, tile_y))
|
||||||
|
if dungeon_tilemap_layer_cracked:
|
||||||
|
dungeon_tilemap_layer_cracked.erase_cell(Vector2i(tile_x, tile_y))
|
||||||
|
var tile_center = dungeon_tilemap_layer.map_to_local(Vector2i(tile_x, tile_y)) + dungeon_tilemap_layer.global_position if dungeon_tilemap_layer else Vector2(tile_x * 16 + 8, tile_y * 16 + 8)
|
||||||
|
_play_whoosh_at(tile_center)
|
||||||
|
|
||||||
|
func update_last_safe_position_for_player(player: Node, world_pos: Vector2) -> void:
|
||||||
|
# Only store when position is on a non-fallout tile so we never remember a pit as safe
|
||||||
|
if _is_position_on_fallout_tile(world_pos):
|
||||||
|
return
|
||||||
|
var key: String = str(player.get_path()) if player.is_inside_tree() else str(player.name)
|
||||||
|
last_safe_position_by_player[key] = _get_tile_center_at(world_pos)
|
||||||
|
|
||||||
|
func _get_nearest_safe_tile_center(from_world_pos: Vector2) -> Vector2:
|
||||||
|
# Search outward from the tile at from_world_pos; return nearest safe tile (walkable, not fallout).
|
||||||
|
# Prioritize previously visited (explored) tiles so we never place the player in a wall or in unseen area.
|
||||||
|
if not dungeon_tilemap_layer or dungeon_data.is_empty() or not dungeon_data.has("map_size"):
|
||||||
|
return from_world_pos
|
||||||
|
var map_size: Vector2i = dungeon_data.map_size
|
||||||
|
var center_tile = dungeon_tilemap_layer.local_to_map(from_world_pos - dungeon_tilemap_layer.global_position)
|
||||||
|
# Pass 1: prefer explored tiles that are walkable and not fallout
|
||||||
|
if not explored_map.is_empty():
|
||||||
|
for r in range(0, 10):
|
||||||
|
for dx in range(-r, r + 1):
|
||||||
|
for dy in range(-r, r + 1):
|
||||||
|
if abs(dx) != r and abs(dy) != r:
|
||||||
|
continue
|
||||||
|
var t = center_tile + Vector2i(dx, dy)
|
||||||
|
if t.x < 0 or t.x >= map_size.x or t.y < 0 or t.y >= map_size.y:
|
||||||
|
continue
|
||||||
|
var tile_center = dungeon_tilemap_layer.map_to_local(t) + dungeon_tilemap_layer.global_position
|
||||||
|
if not _is_walkable_tile(tile_center):
|
||||||
|
continue
|
||||||
|
if _is_position_on_fallout_tile(tile_center):
|
||||||
|
continue
|
||||||
|
var idx = t.x + t.y * map_size.x
|
||||||
|
if idx >= 0 and idx < explored_map.size() and explored_map[idx] == 1:
|
||||||
|
return tile_center
|
||||||
|
# Pass 2: any walkable, non-fallout tile (never return a wall)
|
||||||
|
for r in range(0, 10):
|
||||||
|
for dx in range(-r, r + 1):
|
||||||
|
for dy in range(-r, r + 1):
|
||||||
|
if abs(dx) != r and abs(dy) != r:
|
||||||
|
continue
|
||||||
|
var t = center_tile + Vector2i(dx, dy)
|
||||||
|
if t.x < 0 or t.x >= map_size.x or t.y < 0 or t.y >= map_size.y:
|
||||||
|
continue
|
||||||
|
var tile_center = dungeon_tilemap_layer.map_to_local(t) + dungeon_tilemap_layer.global_position
|
||||||
|
if not _is_walkable_tile(tile_center):
|
||||||
|
continue
|
||||||
|
if _is_position_on_fallout_tile(tile_center):
|
||||||
|
continue
|
||||||
|
return tile_center
|
||||||
|
return from_world_pos
|
||||||
|
|
||||||
|
func get_last_safe_position_for_player(player: Node) -> Vector2:
|
||||||
|
var key: String = str(player.get_path()) if player.is_inside_tree() else str(player.name)
|
||||||
|
var stored: Vector2
|
||||||
|
if last_safe_position_by_player.has(key):
|
||||||
|
stored = last_safe_position_by_player[key]
|
||||||
|
else:
|
||||||
|
stored = player.global_position
|
||||||
|
# If stored position is on a fallout tile (e.g. never updated or room changed), use nearest safe tile
|
||||||
|
if _is_position_on_fallout_tile(stored):
|
||||||
|
return _get_nearest_safe_tile_center(stored)
|
||||||
|
return stored
|
||||||
|
|
||||||
func _get_adjacent_valid_spell_tile_centers(center_world_pos: Vector2, _player_pos: Vector2) -> Array:
|
func _get_adjacent_valid_spell_tile_centers(center_world_pos: Vector2, _player_pos: Vector2) -> Array:
|
||||||
var out: Array = []
|
var out: Array = []
|
||||||
if not dungeon_tilemap_layer or dungeon_data.is_empty() or not dungeon_data.has("grid"):
|
if not dungeon_tilemap_layer or dungeon_data.is_empty() or not dungeon_data.has("grid"):
|
||||||
@@ -3151,7 +3502,7 @@ func _generate_dungeon():
|
|||||||
|
|
||||||
LogManager.log("GameWorld: Dungeon generation completed successfully", LogManager.CATEGORY_DUNGEON)
|
LogManager.log("GameWorld: Dungeon generation completed successfully", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
# Dungeon shader color replacement: 13 original colors (wall x6, ground x5, fallout x2)
|
# Dungeon shader color replacement: 14 original colors (wall x6, ground x5, fallout x3)
|
||||||
const _DUNGEON_ORIGINALS: Array = [
|
const _DUNGEON_ORIGINALS: Array = [
|
||||||
Color(24 / 255.0, 59 / 255.0, 255 / 255.0), # 0 wall
|
Color(24 / 255.0, 59 / 255.0, 255 / 255.0), # 0 wall
|
||||||
Color(33 / 255.0, 50 / 255.0, 195 / 255.0), # 1 wall
|
Color(33 / 255.0, 50 / 255.0, 195 / 255.0), # 1 wall
|
||||||
@@ -3164,8 +3515,9 @@ const _DUNGEON_ORIGINALS: Array = [
|
|||||||
Color(48 / 255.0, 38 / 255.0, 20 / 255.0), # 8 ground
|
Color(48 / 255.0, 38 / 255.0, 20 / 255.0), # 8 ground
|
||||||
Color(143 / 255.0, 71 / 255.0, 112 / 255.0), # 9 ground
|
Color(143 / 255.0, 71 / 255.0, 112 / 255.0), # 9 ground
|
||||||
Color(106 / 255.0, 62 / 255.0, 57 / 255.0), # 10 ground
|
Color(106 / 255.0, 62 / 255.0, 57 / 255.0), # 10 ground
|
||||||
Color(69 / 255.0, 42 / 255.0, 31 / 255.0), # 11 fallout
|
Color(109 / 255.0, 33 / 255.0, 24 / 255.0), # 11 fallout
|
||||||
Color(53 / 255.0, 46 / 255.0, 26 / 255.0), # 12 fallout
|
Color(62 / 255.0, 29 / 255.0, 15 / 255.0), # 12 fallout
|
||||||
|
Color(2 / 255.0, 0 / 255.0, 4 / 255.0), # 13 fallout (near-black)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Original wall slots ordered light→dark (by luminance). Tile art uses these for highlight/mid/shadow.
|
# Original wall slots ordered light→dark (by luminance). Tile art uses these for highlight/mid/shadow.
|
||||||
@@ -3209,7 +3561,7 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
|||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.78, 0.48, 0.24), Color(0.84, 0.54, 0.30), Color(0.58, 0.36, 0.16),
|
Color(0.78, 0.48, 0.24), Color(0.84, 0.54, 0.30), Color(0.58, 0.36, 0.16),
|
||||||
Color(0.72, 0.44, 0.22), Color(0.66, 0.40, 0.20), Color(0.38, 0.30, 0.22),
|
Color(0.72, 0.44, 0.22), Color(0.66, 0.40, 0.20), Color(0.38, 0.30, 0.22),
|
||||||
Color(0.32, 0.28, 0.20),
|
Color(0.32, 0.28, 0.20), Color(0.28, 0.24, 0.18),
|
||||||
]
|
]
|
||||||
1: # 2️⃣ Crimson Void (blood / corruption / danger)
|
1: # 2️⃣ Crimson Void (blood / corruption / danger)
|
||||||
walls = [
|
walls = [
|
||||||
@@ -3219,7 +3571,7 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
|||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.22, 0.58, 0.62), Color(0.28, 0.64, 0.66), Color(0.18, 0.48, 0.52),
|
Color(0.22, 0.58, 0.62), Color(0.28, 0.64, 0.66), Color(0.18, 0.48, 0.52),
|
||||||
Color(0.20, 0.54, 0.58), Color(0.20, 0.50, 0.54), Color(0.26, 0.38, 0.34),
|
Color(0.20, 0.54, 0.58), Color(0.20, 0.50, 0.54), Color(0.26, 0.38, 0.34),
|
||||||
Color(0.22, 0.34, 0.30),
|
Color(0.22, 0.34, 0.30), Color(0.20, 0.30, 0.26),
|
||||||
]
|
]
|
||||||
2: # 3️⃣ Toxic Green (poison / nature / alchemy)
|
2: # 3️⃣ Toxic Green (poison / nature / alchemy)
|
||||||
walls = [
|
walls = [
|
||||||
@@ -3229,7 +3581,7 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
|||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.64, 0.36, 0.72), Color(0.70, 0.42, 0.78), Color(0.48, 0.26, 0.56),
|
Color(0.64, 0.36, 0.72), Color(0.70, 0.42, 0.78), Color(0.48, 0.26, 0.56),
|
||||||
Color(0.58, 0.32, 0.66), Color(0.54, 0.30, 0.62), Color(0.34, 0.26, 0.38),
|
Color(0.58, 0.32, 0.66), Color(0.54, 0.30, 0.62), Color(0.34, 0.26, 0.38),
|
||||||
Color(0.28, 0.22, 0.32),
|
Color(0.28, 0.22, 0.32), Color(0.24, 0.18, 0.28),
|
||||||
]
|
]
|
||||||
3: # 4️⃣ Stone Grey (industrial / ruins / UI neutral) — brightened for visibility
|
3: # 4️⃣ Stone Grey (industrial / ruins / UI neutral) — brightened for visibility
|
||||||
walls = [
|
walls = [
|
||||||
@@ -3239,7 +3591,7 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
|||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.58, 0.58, 0.60), Color(0.62, 0.62, 0.64), Color(0.46, 0.46, 0.48),
|
Color(0.58, 0.58, 0.60), Color(0.62, 0.62, 0.64), Color(0.46, 0.46, 0.48),
|
||||||
Color(0.55, 0.55, 0.57), Color(0.50, 0.50, 0.52), Color(0.40, 0.40, 0.42),
|
Color(0.55, 0.55, 0.57), Color(0.50, 0.50, 0.52), Color(0.40, 0.40, 0.42),
|
||||||
Color(0.36, 0.36, 0.38),
|
Color(0.36, 0.36, 0.38), Color(0.32, 0.32, 0.34),
|
||||||
]
|
]
|
||||||
4: # 5️⃣ Royal Purple (arcane royalty / bosses)
|
4: # 5️⃣ Royal Purple (arcane royalty / bosses)
|
||||||
walls = [
|
walls = [
|
||||||
@@ -3249,7 +3601,7 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
|||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.90, 0.68, 0.22), Color(0.94, 0.74, 0.28), Color(0.72, 0.52, 0.14),
|
Color(0.90, 0.68, 0.22), Color(0.94, 0.74, 0.28), Color(0.72, 0.52, 0.14),
|
||||||
Color(0.84, 0.60, 0.18), Color(0.78, 0.56, 0.16), Color(0.46, 0.36, 0.20),
|
Color(0.84, 0.60, 0.18), Color(0.78, 0.56, 0.16), Color(0.46, 0.36, 0.20),
|
||||||
Color(0.38, 0.30, 0.18),
|
Color(0.38, 0.30, 0.18), Color(0.32, 0.26, 0.16),
|
||||||
]
|
]
|
||||||
5: # 6️⃣ Desert Gold (sand / temples / sunlight)
|
5: # 6️⃣ Desert Gold (sand / temples / sunlight)
|
||||||
walls = [
|
walls = [
|
||||||
@@ -3259,7 +3611,7 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
|||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.22, 0.58, 0.62), Color(0.28, 0.64, 0.66), Color(0.18, 0.48, 0.52),
|
Color(0.22, 0.58, 0.62), Color(0.28, 0.64, 0.66), Color(0.18, 0.48, 0.52),
|
||||||
Color(0.20, 0.54, 0.58), Color(0.20, 0.50, 0.54), Color(0.26, 0.38, 0.34),
|
Color(0.20, 0.54, 0.58), Color(0.20, 0.50, 0.54), Color(0.26, 0.38, 0.34),
|
||||||
Color(0.22, 0.34, 0.30),
|
Color(0.22, 0.34, 0.30), Color(0.20, 0.30, 0.26),
|
||||||
]
|
]
|
||||||
6: # 7️⃣ Ancient Stone (medieval / castles / ruins)
|
6: # 7️⃣ Ancient Stone (medieval / castles / ruins)
|
||||||
walls = [
|
walls = [
|
||||||
@@ -3269,7 +3621,7 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
|||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.35, 0.28, 0.22), Color(0.40, 0.32, 0.26), Color(0.28, 0.22, 0.18),
|
Color(0.35, 0.28, 0.22), Color(0.40, 0.32, 0.26), Color(0.28, 0.22, 0.18),
|
||||||
Color(0.38, 0.30, 0.24), Color(0.32, 0.26, 0.20), Color(0.24, 0.20, 0.16),
|
Color(0.38, 0.30, 0.24), Color(0.32, 0.26, 0.20), Color(0.24, 0.20, 0.16),
|
||||||
Color(0.20, 0.16, 0.14),
|
Color(0.20, 0.16, 0.14), Color(0.18, 0.14, 0.12),
|
||||||
]
|
]
|
||||||
7: # 8️⃣ Infernal Lava (hell / bosses / damage)
|
7: # 8️⃣ Infernal Lava (hell / bosses / damage)
|
||||||
walls = [
|
walls = [
|
||||||
@@ -3279,11 +3631,11 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
|||||||
ground_fallout = [
|
ground_fallout = [
|
||||||
Color(0.32, 0.68, 0.48), Color(0.38, 0.74, 0.54), Color(0.22, 0.52, 0.36),
|
Color(0.32, 0.68, 0.48), Color(0.38, 0.74, 0.54), Color(0.22, 0.52, 0.36),
|
||||||
Color(0.28, 0.62, 0.44), Color(0.26, 0.58, 0.40), Color(0.26, 0.34, 0.28),
|
Color(0.28, 0.62, 0.44), Color(0.26, 0.58, 0.40), Color(0.26, 0.34, 0.28),
|
||||||
Color(0.22, 0.30, 0.24),
|
Color(0.22, 0.30, 0.24), Color(0.20, 0.26, 0.22),
|
||||||
]
|
]
|
||||||
_:
|
_:
|
||||||
return o.duplicate()
|
return o.duplicate()
|
||||||
if walls.size() == 6 and ground_fallout.size() == 7:
|
if walls.size() == 6 and ground_fallout.size() == 8:
|
||||||
walls = _reorder_wall_colors_by_luminance(walls)
|
walls = _reorder_wall_colors_by_luminance(walls)
|
||||||
var out: Array = []
|
var out: Array = []
|
||||||
out.append_array(walls)
|
out.append_array(walls)
|
||||||
@@ -3297,26 +3649,27 @@ func _apply_dungeon_color_scheme() -> void:
|
|||||||
var shader_res = load("res://shaders/game_world.gdshader") as Shader
|
var shader_res = load("res://shaders/game_world.gdshader") as Shader
|
||||||
if not shader_res:
|
if not shader_res:
|
||||||
return
|
return
|
||||||
for layer in [dungeon_tilemap_layer, dungeon_tilemap_layer_above]:
|
var env_node = get_node_or_null("Environment")
|
||||||
if not layer or not is_instance_valid(layer):
|
if not env_node:
|
||||||
|
return
|
||||||
|
for child in env_node.get_children():
|
||||||
|
if not child is TileMapLayer or not is_instance_valid(child):
|
||||||
continue
|
continue
|
||||||
|
var layer = child as TileMapLayer
|
||||||
var mat = layer.material
|
var mat = layer.material
|
||||||
if not mat or not (mat is ShaderMaterial):
|
if not mat or not (mat is ShaderMaterial):
|
||||||
mat = ShaderMaterial.new()
|
mat = ShaderMaterial.new()
|
||||||
mat.shader = shader_res
|
mat.shader = shader_res
|
||||||
layer.material = mat
|
layer.material = mat
|
||||||
var sm = mat as ShaderMaterial
|
var sm = mat as ShaderMaterial
|
||||||
for i in range(13):
|
for i in range(14):
|
||||||
var orig = _DUNGEON_ORIGINALS[i] as Color
|
var orig = _DUNGEON_ORIGINALS[i] as Color
|
||||||
var rpl = replace_colors[i] as Color
|
# Index 13: darkest fallout (2,0,4) — keep as-is, don't replace with scheme color
|
||||||
|
var rpl = (_DUNGEON_ORIGINALS[13] as Color) if i == 13 else (replace_colors[i] as Color)
|
||||||
sm.set_shader_parameter("original_" + str(i), orig)
|
sm.set_shader_parameter("original_" + str(i), orig)
|
||||||
sm.set_shader_parameter("replace_" + str(i), rpl)
|
sm.set_shader_parameter("replace_" + str(i), rpl)
|
||||||
# Index 13 unused; set to no-op (original same as replace, distinct from tile colors)
|
|
||||||
var neutral = Color(0.0, 0.0, 0.0, 1.0)
|
|
||||||
sm.set_shader_parameter("original_13", neutral)
|
|
||||||
sm.set_shader_parameter("replace_13", neutral)
|
|
||||||
# TileMapLayerAbove: tint ffffff77 for slight transparency
|
# TileMapLayerAbove: tint ffffff77 for slight transparency
|
||||||
if layer == dungeon_tilemap_layer_above:
|
if layer.name == "TileMapLayerAbove":
|
||||||
sm.set_shader_parameter("tint", Color(1.0, 1.0, 1.0, 0x77 / 255.0))
|
sm.set_shader_parameter("tint", Color(1.0, 1.0, 1.0, 0x77 / 255.0))
|
||||||
else:
|
else:
|
||||||
sm.set_shader_parameter("tint", Color(1.0, 1.0, 1.0, 1.0))
|
sm.set_shader_parameter("tint", Color(1.0, 1.0, 1.0, 1.0))
|
||||||
@@ -3339,6 +3692,8 @@ func _render_dungeon():
|
|||||||
if env_node:
|
if env_node:
|
||||||
dungeon_tilemap_layer = env_node.get_node_or_null("DungeonLayer0")
|
dungeon_tilemap_layer = env_node.get_node_or_null("DungeonLayer0")
|
||||||
dungeon_tilemap_layer_above = env_node.get_node_or_null("TileMapLayerAbove")
|
dungeon_tilemap_layer_above = env_node.get_node_or_null("TileMapLayerAbove")
|
||||||
|
dungeon_tilemap_layer_decorated = env_node.get_node_or_null("TileMapLayerDecoratedGround")
|
||||||
|
dungeon_tilemap_layer_cracked = env_node.get_node_or_null("TileMapLayerCrackedGround")
|
||||||
|
|
||||||
if not dungeon_tilemap_layer:
|
if not dungeon_tilemap_layer:
|
||||||
# Create new TileMapLayer
|
# Create new TileMapLayer
|
||||||
@@ -3506,6 +3861,45 @@ func _render_dungeon():
|
|||||||
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, BLACK_TILE)
|
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, BLACK_TILE)
|
||||||
above_tiles_placed += 1
|
above_tiles_placed += 1
|
||||||
|
|
||||||
|
# Render decorated ground (on TileMapLayerDecoratedGround) and cracked ground (on TileMapLayerCrackedGround)
|
||||||
|
if dungeon_tilemap_layer_decorated or dungeon_tilemap_layer_cracked:
|
||||||
|
for x in range(map_size.x):
|
||||||
|
for y in range(map_size.y):
|
||||||
|
if dungeon_tilemap_layer_decorated:
|
||||||
|
dungeon_tilemap_layer_decorated.erase_cell(Vector2i(x, y))
|
||||||
|
if dungeon_tilemap_layer_cracked:
|
||||||
|
dungeon_tilemap_layer_cracked.erase_cell(Vector2i(x, y))
|
||||||
|
if dungeon_data.has("decorated_tile_grid") and dungeon_tilemap_layer_decorated:
|
||||||
|
var decorated_tile_grid = dungeon_data.decorated_tile_grid
|
||||||
|
for x in range(map_size.x):
|
||||||
|
for y in range(map_size.y):
|
||||||
|
if grid[x][y] != 1 and grid[x][y] != 3:
|
||||||
|
continue
|
||||||
|
if x >= decorated_tile_grid.size() or y >= decorated_tile_grid[x].size():
|
||||||
|
continue
|
||||||
|
var dt = decorated_tile_grid[x][y]
|
||||||
|
if dt != null and dt is Vector2i:
|
||||||
|
dungeon_tilemap_layer_decorated.set_cell(Vector2i(x, y), 0, dt)
|
||||||
|
if dungeon_data.has("cracked_tile_grid") and dungeon_tilemap_layer_cracked:
|
||||||
|
var cracked_tile_grid = dungeon_data.cracked_tile_grid
|
||||||
|
# Floor switch positions must NEVER show cracked ground
|
||||||
|
var switch_tiles = {}
|
||||||
|
if dungeon_data.has("blocking_doors"):
|
||||||
|
var bd = dungeon_data.blocking_doors
|
||||||
|
var bd_array = bd if bd is Array else (bd.doors if "doors" in bd else [])
|
||||||
|
for door_data in bd_array:
|
||||||
|
if "switch_tile_x" in door_data and "switch_tile_y" in door_data:
|
||||||
|
var k = str(door_data.switch_tile_x) + "," + str(door_data.switch_tile_y)
|
||||||
|
switch_tiles[k] = true
|
||||||
|
for x in range(map_size.x):
|
||||||
|
for y in range(map_size.y):
|
||||||
|
if x >= cracked_tile_grid.size() or y >= cracked_tile_grid[x].size():
|
||||||
|
continue
|
||||||
|
if switch_tiles.has(str(x) + "," + str(y)):
|
||||||
|
continue
|
||||||
|
if cracked_tile_grid[x][y]:
|
||||||
|
dungeon_tilemap_layer_cracked.set_cell(Vector2i(x, y), 0, CRACKED_TILE_ATLAS)
|
||||||
|
|
||||||
LogManager.log("GameWorld: Placed " + str(tiles_placed) + " tiles on main layer", LogManager.CATEGORY_DUNGEON)
|
LogManager.log("GameWorld: Placed " + str(tiles_placed) + " tiles on main layer", LogManager.CATEGORY_DUNGEON)
|
||||||
LogManager.log("GameWorld: Placed " + str(above_tiles_placed) + " tiles on above layer", LogManager.CATEGORY_DUNGEON)
|
LogManager.log("GameWorld: Placed " + str(above_tiles_placed) + " tiles on above layer", LogManager.CATEGORY_DUNGEON)
|
||||||
LogManager.log("GameWorld: Dungeon rendered on TileMapLayer", LogManager.CATEGORY_DUNGEON)
|
LogManager.log("GameWorld: Dungeon rendered on TileMapLayer", LogManager.CATEGORY_DUNGEON)
|
||||||
@@ -3543,25 +3937,48 @@ func _update_spawn_points(target_room: Dictionary = {}, clear_existing: bool = t
|
|||||||
|
|
||||||
# Find free floor tiles in the room (excluding already assigned positions)
|
# Find free floor tiles in the room (excluding already assigned positions)
|
||||||
var free_tiles = _find_free_floor_tiles_in_room(room, exclude_positions)
|
var free_tiles = _find_free_floor_tiles_in_room(room, exclude_positions)
|
||||||
|
var room_center_x = (room.x + room.w / 2.0) * tile_size
|
||||||
|
var room_center_y = (room.y + room.h / 2.0) * tile_size
|
||||||
|
|
||||||
|
# Helper: only add spawn point if NOT on a fallout tile (players must never start on fallout)
|
||||||
|
var try_add_spawn = func(world_pos: Vector2) -> void:
|
||||||
|
if not _is_position_on_fallout_tile(world_pos):
|
||||||
|
player_manager.spawn_points.append(world_pos)
|
||||||
|
|
||||||
# Update player manager spawn points
|
# Update player manager spawn points
|
||||||
if free_tiles.size() > 0:
|
if free_tiles.size() > 0:
|
||||||
# Use free floor tiles as spawn points
|
# Use free floor tiles as spawn points (exclude fallout tiles)
|
||||||
for tile_pos in free_tiles:
|
for tile_pos in free_tiles:
|
||||||
var world_x = tile_pos.x * tile_size + tile_size / 2.0 # Center of tile
|
var world_x = tile_pos.x * tile_size + tile_size / 2.0 # Center of tile
|
||||||
var world_y = tile_pos.y * tile_size + tile_size / 2.0 # Center of tile
|
var world_y = tile_pos.y * tile_size + tile_size / 2.0 # Center of tile
|
||||||
player_manager.spawn_points.append(Vector2(world_x, world_y))
|
try_add_spawn.call(Vector2(world_x, world_y))
|
||||||
LogManager.log("GameWorld: Updated spawn points with " + str(free_tiles.size()) + " free floor tiles in room", LogManager.CATEGORY_DUNGEON)
|
LogManager.log("GameWorld: Updated spawn points with " + str(player_manager.spawn_points.size()) + " safe (non-fallout) floor tiles in room", LogManager.CATEGORY_DUNGEON)
|
||||||
else:
|
else:
|
||||||
# Fallback: Create spawn points in a circle around the room center
|
# Fallback: Create spawn points in a circle around the room center (exclude fallout)
|
||||||
var room_center_x = (room.x + room.w / 2.0) * tile_size
|
|
||||||
var room_center_y = (room.y + room.h / 2.0) * tile_size
|
|
||||||
var num_spawn_points = 8
|
var num_spawn_points = 8
|
||||||
for i in range(num_spawn_points):
|
for i in range(num_spawn_points):
|
||||||
var angle = i * PI * 2 / num_spawn_points
|
var angle = i * PI * 2 / num_spawn_points
|
||||||
var offset = Vector2(cos(angle), sin(angle)) * 30 # 30 pixel radius
|
var offset = Vector2(cos(angle), sin(angle)) * 30 # 30 pixel radius
|
||||||
player_manager.spawn_points.append(Vector2(room_center_x, room_center_y) + offset)
|
try_add_spawn.call(Vector2(room_center_x, room_center_y) + offset)
|
||||||
LogManager.log("GameWorld: Updated spawn points in circle around room center (no free tiles found)", LogManager.CATEGORY_DUNGEON)
|
LogManager.log("GameWorld: Updated spawn points in circle (safe count: " + str(player_manager.spawn_points.size()) + ")", LogManager.CATEGORY_DUNGEON)
|
||||||
|
|
||||||
|
# If no safe spawn points (e.g. room is all fallout), add room center only if safe; else first non-fallout tile in room
|
||||||
|
if player_manager.spawn_points.size() == 0:
|
||||||
|
var room_center = Vector2(room_center_x, room_center_y)
|
||||||
|
if not _is_position_on_fallout_tile(room_center):
|
||||||
|
player_manager.spawn_points.append(room_center)
|
||||||
|
LogManager.log("GameWorld: Fallback spawn at room center (safe)", LogManager.CATEGORY_DUNGEON)
|
||||||
|
else:
|
||||||
|
# Search room for any non-fallout floor tile
|
||||||
|
for tx in range(room.x + 2, room.x + room.w - 2):
|
||||||
|
for ty in range(room.y + 2, room.y + room.h - 2):
|
||||||
|
var wp = Vector2(tx * tile_size + tile_size / 2.0, ty * tile_size + tile_size / 2.0)
|
||||||
|
if not _is_position_on_fallout_tile(wp):
|
||||||
|
player_manager.spawn_points.append(wp)
|
||||||
|
LogManager.log("GameWorld: Fallback spawn at first non-fallout tile in room", LogManager.CATEGORY_DUNGEON)
|
||||||
|
break
|
||||||
|
if player_manager.spawn_points.size() > 0:
|
||||||
|
break
|
||||||
|
|
||||||
func _find_room_at_position(world_pos: Vector2) -> Dictionary:
|
func _find_room_at_position(world_pos: Vector2) -> Dictionary:
|
||||||
# Find which room contains the given world position
|
# Find which room contains the given world position
|
||||||
@@ -3959,10 +4376,12 @@ func _collect_current_world_metadata() -> Dictionary:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func _send_dungeon_blob_sync(client_peer_id: int):
|
func _send_dungeon_blob_sync(client_peer_id: int):
|
||||||
# Send pre-packed dungeon blob chunks with acknowledgment-based flow control
|
# Send dungeon blob chunks with acknowledgment-based flow control.
|
||||||
|
# Re-pack from current dungeon_data so joiners get up-to-date floor (including broken cracked tiles).
|
||||||
if not is_inside_tree() or not multiplayer.is_server():
|
if not is_inside_tree() or not multiplayer.is_server():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
_pack_dungeon_blob()
|
||||||
if dungeon_blob_chunks.is_empty():
|
if dungeon_blob_chunks.is_empty():
|
||||||
print("GameWorld: HOST - ERROR: No dungeon blob chunks available!")
|
print("GameWorld: HOST - ERROR: No dungeon blob chunks available!")
|
||||||
return
|
return
|
||||||
@@ -4235,6 +4654,8 @@ func _pack_dungeon_blob():
|
|||||||
var full_dungeon_data = {
|
var full_dungeon_data = {
|
||||||
"tile_grid": dungeon_data.get("tile_grid", []),
|
"tile_grid": dungeon_data.get("tile_grid", []),
|
||||||
"grid": dungeon_data.get("grid", []),
|
"grid": dungeon_data.get("grid", []),
|
||||||
|
"decorated_tile_grid": dungeon_data.get("decorated_tile_grid", []),
|
||||||
|
"cracked_tile_grid": dungeon_data.get("cracked_tile_grid", []),
|
||||||
"map_size": dungeon_data.get("map_size", Vector2i(72, 72)),
|
"map_size": dungeon_data.get("map_size", Vector2i(72, 72)),
|
||||||
"rooms": dungeon_data.get("rooms", []),
|
"rooms": dungeon_data.get("rooms", []),
|
||||||
"start_room": dungeon_data.get("start_room", {}),
|
"start_room": dungeon_data.get("start_room", {}),
|
||||||
@@ -4399,17 +4820,17 @@ func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, h
|
|||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
await get_tree().process_frame # Wait an extra frame to ensure enemies are fully ready
|
await get_tree().process_frame # Wait an extra frame to ensure enemies are fully ready
|
||||||
|
|
||||||
# Update spawn points - use host's room if available, otherwise use start room
|
# Update spawn points - always use start room for joiners so they never end up in a corridor or outside the world
|
||||||
print("GameWorld: Client - Updating spawn points...")
|
print("GameWorld: Client - Updating spawn points...")
|
||||||
if not host_room.is_empty():
|
var spawn_room = dungeon_data.start_room if dungeon_data.has("start_room") and not dungeon_data.start_room.is_empty() else host_room
|
||||||
print("GameWorld: Client - Using host's room for spawn points: ", host_room)
|
if not spawn_room.is_empty():
|
||||||
LogManager.log("GameWorld: Using host's room for spawn points", LogManager.CATEGORY_DUNGEON)
|
print("GameWorld: Client - Using start room for joiner spawn points (avoid corridor/outside world)")
|
||||||
_update_spawn_points(host_room)
|
LogManager.log("GameWorld: Using start room for joiner spawn points", LogManager.CATEGORY_DUNGEON)
|
||||||
# Move any existing players to spawn near host
|
_update_spawn_points(spawn_room)
|
||||||
_move_players_to_host_room(host_room)
|
_move_players_to_host_room(spawn_room)
|
||||||
else:
|
else:
|
||||||
print("GameWorld: Client - Host room not available, using start room")
|
print("GameWorld: Client - No start room, using default spawn points")
|
||||||
LogManager.log("GameWorld: Host room not available, using start room", LogManager.CATEGORY_DUNGEON)
|
LogManager.log("GameWorld: No start room for joiner, using default spawn points", LogManager.CATEGORY_DUNGEON)
|
||||||
_update_spawn_points()
|
_update_spawn_points()
|
||||||
# Move all players to start room
|
# Move all players to start room
|
||||||
print("GameWorld: Client - Moving all players to start room...")
|
print("GameWorld: Client - Moving all players to start room...")
|
||||||
@@ -4466,8 +4887,7 @@ func _sync_dungeon_blob_metadata(seed_value: int, level: int, map_size_sync: Vec
|
|||||||
print("GameWorld: Client - defeated_enemies dictionary now has ", defeated_enemies.size(), " entries")
|
print("GameWorld: Client - defeated_enemies dictionary now has ", defeated_enemies.size(), " entries")
|
||||||
LogManager.log("GameWorld: Client received " + str(defeated_enemies_list.size()) + " defeated enemy indices", LogManager.CATEGORY_NETWORK)
|
LogManager.log("GameWorld: Client received " + str(defeated_enemies_list.size()) + " defeated enemy indices", LogManager.CATEGORY_NETWORK)
|
||||||
|
|
||||||
# Store broken objects BEFORE objects are spawned
|
# Store broken objects BEFORE objects are spawned (merge, don't replace - so _sync_broken_objects isn't overwritten if it arrived first)
|
||||||
broken_objects.clear()
|
|
||||||
for obj_index in broken_objects_list:
|
for obj_index in broken_objects_list:
|
||||||
broken_objects[obj_index] = true
|
broken_objects[obj_index] = true
|
||||||
if broken_objects_list.size() > 0:
|
if broken_objects_list.size() > 0:
|
||||||
@@ -4571,7 +4991,7 @@ func _sync_dungeon_blob_metadata(seed_value: int, level: int, map_size_sync: Vec
|
|||||||
"seed": seed_value,
|
"seed": seed_value,
|
||||||
"level": level,
|
"level": level,
|
||||||
"host_room": host_room,
|
"host_room": host_room,
|
||||||
"existing_loot": existing_loot_list # Store for spawning after dungeon is ready
|
"existing_loot": existing_loot_list
|
||||||
}
|
}
|
||||||
dungeon_sync_chunks.clear()
|
dungeon_sync_chunks.clear()
|
||||||
dungeon_sync_received_chunks = 0
|
dungeon_sync_received_chunks = 0
|
||||||
@@ -4889,15 +5309,16 @@ func _reassemble_dungeon_blob():
|
|||||||
_sync_loot_spawn(loot_data.position, loot_data.get("loot_type", 0), loot_data.get("velocity", Vector2.ZERO), loot_data.get("velocity_z", 0.0), loot_data.get("loot_id", -1))
|
_sync_loot_spawn(loot_data.position, loot_data.get("loot_type", 0), loot_data.get("velocity", Vector2.ZERO), loot_data.get("velocity_z", 0.0), loot_data.get("loot_id", -1))
|
||||||
print("GameWorld: Client - Existing loot spawned")
|
print("GameWorld: Client - Existing loot spawned")
|
||||||
|
|
||||||
# Update spawn points
|
# Update spawn points - always use start room for joiners so they never end up in a corridor or outside the world
|
||||||
print("GameWorld: Client - Updating spawn points...")
|
print("GameWorld: Client - Updating spawn points...")
|
||||||
var host_room = dungeon_sync_metadata.host_room
|
var host_room = dungeon_sync_metadata.host_room
|
||||||
if not host_room.is_empty():
|
var spawn_room = dungeon_data.start_room if dungeon_data.has("start_room") and not dungeon_data.start_room.is_empty() else host_room
|
||||||
print("GameWorld: Client - Using host's room for spawn points: ", host_room)
|
if not spawn_room.is_empty():
|
||||||
_update_spawn_points(host_room)
|
print("GameWorld: Client - Using start room for joiner spawn points")
|
||||||
_move_players_to_host_room(host_room)
|
_update_spawn_points(spawn_room)
|
||||||
|
_move_players_to_host_room(spawn_room)
|
||||||
else:
|
else:
|
||||||
print("GameWorld: Client - Host room not available, using start room")
|
print("GameWorld: Client - No start room, using default spawn points")
|
||||||
_update_spawn_points()
|
_update_spawn_points()
|
||||||
|
|
||||||
# Move all players to start room
|
# Move all players to start room
|
||||||
@@ -5026,15 +5447,16 @@ func _check_and_render_dungeon():
|
|||||||
_render_dungeon()
|
_render_dungeon()
|
||||||
print("GameWorld: Client - Dungeon rendered")
|
print("GameWorld: Client - Dungeon rendered")
|
||||||
|
|
||||||
# Update spawn points
|
# Update spawn points - always use start room for joiners so they never end up in a corridor or outside the world
|
||||||
print("GameWorld: Client - Updating spawn points...")
|
print("GameWorld: Client - Updating spawn points...")
|
||||||
var host_room = dungeon_sync_metadata.host_room
|
var host_room_chunks = dungeon_sync_metadata.host_room
|
||||||
if not host_room.is_empty():
|
var spawn_room_chunks = dungeon_data.start_room if dungeon_data.has("start_room") and not dungeon_data.start_room.is_empty() else host_room_chunks
|
||||||
print("GameWorld: Client - Using host's room for spawn points: ", host_room)
|
if not spawn_room_chunks.is_empty():
|
||||||
_update_spawn_points(host_room)
|
print("GameWorld: Client - Using start room for joiner spawn points")
|
||||||
_move_players_to_host_room(host_room)
|
_update_spawn_points(spawn_room_chunks)
|
||||||
|
_move_players_to_host_room(spawn_room_chunks)
|
||||||
else:
|
else:
|
||||||
print("GameWorld: Client - Host room not available, using start room")
|
print("GameWorld: Client - No start room, using default spawn points")
|
||||||
_update_spawn_points()
|
_update_spawn_points()
|
||||||
|
|
||||||
# Move all players to start room
|
# Move all players to start room
|
||||||
@@ -5272,15 +5694,20 @@ func _spawn_enemies():
|
|||||||
if "damage" in enemy_data:
|
if "damage" in enemy_data:
|
||||||
enemy.damage = enemy_data.damage
|
enemy.damage = enemy_data.damage
|
||||||
|
|
||||||
# If it's a humanoid enemy, set the humanoid_type
|
# If it's a humanoid enemy, set the humanoid_type and spawn_position for unique appearance seed
|
||||||
if enemy_type.ends_with("enemy_humanoid.tscn") and "humanoid_type" in enemy_data:
|
if enemy_type.ends_with("enemy_humanoid.tscn"):
|
||||||
|
if "humanoid_type" in enemy_data:
|
||||||
enemy.humanoid_type = enemy_data.humanoid_type
|
enemy.humanoid_type = enemy_data.humanoid_type
|
||||||
|
if "spawn_position" in enemy:
|
||||||
|
enemy.spawn_position = enemy_data.position
|
||||||
|
|
||||||
# CRITICAL: Set collision mask BEFORE adding to scene to ensure enemies collide with walls (layer 7 = bit 6 = 64)
|
# CRITICAL: Set collision mask BEFORE adding to scene to ensure enemies collide with walls (layer 7 = bit 6 = 64)
|
||||||
# This overrides any collision_mask set in the scene file
|
# This overrides any collision_mask set in the scene file
|
||||||
enemy.collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
|
enemy.collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
|
||||||
|
|
||||||
# Add to scene tree AFTER setting authority and stats
|
# Set position BEFORE add_child so humanoid _ready() sees correct global_position for unique appearance seed
|
||||||
|
enemy.position = enemy_data.position
|
||||||
|
# Add to scene tree AFTER setting authority, stats, and position
|
||||||
entities_node.add_child(enemy)
|
entities_node.add_child(enemy)
|
||||||
enemy.global_position = enemy_data.position
|
enemy.global_position = enemy_data.position
|
||||||
|
|
||||||
@@ -9255,14 +9682,14 @@ func _spawn_floor_switch(i_position: Vector2, required_weight: float, tile_x: in
|
|||||||
entities_node.add_child(switch)
|
entities_node.add_child(switch)
|
||||||
switch.global_position = i_position
|
switch.global_position = i_position
|
||||||
|
|
||||||
# Update tilemap to show switch tile (initial inactive state)
|
# Draw switch on TileMapLayerDecoratedGround (terrain 1 = walk, 2 = pillar set in tileset)
|
||||||
if dungeon_tilemap_layer:
|
if dungeon_tilemap_layer_decorated:
|
||||||
var initial_tile: Vector2i
|
var initial_tile: Vector2i
|
||||||
if switch_type == "pillar":
|
if switch_type == "pillar":
|
||||||
initial_tile = Vector2i(16, 9) # Pillar switch inactive
|
initial_tile = Vector2i(16, 9) # Pillar switch inactive (terrain 2)
|
||||||
else:
|
else:
|
||||||
initial_tile = Vector2i(11, 9) # Walk-on switch inactive
|
initial_tile = Vector2i(11, 9) # Walk-on switch inactive (terrain 1)
|
||||||
dungeon_tilemap_layer.set_cell(Vector2i(tile_x, tile_y), 0, initial_tile)
|
dungeon_tilemap_layer_decorated.set_cell(Vector2i(tile_x, tile_y), 0, initial_tile)
|
||||||
|
|
||||||
var room_x_str = str(switch_room.get("x", "?")) if switch_room and not switch_room.is_empty() else "?"
|
var room_x_str = str(switch_room.get("x", "?")) if switch_room and not switch_room.is_empty() else "?"
|
||||||
var room_y_str = str(switch_room.get("y", "?")) if switch_room and not switch_room.is_empty() else "?"
|
var room_y_str = str(switch_room.get("y", "?")) if switch_room and not switch_room.is_empty() else "?"
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ var is_chest_opened: bool = false
|
|||||||
var sync_timer: float = 0.0
|
var sync_timer: float = 0.0
|
||||||
var sync_interval: float = 0.05 # Sync 20 times per second
|
var sync_interval: float = 0.05 # Sync 20 times per second
|
||||||
|
|
||||||
|
# Fallout: sink and disappear (like loot)
|
||||||
|
var falling_into_fallout: bool = false
|
||||||
|
var fallout_sink_progress: float = 1.0
|
||||||
|
const FALLOUT_SINK_DURATION: float = 0.4
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
# Make sure it's on the interactable layer
|
# Make sure it's on the interactable layer
|
||||||
collision_layer = 2 # Layer 2 for objects
|
collision_layer = 2 # Layer 2 for objects
|
||||||
@@ -76,6 +81,22 @@ func _physics_process(delta):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not is_frozen:
|
if not is_frozen:
|
||||||
|
# Fallout: sink and disappear when on ground (not held, not airborne)
|
||||||
|
if not is_airborne and position_z <= 0.0:
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw and gw.has_method("_is_position_on_fallout_tile") and gw._is_position_on_fallout_tile(global_position):
|
||||||
|
if not falling_into_fallout:
|
||||||
|
falling_into_fallout = true
|
||||||
|
fallout_sink_progress = 1.0
|
||||||
|
fallout_sink_progress -= delta / FALLOUT_SINK_DURATION
|
||||||
|
if fallout_sink_progress <= 0.0:
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
|
if sprite:
|
||||||
|
sprite.scale = Vector2.ONE * max(0.0, fallout_sink_progress)
|
||||||
|
move_and_slide()
|
||||||
|
return
|
||||||
|
falling_into_fallout = false
|
||||||
# Z-axis physics for airborne boxes
|
# Z-axis physics for airborne boxes
|
||||||
if is_airborne:
|
if is_airborne:
|
||||||
# Apply gravity to Z velocity
|
# Apply gravity to Z velocity
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ static func spawn_item_loot(item: Item, position: Vector2, entities_node: Node,
|
|||||||
|
|
||||||
# Set properties before adding to scene tree (to avoid physics state change errors)
|
# Set properties before adding to scene tree (to avoid physics state change errors)
|
||||||
loot.global_position = safe_spawn_pos
|
loot.global_position = safe_spawn_pos
|
||||||
|
loot.position_z = 1.0 if "position_z" in loot else 0.0
|
||||||
loot.loot_type = loot.LootType.ITEM
|
loot.loot_type = loot.LootType.ITEM
|
||||||
loot.item = item # Set the item instance
|
loot.item = item # Set the item instance
|
||||||
# Set initial velocity before _ready() processes
|
# Set initial velocity before _ready() processes
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ enum LootType {
|
|||||||
|
|
||||||
@export var loot_type: LootType = LootType.COIN
|
@export var loot_type: LootType = LootType.COIN
|
||||||
|
|
||||||
# Z-axis physics (like boxes and players)
|
# Z-axis physics (like boxes and players) - start at 1 to avoid instantly falling into fallout
|
||||||
var position_z: float = 0.0
|
const SPAWN_POSITION_Z: float = 1.0
|
||||||
|
var position_z: float = 1.0
|
||||||
var velocity_z: float = 0.0
|
var velocity_z: float = 0.0
|
||||||
var acceleration_z: float = 0.0
|
var acceleration_z: float = 0.0
|
||||||
var is_airborne: bool = true
|
var is_airborne: bool = true
|
||||||
@@ -57,8 +58,15 @@ var item: Item = null # Item instance (for LootType.ITEM)
|
|||||||
# Quantity badge for items with quantity > 1
|
# Quantity badge for items with quantity > 1
|
||||||
var quantity_badge: Label = null
|
var quantity_badge: Label = null
|
||||||
|
|
||||||
|
# Fallout: sink and disappear (no recovery)
|
||||||
|
var falling_into_fallout: bool = false
|
||||||
|
var fallout_sink_progress: float = 1.0
|
||||||
|
const FALLOUT_SINK_DURATION: float = 0.4
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
add_to_group("loot")
|
add_to_group("loot")
|
||||||
|
# Always start slightly above ground to prevent instantly falling into fallout
|
||||||
|
position_z = SPAWN_POSITION_Z
|
||||||
|
|
||||||
# Setup shadow
|
# Setup shadow
|
||||||
if shadow:
|
if shadow:
|
||||||
@@ -219,6 +227,36 @@ func _physics_process(delta):
|
|||||||
|
|
||||||
# Server (authority): Run physics normally
|
# Server (authority): Run physics normally
|
||||||
if is_server:
|
if is_server:
|
||||||
|
# Fallout: lock to tile center, stop x/y movement, not collectible, sink then remove
|
||||||
|
if not is_airborne and position_z <= 0.0:
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw and gw.has_method("_is_position_on_fallout_tile") and gw._is_position_on_fallout_tile(global_position):
|
||||||
|
if not falling_into_fallout:
|
||||||
|
falling_into_fallout = true
|
||||||
|
fallout_sink_progress = 1.0
|
||||||
|
# Lock to center of fallout tile and stop all x/y movement
|
||||||
|
if gw.has_method("_get_tile_center_at"):
|
||||||
|
global_position = gw._get_tile_center_at(global_position)
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
# Not collectible while falling
|
||||||
|
if pickup_area:
|
||||||
|
pickup_area.monitoring = false
|
||||||
|
pickup_area.monitorable = false
|
||||||
|
fallout_sink_progress -= delta / FALLOUT_SINK_DURATION
|
||||||
|
if fallout_sink_progress <= 0.0:
|
||||||
|
# Sync removal to clients so joiner sees loot disappear (same as pickup)
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
||||||
|
var loot_id = get_meta("loot_id") if has_meta("loot_id") else -1
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world and game_world.has_method("_sync_loot_remove"):
|
||||||
|
game_world._rpc_to_ready_peers("_sync_loot_remove", [loot_id, global_position])
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
|
scale = Vector2.ONE * max(0.0, fallout_sink_progress)
|
||||||
|
move_and_slide()
|
||||||
|
_update_visuals()
|
||||||
|
return
|
||||||
|
|
||||||
# Update bounce timer
|
# Update bounce timer
|
||||||
if bounce_timer > 0.0:
|
if bounce_timer > 0.0:
|
||||||
bounce_timer -= delta
|
bounce_timer -= delta
|
||||||
@@ -410,6 +448,8 @@ func _animate_coin(delta):
|
|||||||
sprite.frame = frame
|
sprite.frame = frame
|
||||||
|
|
||||||
func _on_pickup_area_body_entered(body):
|
func _on_pickup_area_body_entered(body):
|
||||||
|
if falling_into_fallout:
|
||||||
|
return
|
||||||
if body and body.is_in_group("player") and not body.is_dead:
|
if body and body.is_in_group("player") and not body.is_dead:
|
||||||
# Check if this item was dropped by this player recently (5 second cooldown)
|
# Check if this item was dropped by this player recently (5 second cooldown)
|
||||||
if has_meta("dropped_by_peer_id") and has_meta("drop_time"):
|
if has_meta("dropped_by_peer_id") and has_meta("drop_time"):
|
||||||
@@ -430,6 +470,8 @@ func _on_pickup_area_body_entered(body):
|
|||||||
_pickup(body)
|
_pickup(body)
|
||||||
|
|
||||||
func _pickup(player: Node):
|
func _pickup(player: Node):
|
||||||
|
if falling_into_fallout:
|
||||||
|
return
|
||||||
# Prevent multiple pickups
|
# Prevent multiple pickups
|
||||||
if collected:
|
if collected:
|
||||||
print("Loot: Already collected, ignoring pickup")
|
print("Loot: Already collected, ignoring pickup")
|
||||||
@@ -774,6 +816,9 @@ func _request_pickup(player_peer_id: int):
|
|||||||
print("Loot: Already collected (collected=", collected, "), ignoring pickup request")
|
print("Loot: Already collected (collected=", collected, "), ignoring pickup request")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if falling_into_fallout:
|
||||||
|
return
|
||||||
|
|
||||||
# Set mutex and mark as collected IMMEDIATELY to prevent any race conditions
|
# Set mutex and mark as collected IMMEDIATELY to prevent any race conditions
|
||||||
processing_pickup = true
|
processing_pickup = true
|
||||||
collected = true
|
collected = true
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ var controls_disabled: bool = false # True when player has reached exit and cont
|
|||||||
|
|
||||||
# Being held state
|
# Being held state
|
||||||
var being_held_by: Node = null
|
var being_held_by: Node = null
|
||||||
|
var is_being_held: bool = false # Set by set_being_held(); reliable on all clients for fallout immunity
|
||||||
var grabbed_by_enemy_hand: Node = null # Set when enemy hand grabs (snatch); locks movement until release
|
var grabbed_by_enemy_hand: Node = null # Set when enemy hand grabs (snatch); locks movement until release
|
||||||
var enemy_hand_grab_knockback_time: float = 0.0 # Timer for initial knockback when grabbed
|
var enemy_hand_grab_knockback_time: float = 0.0 # Timer for initial knockback when grabbed
|
||||||
const ENEMY_HAND_GRAB_KNOCKBACK_DURATION: float = 0.15 # Short knockback duration before moving to hand
|
const ENEMY_HAND_GRAB_KNOCKBACK_DURATION: float = 0.15 # Short knockback duration before moving to hand
|
||||||
@@ -143,6 +144,27 @@ var spawn_landing_bounced: bool = false
|
|||||||
var spawn_landing_visible_shown: bool = false # One-shot: set true when we show right before falling
|
var spawn_landing_visible_shown: bool = false # One-shot: set true when we show right before falling
|
||||||
var has_seen_exit_this_level: bool = false # Track if player has seen exit notification for current level
|
var has_seen_exit_this_level: bool = false # Track if player has seen exit notification for current level
|
||||||
|
|
||||||
|
# Fallout (quicksand) state: sink into tile, then respawn at last safe
|
||||||
|
var fallout_state: bool = false
|
||||||
|
var fallout_scale_progress: float = 1.0 # 1.0 -> 0.0 during sink
|
||||||
|
var fallout_respawn_delay_timer: float = 0.0 # After scale hits 0, wait this long before respawn
|
||||||
|
var fallout_respawn_stun_timer: float = 0.0 # After respawn from fallout, stun for this long (no control)
|
||||||
|
var on_fallout_tile_near_sink: bool = false # True when on fallout tile but not yet at center (fast walk plays)
|
||||||
|
var animation_speed_multiplier: float = 1.0 # 1.0 = normal; >1 when on fallout tile so run anim plays faster
|
||||||
|
const FALLOUT_CENTER_THRESHOLD: float = 2.0 # Player center must be almost exactly at tile center to sink (Zelda Link's Awakening style)
|
||||||
|
const FALLOUT_DRAG_STRENGTH: float = 820.0 # Base pull toward fallout center (strong enough to prevent running over)
|
||||||
|
const FALLOUT_CENTER_PULL_BOOST: float = 1.8 # Pull is stronger near center: at center (1+BOOST)x, at edge 1x
|
||||||
|
const FALLOUT_DRAG_EDGE_FACTOR: float = 0.45 # At tile edge drag is 45% strength; ramps to 100% toward center
|
||||||
|
const FALLOUT_MOVEMENT_FACTOR: float = 0.3 # Movement speed on fallout tile (30%) so player cannot run over it
|
||||||
|
const FALLOUT_TILE_HALF_SIZE: float = 8.0 # Half of tile size (16) for distance-based strength
|
||||||
|
const FALLOUT_PLAYER_BOX_HALF: float = 8.0 # Player treated as 16x16 box for quicksand (center ± 8)
|
||||||
|
const FALLOUT_TILE_ANIMATION_SPEED: float = 3.0 # Run animation plays this many times faster when on fallout tile (warning to player)
|
||||||
|
const FALLOUT_SINK_DURATION: float = 0.5 # Seconds to scale from 1 to 0 (faster sink)
|
||||||
|
const FALLOUT_RESPAWN_DELAY: float = 0.3 # Seconds after scale reaches 0 before respawning at safe tile
|
||||||
|
const FALLOUT_RESPAWN_STUN_DURATION: float = 0.3 # Seconds of stun after respawn from fallout
|
||||||
|
const FALLOUT_RESPAWN_HP_PENALTY: float = 1.0 # HP lost when respawning from fallout
|
||||||
|
const HELD_POSITION_Z: float = 12.0 # Z height when held/lifted (above ground; immune to fallout)
|
||||||
|
|
||||||
# Components
|
# Components
|
||||||
# @onready var sprite = $Sprite2D # REMOVED: Now using layered sprites
|
# @onready var sprite = $Sprite2D # REMOVED: Now using layered sprites
|
||||||
@onready var shadow = $Shadow
|
@onready var shadow = $Shadow
|
||||||
@@ -150,6 +172,7 @@ var has_seen_exit_this_level: bool = false # Track if player has seen exit notif
|
|||||||
@onready var point_light = $PointLight2D
|
@onready var point_light = $PointLight2D
|
||||||
@onready var collision_shape = $CollisionShape2D
|
@onready var collision_shape = $CollisionShape2D
|
||||||
@onready var grab_area = $GrabArea
|
@onready var grab_area = $GrabArea
|
||||||
|
@onready var quicksand_area = $QuicksandArea
|
||||||
@onready var interaction_indicator = $InteractionIndicator
|
@onready var interaction_indicator = $InteractionIndicator
|
||||||
|
|
||||||
# Audio
|
# Audio
|
||||||
@@ -1584,9 +1607,10 @@ func _get_collision_extent(node: Node) -> float:
|
|||||||
return 8.0
|
return 8.0
|
||||||
|
|
||||||
func _update_animation(delta):
|
func _update_animation(delta):
|
||||||
# Update animation frame timing
|
# Update animation frame timing (faster when on fallout tile to warn player)
|
||||||
|
var frame_duration_sec = ANIMATIONS[current_animation]["frameDurations"][current_frame] / 1000.0 / animation_speed_multiplier
|
||||||
time_since_last_frame += delta
|
time_since_last_frame += delta
|
||||||
if time_since_last_frame >= ANIMATIONS[current_animation]["frameDurations"][current_frame] / 1000.0:
|
if time_since_last_frame >= frame_duration_sec:
|
||||||
current_frame += 1
|
current_frame += 1
|
||||||
if current_frame >= len(ANIMATIONS[current_animation]["frames"]):
|
if current_frame >= len(ANIMATIONS[current_animation]["frames"]):
|
||||||
current_frame -= 1 # Prevent out of bounds
|
current_frame -= 1 # Prevent out of bounds
|
||||||
@@ -1972,7 +1996,26 @@ func _physics_process(delta):
|
|||||||
was_reviving_last_frame = false
|
was_reviving_last_frame = false
|
||||||
_stop_spell_charge_incantation()
|
_stop_spell_charge_incantation()
|
||||||
|
|
||||||
if is_local_player and is_multiplayer_authority():
|
# Fallout (quicksand) sink: run for ALL players so remote see scale/rotation/FALL animation
|
||||||
|
if fallout_state:
|
||||||
|
current_direction = Direction.DOWN
|
||||||
|
facing_direction_vector = Vector2.DOWN
|
||||||
|
_set_animation("FALL")
|
||||||
|
scale = Vector2.ONE * max(0.0, fallout_scale_progress)
|
||||||
|
rotation = deg_to_rad(45.0)
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
if fallout_respawn_delay_timer > 0.0:
|
||||||
|
fallout_respawn_delay_timer -= delta
|
||||||
|
if fallout_respawn_delay_timer <= 0.0:
|
||||||
|
fallout_respawn_delay_timer = 0.0
|
||||||
|
if is_multiplayer_authority():
|
||||||
|
_respawn_from_fallout()
|
||||||
|
else:
|
||||||
|
fallout_scale_progress -= delta / FALLOUT_SINK_DURATION
|
||||||
|
if fallout_scale_progress <= 0.0:
|
||||||
|
fallout_scale_progress = 0.0
|
||||||
|
fallout_respawn_delay_timer = FALLOUT_RESPAWN_DELAY
|
||||||
|
elif is_local_player and is_multiplayer_authority():
|
||||||
# When dead: only corpse knockback friction + sync; no input or other logic
|
# When dead: only corpse knockback friction + sync; no input or other logic
|
||||||
if is_dead:
|
if is_dead:
|
||||||
if is_knocked_back:
|
if is_knocked_back:
|
||||||
@@ -1983,6 +2026,42 @@ func _physics_process(delta):
|
|||||||
else:
|
else:
|
||||||
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
|
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
|
||||||
else:
|
else:
|
||||||
|
# Reset fallout-tile animation state each frame (set when on fallout tile below)
|
||||||
|
animation_speed_multiplier = 1.0
|
||||||
|
on_fallout_tile_near_sink = false
|
||||||
|
# Held players cannot fallout (use is_being_held so it works on all clients; position can put held player over a tile)
|
||||||
|
var is_held = is_being_held or (being_held_by != null and is_instance_valid(being_held_by))
|
||||||
|
if position_z == 0.0 and not is_held:
|
||||||
|
# Quicksand: only when player CENTER is on a fallout tile (avoids vortex pull from adjacent ground)
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
var area_center = quicksand_area.global_position if quicksand_area else global_position
|
||||||
|
if gw and gw.has_method("_is_position_on_fallout_tile"):
|
||||||
|
if not gw._is_position_on_fallout_tile(area_center):
|
||||||
|
gw.update_last_safe_position_for_player(self, global_position)
|
||||||
|
else:
|
||||||
|
# Center is on fallout: use this tile's center for drag/sink (symmetric)
|
||||||
|
var tile_center = gw._get_tile_center_at(area_center)
|
||||||
|
var dist_to_center = area_center.distance_to(tile_center)
|
||||||
|
if dist_to_center < FALLOUT_CENTER_THRESHOLD:
|
||||||
|
# If carrying something, throw it in the direction we were looking before falling
|
||||||
|
if held_object and is_lifting:
|
||||||
|
_force_throw_held_object(facing_direction_vector)
|
||||||
|
# Snap player center exactly to fallout tile center so sink looks correct
|
||||||
|
global_position = tile_center
|
||||||
|
fallout_state = true
|
||||||
|
fallout_scale_progress = 1.0
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
current_direction = Direction.DOWN
|
||||||
|
facing_direction_vector = Vector2.DOWN
|
||||||
|
_set_animation("FALL")
|
||||||
|
if has_node("SfxFallout"):
|
||||||
|
$SfxFallout.play()
|
||||||
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
|
_rpc_to_ready_peers("_sync_fallout_start", [tile_center])
|
||||||
|
else:
|
||||||
|
on_fallout_tile_near_sink = true
|
||||||
|
animation_speed_multiplier = FALLOUT_TILE_ANIMATION_SPEED
|
||||||
|
_set_animation("RUN")
|
||||||
# Handle knockback timer (always handle knockback, even when controls are disabled)
|
# Handle knockback timer (always handle knockback, even when controls are disabled)
|
||||||
if is_knocked_back:
|
if is_knocked_back:
|
||||||
knockback_time += delta
|
knockback_time += delta
|
||||||
@@ -1994,6 +2073,12 @@ func _physics_process(delta):
|
|||||||
if grabbed_by_enemy_hand:
|
if grabbed_by_enemy_hand:
|
||||||
enemy_hand_grab_knockback_time += delta
|
enemy_hand_grab_knockback_time += delta
|
||||||
|
|
||||||
|
# Update fallout respawn stun timer (no control for 0.3s after respawn from fallout)
|
||||||
|
if fallout_respawn_stun_timer > 0.0:
|
||||||
|
fallout_respawn_stun_timer -= delta
|
||||||
|
if fallout_respawn_stun_timer <= 0.0:
|
||||||
|
fallout_respawn_stun_timer = 0.0
|
||||||
|
|
||||||
# Update movement lock timer (for bow release)
|
# Update movement lock timer (for bow release)
|
||||||
if movement_lock_timer > 0.0:
|
if movement_lock_timer > 0.0:
|
||||||
movement_lock_timer -= delta
|
movement_lock_timer -= delta
|
||||||
@@ -2084,18 +2169,18 @@ func _physics_process(delta):
|
|||||||
burn_damage_timer = 0.0
|
burn_damage_timer = 0.0
|
||||||
_remove_burn_debuff()
|
_remove_burn_debuff()
|
||||||
|
|
||||||
# Skip input if controls are disabled (e.g., when inventory is open) or spawn landing (fall → DIE → stand up)
|
# Skip input if controls are disabled (e.g., when inventory is open) or spawn landing (fall → DIE → stand up) or fallout (sinking) or post-fallout stun
|
||||||
# But still allow knockback to continue (handled above)
|
# But still allow knockback to continue (handled above)
|
||||||
# CRITICAL: During entrance walk-out cut-scene, game_world sets velocity; do NOT zero it here
|
# CRITICAL: During entrance walk-out cut-scene, game_world sets velocity; do NOT zero it here
|
||||||
var entrance_walk_out = controls_disabled and has_meta("entrance_walk_target")
|
var entrance_walk_out = controls_disabled and has_meta("entrance_walk_target")
|
||||||
var skip_input = controls_disabled or spawn_landing
|
var skip_input = controls_disabled or spawn_landing or fallout_state or (fallout_respawn_stun_timer > 0.0)
|
||||||
if controls_disabled or spawn_landing:
|
if controls_disabled or spawn_landing or fallout_state or (fallout_respawn_stun_timer > 0.0):
|
||||||
if not is_knocked_back and not entrance_walk_out:
|
if not is_knocked_back and not entrance_walk_out:
|
||||||
# Immediately stop movement when controls are disabled (e.g., inventory opened)
|
# Immediately stop movement when controls are disabled (e.g., inventory opened)
|
||||||
# Exception: entrance walk-out - velocity is driven by game_world for cut-scene
|
# Exception: entrance walk-out - velocity is driven by game_world for cut-scene
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
# Reset animation to IDLE if not in a special state (skip when spawn_landing: we use DIE until stand up)
|
# Reset animation to IDLE if not in a special state (skip when spawn_landing: we use DIE until stand up)
|
||||||
if not spawn_landing and current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF" and current_animation != "AXE" and current_animation != "PUNCH":
|
if not spawn_landing and not fallout_state and current_animation != "THROW" and current_animation != "DAMAGE" and current_animation != "SWORD" and current_animation != "BOW" and current_animation != "STAFF" and current_animation != "AXE" and current_animation != "PUNCH":
|
||||||
if is_lifting:
|
if is_lifting:
|
||||||
_set_animation("IDLE_HOLD")
|
_set_animation("IDLE_HOLD")
|
||||||
elif is_pushing:
|
elif is_pushing:
|
||||||
@@ -2152,6 +2237,24 @@ func _physics_process(delta):
|
|||||||
struggle_time = 0.0 # Reset struggle timer
|
struggle_time = 0.0 # Reset struggle timer
|
||||||
struggle_direction = Vector2.ZERO
|
struggle_direction = Vector2.ZERO
|
||||||
_handle_input()
|
_handle_input()
|
||||||
|
# Apply quicksand only when player CENTER is on a fallout tile (no vortex pull from adjacent tiles)
|
||||||
|
if position_z == 0.0 and not (is_being_held or (being_held_by != null and is_instance_valid(being_held_by))):
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
var area_center = quicksand_area.global_position if quicksand_area else global_position
|
||||||
|
if gw and gw.has_method("_is_position_on_fallout_tile") and gw._is_position_on_fallout_tile(area_center):
|
||||||
|
# Heavy movement penalty so running over the tile is not possible
|
||||||
|
velocity *= FALLOUT_MOVEMENT_FACTOR
|
||||||
|
var tile_center = gw._get_tile_center_at(area_center)
|
||||||
|
var area_center_dist = area_center.distance_to(tile_center)
|
||||||
|
if area_center_dist >= FALLOUT_CENTER_THRESHOLD:
|
||||||
|
# Drag toward this tile's center (same strength from all directions)
|
||||||
|
var dir = (tile_center - area_center).normalized()
|
||||||
|
# Softer at edge: drag ramps from FALLOUT_DRAG_EDGE_FACTOR at tile edge to 1.0 toward center
|
||||||
|
var edge_t = clamp(area_center_dist / FALLOUT_TILE_HALF_SIZE, 0.0, 1.0)
|
||||||
|
var edge_drag_factor = lerp(1.0, FALLOUT_DRAG_EDGE_FACTOR, edge_t)
|
||||||
|
# Strength: stronger when player center is closer to fallout tile center (distance-based only, no direction bias)
|
||||||
|
var strength_mult = 1.0 + FALLOUT_CENTER_PULL_BOOST * (1.0 - clamp(area_center_dist / FALLOUT_TILE_HALF_SIZE, 0.0, 1.0))
|
||||||
|
velocity += dir * FALLOUT_DRAG_STRENGTH * strength_mult * edge_drag_factor * delta
|
||||||
_handle_movement(delta)
|
_handle_movement(delta)
|
||||||
_handle_interactions()
|
_handle_interactions()
|
||||||
else:
|
else:
|
||||||
@@ -3301,10 +3404,9 @@ func _try_grab():
|
|||||||
closest_body.set_collision_mask_value(2, false)
|
closest_body.set_collision_mask_value(2, false)
|
||||||
closest_body.set_collision_mask_value(7, true) # Keep wall collision
|
closest_body.set_collision_mask_value(7, true) # Keep wall collision
|
||||||
elif _is_player(closest_body):
|
elif _is_player(closest_body):
|
||||||
# Players: remove from layer fully when lifted – no collision with anything
|
# Players: no collision layer at all while held
|
||||||
closest_body.set_collision_layer_value(1, false)
|
closest_body.collision_layer = 0
|
||||||
closest_body.set_collision_mask_value(1, false)
|
closest_body.collision_mask = 0
|
||||||
closest_body.set_collision_mask_value(7, false)
|
|
||||||
|
|
||||||
# When grabbing, immediately try to lift if possible
|
# When grabbing, immediately try to lift if possible
|
||||||
_set_animation("IDLE")
|
_set_animation("IDLE")
|
||||||
@@ -3363,10 +3465,14 @@ func _lift_object():
|
|||||||
if "is_frozen" in held_object:
|
if "is_frozen" in held_object:
|
||||||
held_object.is_frozen = true
|
held_object.is_frozen = true
|
||||||
elif _is_player(held_object):
|
elif _is_player(held_object):
|
||||||
# Player: use set_being_held
|
# Player: use set_being_held (also sets position_z = HELD_POSITION_Z)
|
||||||
if held_object.has_method("set_being_held"):
|
if held_object.has_method("set_being_held"):
|
||||||
held_object.set_being_held(true)
|
held_object.set_being_held(true)
|
||||||
|
|
||||||
|
# Any held object with position_z gets lifted above ground
|
||||||
|
if "position_z" in held_object:
|
||||||
|
held_object.position_z = HELD_POSITION_Z
|
||||||
|
|
||||||
if held_object.has_method("on_lifted"):
|
if held_object.has_method("on_lifted"):
|
||||||
held_object.on_lifted(self)
|
held_object.on_lifted(self)
|
||||||
|
|
||||||
@@ -3453,11 +3559,12 @@ func reset_grab_state():
|
|||||||
if "held_by_player" in held_object:
|
if "held_by_player" in held_object:
|
||||||
held_object.held_by_player = null
|
held_object.held_by_player = null
|
||||||
elif _is_player(held_object):
|
elif _is_player(held_object):
|
||||||
held_object.set_collision_layer_value(1, true)
|
held_object.collision_layer = 1
|
||||||
held_object.set_collision_mask_value(1, true)
|
held_object.collision_mask = 1 | 2 | 64 # players, objects, walls
|
||||||
held_object.set_collision_mask_value(7, true)
|
|
||||||
if held_object.has_method("set_being_held"):
|
if held_object.has_method("set_being_held"):
|
||||||
held_object.set_being_held(false)
|
held_object.set_being_held(false)
|
||||||
|
if "position_z" in held_object:
|
||||||
|
held_object.position_z = 0.0
|
||||||
|
|
||||||
# Stop drag sound if playing
|
# Stop drag sound if playing
|
||||||
if held_object.has_method("stop_drag_sound"):
|
if held_object.has_method("stop_drag_sound"):
|
||||||
@@ -3515,14 +3622,16 @@ func _stop_pushing():
|
|||||||
released_obj.set_collision_mask_value(2, true)
|
released_obj.set_collision_mask_value(2, true)
|
||||||
released_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
released_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
elif _is_player(released_obj):
|
elif _is_player(released_obj):
|
||||||
# Players: back on layer 1
|
# Players: restore collision layer and mask (layer 1, mask 1|2|64 so we collide with players, objects, walls)
|
||||||
released_obj.set_collision_layer_value(1, true)
|
released_obj.collision_layer = 1
|
||||||
released_obj.set_collision_mask_value(1, true)
|
released_obj.collision_mask = 1 | 2 | 64
|
||||||
released_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
|
||||||
|
|
||||||
if released_obj is CharacterBody2D and released_obj.has_method("set_being_held"):
|
if released_obj is CharacterBody2D and released_obj.has_method("set_being_held"):
|
||||||
released_obj.set_being_held(false)
|
released_obj.set_being_held(false)
|
||||||
|
|
||||||
|
if "position_z" in released_obj:
|
||||||
|
released_obj.position_z = 0.0
|
||||||
|
|
||||||
# Ensure position stays exactly where it is - no movement on release!
|
# Ensure position stays exactly where it is - no movement on release!
|
||||||
# Do this AFTER calling on_released in case it tries to change position
|
# Do this AFTER calling on_released in case it tries to change position
|
||||||
if released_obj.has_method("on_released"):
|
if released_obj.has_method("on_released"):
|
||||||
@@ -3640,10 +3749,11 @@ func _throw_object():
|
|||||||
if thrown_obj.has_method("set_being_held"):
|
if thrown_obj.has_method("set_being_held"):
|
||||||
thrown_obj.set_being_held(false)
|
thrown_obj.set_being_held(false)
|
||||||
|
|
||||||
# Re-add to layer DIRECTLY when thrown (no delay)
|
# Re-add to layer DIRECTLY when thrown (no delay); restore full mask 1|2|64
|
||||||
if thrown_obj and is_instance_valid(thrown_obj):
|
if thrown_obj and is_instance_valid(thrown_obj):
|
||||||
thrown_obj.set_collision_layer_value(1, true)
|
thrown_obj.set_collision_layer_value(1, true)
|
||||||
thrown_obj.set_collision_mask_value(1, true)
|
thrown_obj.set_collision_mask_value(1, true)
|
||||||
|
thrown_obj.set_collision_mask_value(2, true)
|
||||||
thrown_obj.set_collision_mask_value(7, true)
|
thrown_obj.set_collision_mask_value(7, true)
|
||||||
elif thrown_obj and thrown_obj.has_method("can_be_grabbed") and thrown_obj.can_be_grabbed():
|
elif thrown_obj and thrown_obj.has_method("can_be_grabbed") and thrown_obj.can_be_grabbed():
|
||||||
# Bomb or other grabbable object - handle like box
|
# Bomb or other grabbable object - handle like box
|
||||||
@@ -3785,9 +3895,8 @@ func _force_throw_held_object(direction: Vector2):
|
|||||||
|
|
||||||
# Re-add to layer DIRECTLY when thrown (no delay)
|
# Re-add to layer DIRECTLY when thrown (no delay)
|
||||||
if thrown_obj and is_instance_valid(thrown_obj):
|
if thrown_obj and is_instance_valid(thrown_obj):
|
||||||
thrown_obj.set_collision_layer_value(1, true)
|
thrown_obj.collision_layer = 1
|
||||||
thrown_obj.set_collision_mask_value(1, true)
|
thrown_obj.collision_mask = 1 | 2 | 64 # players, objects, walls
|
||||||
thrown_obj.set_collision_mask_value(7, true)
|
|
||||||
elif thrown_obj and thrown_obj.has_method("can_be_grabbed") and thrown_obj.can_be_grabbed():
|
elif thrown_obj and thrown_obj.has_method("can_be_grabbed") and thrown_obj.can_be_grabbed():
|
||||||
# Other grabbable object - handle like box
|
# Other grabbable object - handle like box
|
||||||
thrown_obj.global_position = throw_start_pos
|
thrown_obj.global_position = throw_start_pos
|
||||||
@@ -3917,10 +4026,9 @@ func _place_down_object():
|
|||||||
if "velocity_z" in placed_obj:
|
if "velocity_z" in placed_obj:
|
||||||
placed_obj.velocity_z = 0.0
|
placed_obj.velocity_z = 0.0
|
||||||
elif _is_player(placed_obj):
|
elif _is_player(placed_obj):
|
||||||
# Player: back on layer 1
|
# Player: restore collision layer and mask (1|2|64 so we collide with players, objects, walls)
|
||||||
placed_obj.set_collision_layer_value(1, true)
|
placed_obj.collision_layer = 1
|
||||||
placed_obj.set_collision_mask_value(1, true)
|
placed_obj.collision_mask = 1 | 2 | 64
|
||||||
placed_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
|
||||||
placed_obj.global_position = place_pos
|
placed_obj.global_position = place_pos
|
||||||
placed_obj.velocity = Vector2.ZERO
|
placed_obj.velocity = Vector2.ZERO
|
||||||
if placed_obj.has_method("set_being_held"):
|
if placed_obj.has_method("set_being_held"):
|
||||||
@@ -5223,7 +5331,7 @@ func _update_lifted_object():
|
|||||||
held_object = null
|
held_object = null
|
||||||
return
|
return
|
||||||
|
|
||||||
# Object floats above player's head
|
# Object floats above player's head (XY) and at HELD_POSITION_Z (above ground, immune to fallout)
|
||||||
var target_pos = position + Vector2(0, -12) # Above head
|
var target_pos = position + Vector2(0, -12) # Above head
|
||||||
|
|
||||||
# Instant follow for local player, smooth for network sync
|
# Instant follow for local player, smooth for network sync
|
||||||
@@ -5232,6 +5340,10 @@ func _update_lifted_object():
|
|||||||
else:
|
else:
|
||||||
held_object.global_position = held_object.global_position.lerp(target_pos, 0.3)
|
held_object.global_position = held_object.global_position.lerp(target_pos, 0.3)
|
||||||
|
|
||||||
|
# Keep held object at Z height so it's "above" ground (no fallout under it)
|
||||||
|
if "position_z" in held_object:
|
||||||
|
held_object.position_z = HELD_POSITION_Z
|
||||||
|
|
||||||
# Sync held object position over network
|
# Sync held object position over network
|
||||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||||
var obj_name = _get_object_name_for_sync(held_object)
|
var obj_name = _get_object_name_for_sync(held_object)
|
||||||
@@ -5301,6 +5413,12 @@ func _update_pushed_object():
|
|||||||
var results = space_state.intersect_point(query)
|
var results = space_state.intersect_point(query)
|
||||||
was_blocked = results.size() > 0
|
was_blocked = results.size() > 0
|
||||||
|
|
||||||
|
# StonePillar must NOT be pushed onto fallout - treat fallout as solid
|
||||||
|
if not was_blocked and held_object.get("object_type") == "Pillar":
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw and gw.has_method("_is_position_on_fallout_tile") and gw._is_position_on_fallout_tile(target_pos):
|
||||||
|
was_blocked = true
|
||||||
|
|
||||||
# Update the flag for next frame's input handling
|
# Update the flag for next frame's input handling
|
||||||
object_blocked_by_wall = was_blocked
|
object_blocked_by_wall = was_blocked
|
||||||
|
|
||||||
@@ -5777,10 +5895,11 @@ func _sync_throw(obj_name: String, throw_pos: Vector2, force: Vector2, thrower_n
|
|||||||
|
|
||||||
print("Player is now airborne on client!")
|
print("Player is now airborne on client!")
|
||||||
|
|
||||||
# Re-add to layer DIRECTLY when thrown (no delay)
|
# Re-add to layer DIRECTLY when thrown (no delay); full mask 1|2|64
|
||||||
if obj and is_instance_valid(obj):
|
if obj and is_instance_valid(obj):
|
||||||
obj.set_collision_layer_value(1, true)
|
obj.set_collision_layer_value(1, true)
|
||||||
obj.set_collision_mask_value(1, true)
|
obj.set_collision_mask_value(1, true)
|
||||||
|
obj.set_collision_mask_value(2, true)
|
||||||
obj.set_collision_mask_value(7, true)
|
obj.set_collision_mask_value(7, true)
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
@@ -5818,9 +5937,8 @@ func _sync_initial_grab(obj_name: String, _offset: Vector2):
|
|||||||
obj.set_collision_mask_value(1, false)
|
obj.set_collision_mask_value(1, false)
|
||||||
obj.set_collision_mask_value(2, false)
|
obj.set_collision_mask_value(2, false)
|
||||||
elif _is_player(obj):
|
elif _is_player(obj):
|
||||||
obj.set_collision_layer_value(1, false)
|
obj.collision_layer = 0
|
||||||
obj.set_collision_mask_value(1, false)
|
obj.collision_mask = 0
|
||||||
obj.set_collision_mask_value(7, false)
|
|
||||||
|
|
||||||
print("Synced initial grab on client: ", obj_name)
|
print("Synced initial grab on client: ", obj_name)
|
||||||
|
|
||||||
@@ -5868,9 +5986,8 @@ func _sync_grab(obj_name: String, is_lift: bool, axis: Vector2 = Vector2.ZERO):
|
|||||||
if "throw_velocity" in obj:
|
if "throw_velocity" in obj:
|
||||||
obj.throw_velocity = Vector2.ZERO
|
obj.throw_velocity = Vector2.ZERO
|
||||||
elif _is_player(obj):
|
elif _is_player(obj):
|
||||||
obj.set_collision_layer_value(1, false)
|
obj.collision_layer = 0
|
||||||
obj.set_collision_mask_value(1, false)
|
obj.collision_mask = 0
|
||||||
obj.set_collision_mask_value(7, false)
|
|
||||||
if obj.has_method("set_being_held"):
|
if obj.has_method("set_being_held"):
|
||||||
obj.set_being_held(true)
|
obj.set_being_held(true)
|
||||||
else:
|
else:
|
||||||
@@ -5887,9 +6004,8 @@ func _sync_grab(obj_name: String, is_lift: bool, axis: Vector2 = Vector2.ZERO):
|
|||||||
if "throw_velocity" in obj:
|
if "throw_velocity" in obj:
|
||||||
obj.throw_velocity = Vector2.ZERO
|
obj.throw_velocity = Vector2.ZERO
|
||||||
elif _is_player(obj):
|
elif _is_player(obj):
|
||||||
obj.set_collision_layer_value(1, false)
|
obj.collision_layer = 0
|
||||||
obj.set_collision_mask_value(1, false)
|
obj.collision_mask = 0
|
||||||
obj.set_collision_mask_value(7, false)
|
|
||||||
if obj.has_method("set_being_held"):
|
if obj.has_method("set_being_held"):
|
||||||
obj.set_being_held(true)
|
obj.set_being_held(true)
|
||||||
|
|
||||||
@@ -5940,6 +6056,7 @@ func _sync_release(obj_name: String):
|
|||||||
elif _is_player(obj):
|
elif _is_player(obj):
|
||||||
obj.set_collision_layer_value(1, true)
|
obj.set_collision_layer_value(1, true)
|
||||||
obj.set_collision_mask_value(1, true)
|
obj.set_collision_mask_value(1, true)
|
||||||
|
obj.set_collision_mask_value(2, true)
|
||||||
obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
if obj.has_method("set_being_held"):
|
if obj.has_method("set_being_held"):
|
||||||
obj.set_being_held(false)
|
obj.set_being_held(false)
|
||||||
@@ -6000,6 +6117,7 @@ func _sync_place_down(obj_name: String, place_pos: Vector2):
|
|||||||
elif _is_player(obj):
|
elif _is_player(obj):
|
||||||
obj.set_collision_layer_value(1, true)
|
obj.set_collision_layer_value(1, true)
|
||||||
obj.set_collision_mask_value(1, true)
|
obj.set_collision_mask_value(1, true)
|
||||||
|
obj.set_collision_mask_value(2, true)
|
||||||
obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
obj.velocity = Vector2.ZERO
|
obj.velocity = Vector2.ZERO
|
||||||
if obj.has_method("set_being_held"):
|
if obj.has_method("set_being_held"):
|
||||||
@@ -6126,6 +6244,7 @@ func _break_free_from_holder():
|
|||||||
struggle_time = 0.0
|
struggle_time = 0.0
|
||||||
struggle_direction = Vector2.ZERO
|
struggle_direction = Vector2.ZERO
|
||||||
being_held_by = null
|
being_held_by = null
|
||||||
|
is_being_held = false
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _sync_break_free(holder_name: String, direction: Vector2):
|
func _sync_break_free(holder_name: String, direction: Vector2):
|
||||||
@@ -6182,6 +6301,7 @@ func _force_place_down(direction: Vector2):
|
|||||||
elif _is_player(placed_obj):
|
elif _is_player(placed_obj):
|
||||||
placed_obj.set_collision_layer_value(1, true)
|
placed_obj.set_collision_layer_value(1, true)
|
||||||
placed_obj.set_collision_mask_value(1, true)
|
placed_obj.set_collision_mask_value(1, true)
|
||||||
|
placed_obj.set_collision_mask_value(2, true)
|
||||||
placed_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
placed_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
placed_obj.velocity = Vector2.ZERO
|
placed_obj.velocity = Vector2.ZERO
|
||||||
if placed_obj.has_method("set_being_held"):
|
if placed_obj.has_method("set_being_held"):
|
||||||
@@ -6193,19 +6313,22 @@ func _force_place_down(direction: Vector2):
|
|||||||
print("Forced to place down ", placed_obj.name)
|
print("Forced to place down ", placed_obj.name)
|
||||||
|
|
||||||
func set_being_held(held: bool):
|
func set_being_held(held: bool):
|
||||||
# When being held by another player, disable movement
|
# When being held by another player, disable movement and collision; use HELD_POSITION_Z so we're "above" ground (immune to fallout)
|
||||||
# But keep physics_process running for network sync
|
is_being_held = held
|
||||||
if held:
|
if held:
|
||||||
# Just prevent input handling, don't disable physics
|
|
||||||
velocity = Vector2.ZERO
|
velocity = Vector2.ZERO
|
||||||
is_airborne = false
|
is_airborne = false
|
||||||
position_z = 0.0
|
position_z = HELD_POSITION_Z
|
||||||
velocity_z = 0.0
|
velocity_z = 0.0
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 0
|
||||||
else:
|
else:
|
||||||
# Released - reset struggle state
|
|
||||||
struggle_time = 0.0
|
struggle_time = 0.0
|
||||||
struggle_direction = Vector2.ZERO
|
struggle_direction = Vector2.ZERO
|
||||||
being_held_by = null
|
being_held_by = null
|
||||||
|
position_z = 0.0
|
||||||
|
collision_layer = 1
|
||||||
|
collision_mask = 1 | 2 | 64 # layer 1 players, 2 objects, 7 walls
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func rpc_grabbed_by_enemy_hand(enemy_name: String) -> void:
|
func rpc_grabbed_by_enemy_hand(enemy_name: String) -> void:
|
||||||
@@ -6257,10 +6380,13 @@ func rpc_take_damage(amount: float, attacker_position: Vector2, is_burn_damage:
|
|||||||
if is_multiplayer_authority():
|
if is_multiplayer_authority():
|
||||||
take_damage(amount, attacker_position, is_burn_damage, apply_burn_debuff)
|
take_damage(amount, attacker_position, is_burn_damage, apply_burn_debuff)
|
||||||
|
|
||||||
func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool = false, apply_burn_debuff: bool = false):
|
func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool = false, apply_burn_debuff: bool = false, no_knockback: bool = false):
|
||||||
# Don't take damage if already dead
|
# Don't take damage if already dead
|
||||||
if is_dead:
|
if is_dead:
|
||||||
return
|
return
|
||||||
|
# Invulnerable during fallout sink (can't take damage from anything while falling)
|
||||||
|
if fallout_state:
|
||||||
|
return
|
||||||
|
|
||||||
# Cancel bow charging when taking damage
|
# Cancel bow charging when taking damage
|
||||||
if is_charging_bow:
|
if is_charging_bow:
|
||||||
@@ -6391,8 +6517,8 @@ func take_damage(amount: float, attacker_position: Vector2, is_burn_damage: bool
|
|||||||
# Lock facing direction briefly so player can't change it while taking damage
|
# Lock facing direction briefly so player can't change it while taking damage
|
||||||
damage_direction_lock_timer = damage_direction_lock_duration
|
damage_direction_lock_timer = damage_direction_lock_duration
|
||||||
|
|
||||||
# Only apply knockback if not burn damage
|
# Only apply knockback if not burn damage and not suppressed (e.g. fallout respawn)
|
||||||
if not is_burn_damage:
|
if not is_burn_damage and not no_knockback:
|
||||||
# Calculate direction FROM attacker TO victim
|
# Calculate direction FROM attacker TO victim
|
||||||
var direction_from_attacker = (global_position - attacker_position).normalized()
|
var direction_from_attacker = (global_position - attacker_position).normalized()
|
||||||
|
|
||||||
@@ -6476,6 +6602,7 @@ func _die():
|
|||||||
elif _is_player(released_obj):
|
elif _is_player(released_obj):
|
||||||
released_obj.set_collision_layer_value(1, true)
|
released_obj.set_collision_layer_value(1, true)
|
||||||
released_obj.set_collision_mask_value(1, true)
|
released_obj.set_collision_mask_value(1, true)
|
||||||
|
released_obj.set_collision_mask_value(2, true)
|
||||||
released_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
released_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
if released_obj.has_method("set_being_held"):
|
if released_obj.has_method("set_being_held"):
|
||||||
released_obj.set_being_held(false)
|
released_obj.set_being_held(false)
|
||||||
@@ -6539,10 +6666,9 @@ func _die():
|
|||||||
other_player.grab_offset = Vector2.ZERO
|
other_player.grab_offset = Vector2.ZERO
|
||||||
other_player.push_axis = Vector2.ZERO
|
other_player.push_axis = Vector2.ZERO
|
||||||
|
|
||||||
# Re-enable our collision
|
# Re-enable our collision (layer 1, mask 1|2|64)
|
||||||
set_collision_layer_value(1, true)
|
collision_layer = 1
|
||||||
set_collision_mask_value(1, true)
|
collision_mask = 1 | 2 | 64
|
||||||
set_collision_mask_value(7, true) # Re-enable wall collision!
|
|
||||||
|
|
||||||
# THEN sync to other clients
|
# THEN sync to other clients
|
||||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
@@ -6555,6 +6681,7 @@ func _die():
|
|||||||
print(name, " is NOT being held by anyone")
|
print(name, " is NOT being held by anyone")
|
||||||
|
|
||||||
being_held_by = null
|
being_held_by = null
|
||||||
|
is_being_held = false
|
||||||
|
|
||||||
# Replicas: no wait loop; we get _sync_respawn from authority.
|
# Replicas: no wait loop; we get _sync_respawn from authority.
|
||||||
if not is_multiplayer_authority():
|
if not is_multiplayer_authority():
|
||||||
@@ -6666,6 +6793,24 @@ func _spawn_landing_stand_up():
|
|||||||
if game_world and game_world.has_method("_start_bg_music"):
|
if game_world and game_world.has_method("_start_bg_music"):
|
||||||
game_world._start_bg_music()
|
game_world._start_bg_music()
|
||||||
|
|
||||||
|
func _respawn_from_fallout():
|
||||||
|
# Teleport to last safe tile, reset fallout state, then apply 1 HP damage via take_damage (no knockback)
|
||||||
|
var gw = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if gw and gw.has_method("get_last_safe_position_for_player"):
|
||||||
|
global_position = gw.get_last_safe_position_for_player(self)
|
||||||
|
fallout_state = false
|
||||||
|
fallout_scale_progress = 1.0
|
||||||
|
fallout_respawn_delay_timer = 0.0
|
||||||
|
scale = Vector2.ONE
|
||||||
|
rotation = 0.0
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
fallout_respawn_stun_timer = FALLOUT_RESPAWN_STUN_DURATION
|
||||||
|
_set_animation("IDLE")
|
||||||
|
# Apply damage via take_damage (shows damage number, sound, etc.) but with no knockback
|
||||||
|
take_damage(FALLOUT_RESPAWN_HP_PENALTY, global_position, false, false, true)
|
||||||
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree():
|
||||||
|
_rpc_to_ready_peers("_sync_respawn_from_fallout", [global_position])
|
||||||
|
|
||||||
func _respawn():
|
func _respawn():
|
||||||
print(name, " respawning!")
|
print(name, " respawning!")
|
||||||
was_revived = false
|
was_revived = false
|
||||||
@@ -6684,6 +6829,7 @@ func _respawn():
|
|||||||
# Re-enable collision in case it was disabled while being carried
|
# Re-enable collision in case it was disabled while being carried
|
||||||
set_collision_layer_value(1, true)
|
set_collision_layer_value(1, true)
|
||||||
set_collision_mask_value(1, true)
|
set_collision_mask_value(1, true)
|
||||||
|
set_collision_mask_value(2, true)
|
||||||
set_collision_mask_value(7, true) # Re-enable wall collision!
|
set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
|
|
||||||
# Reset health and state
|
# Reset health and state
|
||||||
@@ -6815,9 +6961,10 @@ func _force_holder_to_drop_local(holder_name: String):
|
|||||||
holder.grab_offset = Vector2.ZERO
|
holder.grab_offset = Vector2.ZERO
|
||||||
holder.push_axis = Vector2.ZERO
|
holder.push_axis = Vector2.ZERO
|
||||||
|
|
||||||
# Re-enable collision on dropped player
|
# Re-enable collision on dropped player (layer 1, mask 1|2|64)
|
||||||
set_collision_layer_value(1, true)
|
set_collision_layer_value(1, true)
|
||||||
set_collision_mask_value(1, true)
|
set_collision_mask_value(1, true)
|
||||||
|
set_collision_mask_value(2, true)
|
||||||
set_collision_mask_value(7, true) # Re-enable wall collision!
|
set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
else:
|
else:
|
||||||
print(" ✗ held_object doesn't match self")
|
print(" ✗ held_object doesn't match self")
|
||||||
@@ -6936,14 +7083,43 @@ func _sync_death():
|
|||||||
_apply_death_visual()
|
_apply_death_visual()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_fallout_start(tile_center_pos: Vector2):
|
||||||
|
# Other clients: start fallout sink visuals; ignore if this player is being held (immune to fallout)
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
if is_being_held:
|
||||||
|
return
|
||||||
|
global_position = tile_center_pos
|
||||||
|
fallout_state = true
|
||||||
|
fallout_scale_progress = 1.0
|
||||||
|
fallout_respawn_delay_timer = 0.0
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
current_direction = Direction.DOWN
|
||||||
|
facing_direction_vector = Vector2.DOWN
|
||||||
|
_set_animation("FALL")
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_respawn_from_fallout(safe_pos: Vector2):
|
||||||
|
if not is_multiplayer_authority():
|
||||||
|
global_position = safe_pos
|
||||||
|
fallout_state = false
|
||||||
|
fallout_scale_progress = 1.0
|
||||||
|
fallout_respawn_delay_timer = 0.0
|
||||||
|
scale = Vector2.ONE
|
||||||
|
rotation = 0.0
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
fallout_respawn_stun_timer = FALLOUT_RESPAWN_STUN_DURATION
|
||||||
|
_set_animation("IDLE")
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _sync_respawn(spawn_pos: Vector2):
|
func _sync_respawn(spawn_pos: Vector2):
|
||||||
if not is_multiplayer_authority():
|
if not is_multiplayer_authority():
|
||||||
# being_held_by already cleared via RPC in _die()
|
# being_held_by already cleared via RPC in _die()
|
||||||
# Holder already dropped us via _force_holder_to_drop RPC
|
# Holder already dropped us via _force_holder_to_drop RPC
|
||||||
# Re-enable collision in case it was disabled while being carried
|
# Re-enable collision in case it was disabled while being carried (layer 1, mask 1|2|64)
|
||||||
set_collision_layer_value(1, true)
|
set_collision_layer_value(1, true)
|
||||||
set_collision_mask_value(1, true)
|
set_collision_mask_value(1, true)
|
||||||
|
set_collision_mask_value(2, true)
|
||||||
set_collision_mask_value(7, true) # Re-enable wall collision!
|
set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||||
|
|
||||||
# Reset health and state
|
# Reset health and state
|
||||||
|
|||||||
Reference in New Issue
Block a user