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
|
||||
10:9/0 = 0
|
||||
11:9/0 = 0
|
||||
11:9/0/custom_data_0 = 1
|
||||
12:9/0 = 0
|
||||
12:9/0/custom_data_0 = 1
|
||||
13:9/0 = 0
|
||||
14:9/0 = 0
|
||||
15:9/0 = 0
|
||||
16:9/0 = 0
|
||||
16:9/0/custom_data_0 = 2
|
||||
17:9/0 = 0
|
||||
17:9/0/custom_data_0 = 2
|
||||
18:9/0 = 0
|
||||
19:9/0 = 0
|
||||
0:10/0 = 0
|
||||
@@ -321,9 +325,11 @@ separation = Vector2i(1, 1)
|
||||
11:10/0 = 0
|
||||
12:10/0 = 0
|
||||
13:10/0 = 0
|
||||
13:10/0/custom_data_0 = -1
|
||||
14:10/0 = 0
|
||||
15:10/0 = 0
|
||||
16:10/0 = 0
|
||||
16:10/0/custom_data_0 = -1
|
||||
17:10/0 = 0
|
||||
1:11/0 = 0
|
||||
2:11/0 = 0
|
||||
@@ -400,9 +406,11 @@ separation = Vector2i(1, 1)
|
||||
11:13/0/custom_data_0 = -1
|
||||
12:13/0 = 0
|
||||
13:13/0 = 0
|
||||
13:13/0/custom_data_0 = -1
|
||||
14:13/0 = 0
|
||||
15:13/0 = 0
|
||||
16:13/0 = 0
|
||||
16:13/0/custom_data_0 = -1
|
||||
17:13/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)
|
||||
@@ -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/custom_data_0 = 8
|
||||
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
|
||||
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
|
||||
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
|
||||
5:14/0 = 0
|
||||
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:1/0 = 0
|
||||
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]
|
||||
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/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"]
|
||||
radius = 5.0
|
||||
|
||||
@@ -227,23 +245,13 @@ stream_2/stream = ExtResource("11_5x2ph")
|
||||
stream_3/stream = ExtResource("12_oynfq")
|
||||
stream_4/stream = ExtResource("13_b0veo")
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_shield"]
|
||||
shader = ExtResource("4_r7ul0")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_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")
|
||||
|
||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_bow"]
|
||||
playback_mode = 1
|
||||
@@ -253,14 +261,6 @@ stream_0/stream = ExtResource("22_sfx")
|
||||
stream_1/stream = ExtResource("23_sfx")
|
||||
stream_2/stream = ExtResource("24_sfx")
|
||||
|
||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_block"]
|
||||
playback_mode = 1
|
||||
streams_count = 4
|
||||
stream_0/stream = ExtResource("18_sfx")
|
||||
stream_1/stream = ExtResource("19_sfx")
|
||||
stream_2/stream = ExtResource("20_sfx")
|
||||
stream_3/stream = ExtResource("21_sfx")
|
||||
|
||||
[node name="EnemyHumanoid" type="CharacterBody2D" unique_id=285357386]
|
||||
collision_layer = 2
|
||||
collision_mask = 65
|
||||
@@ -269,13 +269,14 @@ script = ExtResource("1")
|
||||
|
||||
[node name="Shadow" type="Sprite2D" parent="." unique_id=468462304]
|
||||
z_index = -1
|
||||
position = Vector2(0, 7)
|
||||
position = Vector2(0, 3)
|
||||
texture = SubResource("GradientTexture2D_1")
|
||||
script = ExtResource("2")
|
||||
|
||||
[node name="Sprite2DBody" type="Sprite2D" parent="." unique_id=855871821]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_uedn7")
|
||||
position = Vector2(0, -4)
|
||||
texture = ExtResource("3")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
@@ -283,59 +284,67 @@ vframes = 8
|
||||
[node name="Sprite2DBoots" type="Sprite2D" parent="." unique_id=460958943]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_5x2ph")
|
||||
position = Vector2(0, -4)
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DArmour" type="Sprite2D" parent="." unique_id=6790482]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_r7ul0")
|
||||
position = Vector2(0, -4)
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DFacialHair" type="Sprite2D" parent="." unique_id=31110906]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_oynfq")
|
||||
position = Vector2(0, -4)
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DHair" type="Sprite2D" parent="." unique_id=425592986]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_b0veo")
|
||||
position = Vector2(0, -4)
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DEyes" type="Sprite2D" parent="." unique_id=496437887]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_of8l8")
|
||||
position = Vector2(0, -4)
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DEyeLashes" type="Sprite2D" parent="." unique_id=1799398723]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_ofeay")
|
||||
position = Vector2(0, -4)
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DAddons" type="Sprite2D" parent="." unique_id=1702763725]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_5a33a")
|
||||
position = Vector2(0, -4)
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DHeadgear" type="Sprite2D" parent="." unique_id=164186416]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_i1636")
|
||||
position = Vector2(0, -4)
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DShield" type="Sprite2D" parent="."]
|
||||
[node name="Sprite2DShield" type="Sprite2D" parent="." unique_id=470468744]
|
||||
visible = false
|
||||
material = SubResource("ShaderMaterial_shield")
|
||||
texture = ExtResource("14_shield")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DShieldHolding" type="Sprite2D" parent="."]
|
||||
[node name="Sprite2DShieldHolding" type="Sprite2D" parent="." unique_id=1318098286]
|
||||
visible = false
|
||||
material = SubResource("ShaderMaterial_shield")
|
||||
texture = ExtResource("15_shieldh")
|
||||
@@ -344,11 +353,13 @@ vframes = 8
|
||||
|
||||
[node name="Sprite2DWeapon" type="Sprite2D" parent="." unique_id=1718282928]
|
||||
y_sort_enabled = true
|
||||
position = Vector2(0, -4)
|
||||
texture = ExtResource("4")
|
||||
hframes = 35
|
||||
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]
|
||||
visible = false
|
||||
@@ -365,10 +376,10 @@ texture = ExtResource("6")
|
||||
hframes = 3
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=189217716]
|
||||
position = Vector2(0, 4)
|
||||
shape = SubResource("CircleShape2D_1")
|
||||
|
||||
[node name="AttackArea" type="Area2D" parent="." unique_id=1923132385]
|
||||
position = Vector2(0, -4)
|
||||
collision_layer = 0
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="AttackArea" unique_id=1597070641]
|
||||
@@ -376,12 +387,14 @@ position = Vector2(0, 4)
|
||||
shape = SubResource("CircleShape2D_1")
|
||||
|
||||
[node name="AggroArea" type="Area2D" parent="." unique_id=1234567890]
|
||||
position = Vector2(0, -4)
|
||||
collision_layer = 0
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="AggroArea" unique_id=1286608618]
|
||||
shape = SubResource("CircleShape2D_2")
|
||||
|
||||
[node name="SfxDie" type="AudioStreamPlayer2D" parent="." unique_id=693933783]
|
||||
position = Vector2(0, -4)
|
||||
stream = SubResource("AudioStreamRandomizer_fikv0")
|
||||
max_distance = 930.0
|
||||
attenuation = 8.282114
|
||||
@@ -390,6 +403,7 @@ panning_strength = 1.3
|
||||
bus = &"Sfx"
|
||||
|
||||
[node name="SfxAlertFoundPlayer" type="AudioStreamPlayer2D" parent="." unique_id=815591859]
|
||||
position = Vector2(0, -4)
|
||||
stream = SubResource("AudioStreamRandomizer_37mja")
|
||||
max_distance = 1146.0
|
||||
attenuation = 8.57418
|
||||
@@ -397,45 +411,53 @@ max_polyphony = 4
|
||||
panning_strength = 1.04
|
||||
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")
|
||||
volume_db = 9.695
|
||||
attenuation = 1.3660401
|
||||
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")
|
||||
volume_db = 7.254
|
||||
attenuation = 1.3195078
|
||||
panning_strength = 1.06
|
||||
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")
|
||||
pitch_scale = 1.33
|
||||
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")
|
||||
max_distance = 1455.0
|
||||
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")
|
||||
attenuation = 7.727478
|
||||
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")
|
||||
|
||||
[node name="SfxThrow" type="AudioStreamPlayer2D" parent="."]
|
||||
[node name="SfxThrow" type="AudioStreamPlayer2D" parent="." unique_id=92763809]
|
||||
position = Vector2(0, -4)
|
||||
stream = ExtResource("28_sfx")
|
||||
pitch_scale = 0.61
|
||||
max_distance = 983.0
|
||||
attenuation = 8.876549
|
||||
panning_strength = 1.04
|
||||
|
||||
[node name="SfxLift" type="AudioStreamPlayer2D" parent="."]
|
||||
[node name="SfxLift" type="AudioStreamPlayer2D" parent="." unique_id=722569138]
|
||||
position = Vector2(0, -4)
|
||||
stream = ExtResource("29_sfx")
|
||||
max_distance = 1246.0
|
||||
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/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"]
|
||||
shader = ExtResource("4_bhwwd")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
@@ -92,7 +191,16 @@ z_index = -2
|
||||
material = SubResource("ShaderMaterial_pdbwf")
|
||||
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]
|
||||
material = SubResource("ShaderMaterial_ph1f2")
|
||||
tile_set = ExtResource("9")
|
||||
|
||||
[node name="TileMapLayerAbove" type="TileMapLayer" parent="Environment" unique_id=1234567892]
|
||||
|
||||
@@ -289,6 +289,9 @@ radius = 4.0
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_2"]
|
||||
radius = 8.0
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_quicksand"]
|
||||
size = Vector2(8, 8)
|
||||
|
||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_l71n6"]
|
||||
playback_mode = 1
|
||||
random_pitch = 1.0118532
|
||||
@@ -340,6 +343,21 @@ tracks/0/keys = {
|
||||
"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"]
|
||||
resource_name = "fire_charging"
|
||||
length = 0.4
|
||||
@@ -459,6 +477,7 @@ tracks/0/keys = {
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_2dvfe"]
|
||||
_data = {
|
||||
&"RESET": SubResource("Animation_t4otl"),
|
||||
&"fallout": SubResource("Animation_6e8lb"),
|
||||
&"fire_charging": SubResource("Animation_j2b1d"),
|
||||
&"fire_ready": SubResource("Animation_cs1tg"),
|
||||
&"frost_charging": SubResource("Animation_frost_ch"),
|
||||
@@ -702,11 +721,20 @@ collision_mask = 3
|
||||
shape = SubResource("CircleShape2D_2")
|
||||
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]
|
||||
offset_left = -10.0
|
||||
offset_top = -15.0
|
||||
offset_right = 10.0
|
||||
offset_bottom = -9.0
|
||||
offset_bottom = 8.0
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="InteractionIndicator" type="Sprite2D" parent="." unique_id=1661043470]
|
||||
@@ -732,6 +760,12 @@ volume_db = -2.537
|
||||
attenuation = 8.876548
|
||||
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]
|
||||
stream = SubResource("AudioStreamRandomizer_487ah")
|
||||
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 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 explosion_sprite = $ExplosionSprite
|
||||
@onready var shadow = $Shadow
|
||||
@@ -143,7 +148,10 @@ func _start_fuse():
|
||||
|
||||
func _physics_process(delta):
|
||||
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
|
||||
if explosion_sprite:
|
||||
# Play 9 frames of explosion animation at ~15 FPS
|
||||
@@ -157,6 +165,21 @@ func _physics_process(delta):
|
||||
queue_free()
|
||||
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
|
||||
if is_fused:
|
||||
fuse_timer += delta
|
||||
@@ -183,7 +206,7 @@ func _physics_process(delta):
|
||||
|
||||
# Update sprite position and rotation based on height
|
||||
if sprite:
|
||||
sprite.position.y = -position_z * 0.5
|
||||
sprite.position.y = - position_z * 0.5
|
||||
var height_scale = 1.0 - (position_z / 50.0) * 0.2
|
||||
sprite.scale = Vector2.ONE * max(0.8, height_scale)
|
||||
sprite.rotation += rotation_speed * delta
|
||||
@@ -246,6 +269,15 @@ func _land():
|
||||
position_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
|
||||
if not is_fused:
|
||||
_start_fuse()
|
||||
@@ -256,7 +288,27 @@ func _explode():
|
||||
|
||||
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:
|
||||
sprite.visible = false
|
||||
if shadow:
|
||||
@@ -267,38 +319,27 @@ func _explode():
|
||||
explosion_frame = 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:
|
||||
explosion_light.enabled = true
|
||||
# Fade out explosion light over time
|
||||
var tween = create_tween()
|
||||
tween.tween_property(explosion_light, "energy", 0.0, 0.3)
|
||||
tween.tween_callback(func(): if explosion_light: explosion_light.enabled = false)
|
||||
|
||||
# Play explosion sound
|
||||
if has_node("SfxExplosion"):
|
||||
$SfxExplosion.play()
|
||||
|
||||
# Deal area 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()
|
||||
|
||||
# Spawn tile debris particles (4 pieces per affected tile, bounce, fade)
|
||||
_spawn_explosion_tile_particles()
|
||||
if has_node("SfxDebrisFromParticles"):
|
||||
$SfxDebrisFromParticles.play()
|
||||
|
||||
# Disable collision
|
||||
if bomb_area:
|
||||
bomb_area.set_deferred("monitoring", false)
|
||||
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_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
|
||||
const DOOR_UP_START = Vector2i(7, 0) # 3x2 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_DOWN_START = Vector2i(7, 5) # 3x2 large, middle tile is (6,6) instead of (8,6)
|
||||
|
||||
# Ground/floor tiles (random selection)
|
||||
const FLOOR_TILES = [Vector2i(9, 8), Vector2i(14, 8), Vector2i(6, 11)]
|
||||
# Base floor tile always on DungeonLayer0 (standard floor)
|
||||
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
|
||||
# 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)
|
||||
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):
|
||||
grid.append([])
|
||||
tile_grid.append([])
|
||||
decorated_tile_grid.append([])
|
||||
cracked_tile_grid.append([])
|
||||
for y in range(map_size.y):
|
||||
grid[x].append(0) # Start with all walls
|
||||
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_doors = []
|
||||
@@ -271,6 +298,18 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
|
||||
# 7. Render walls around rooms
|
||||
_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)
|
||||
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():
|
||||
@@ -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 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)
|
||||
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:
|
||||
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():
|
||||
@@ -348,7 +403,7 @@ func generate_dungeon(map_size: Vector2i, seed_value: int = 0, level: int = 1) -
|
||||
var room = all_rooms[i]
|
||||
# Skip start room and exit room
|
||||
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)
|
||||
|
||||
# 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,
|
||||
"grid": grid,
|
||||
"tile_grid": tile_grid,
|
||||
"decorated_tile_grid": decorated_tile_grid,
|
||||
"cracked_tile_grid": cracked_tile_grid,
|
||||
"map_size": map_size,
|
||||
"start_room": all_rooms[start_room_index],
|
||||
"exit_room": all_rooms[exit_room_index]
|
||||
}
|
||||
|
||||
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)
|
||||
# Leave 2 tile border for walls (walls are 2 tiles tall)
|
||||
func _set_floor(room: Dictionary, grid: Array, tile_grid: Array, map_size: Vector2i, _rng: RandomNumberGenerator):
|
||||
# Set floor tiles for the room (interior only); base layer always (0,15)
|
||||
for x in range(room.x + 2, room.x + room.w - 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:
|
||||
grid[x][y] = 1 # Floor
|
||||
# Random floor tile variation
|
||||
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
|
||||
tile_grid[x][y] = floor_tile
|
||||
tile_grid[x][y] = FLOOR_BASE
|
||||
|
||||
func _try_place_room_near(source_room: Dictionary, grid: Array, map_size: Vector2i, rng: RandomNumberGenerator) -> Dictionary:
|
||||
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):
|
||||
return {} # Corridor would pass through another room, skip this connection
|
||||
|
||||
# Create corridor (1 tile wide) - use floor tiles
|
||||
# Corridor is between the rooms, after the door
|
||||
# Create corridor (1 tile wide) - base floor (0,15)
|
||||
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:
|
||||
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_tile
|
||||
tile_grid[x][door_y + 1] = FLOOR_BASE
|
||||
|
||||
# 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
|
||||
@@ -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)
|
||||
if door_dx == 0 and door_dy == 1:
|
||||
grid[x][y] = 1 # Floor
|
||||
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
|
||||
tile_grid[x][y] = floor_tile
|
||||
tile_grid[x][y] = FLOOR_BASE
|
||||
|
||||
|
||||
# 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)
|
||||
if door_dx == 1 and door_dy == 1:
|
||||
grid[x][y] = 1 # Floor
|
||||
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
|
||||
tile_grid[x][y] = floor_tile
|
||||
tile_grid[x][y] = FLOOR_BASE
|
||||
|
||||
# CRITICAL: room1 = room the door is ON (left 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):
|
||||
return {} # Corridor would pass through another room, skip this connection
|
||||
|
||||
# Create corridor (1 tile wide) - use floor tiles
|
||||
# Corridor is between the rooms, after the door
|
||||
# Create corridor (1 tile wide) - base floor (0,15)
|
||||
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:
|
||||
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_tile
|
||||
tile_grid[door_x + 1][y] = FLOOR_BASE
|
||||
|
||||
# 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
|
||||
@@ -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)
|
||||
if door_dx == 1 and door_dy == 0:
|
||||
grid[x][y] = 1 # Floor
|
||||
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
|
||||
tile_grid[x][y] = floor_tile
|
||||
tile_grid[x][y] = FLOOR_BASE
|
||||
|
||||
# Also create door on TOP wall of bottom room (if there's a gap)
|
||||
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)
|
||||
if door_dx == 1 and door_dy == 1:
|
||||
grid[x][y] = 1 # Floor
|
||||
var floor_tile = FLOOR_TILES[rng.randi() % FLOOR_TILES.size()]
|
||||
tile_grid[x][y] = floor_tile
|
||||
tile_grid[x][y] = FLOOR_BASE
|
||||
|
||||
# CRITICAL: room1 = room the door is ON (top 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_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
|
||||
for x in range(hole_x, hole_x + 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
|
||||
if grid[x][y] == 1: # Floor
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
tile_grid[x][y] = INNER_WALL_BOTTOM_RIGHT
|
||||
tile_grid[x][y] = FALLOUT_CORNER_INNER_DOWN_RIGHT
|
||||
else:
|
||||
# Use default wall tile for interior of hole
|
||||
tile_grid[x][y] = WALL_TOP_UPPER
|
||||
tile_grid[x][y] = FALLOUT_CENTER
|
||||
|
||||
func _find_farthest_room(all_rooms: Array, start_index: int) -> int:
|
||||
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)
|
||||
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:
|
||||
# Place enemies in a room, scaled by level
|
||||
# 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)
|
||||
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
|
||||
# Small rooms (7-8 tiles): 0-1 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
|
||||
})
|
||||
|
||||
# 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
|
||||
|
||||
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 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
|
||||
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
|
||||
@@ -118,6 +127,71 @@ func _physics_process(delta):
|
||||
burn_sprite.set_meta("burn_animation_timer", anim_timer)
|
||||
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
|
||||
if attack_timer > 0:
|
||||
attack_timer -= delta
|
||||
@@ -137,6 +211,14 @@ func _physics_process(delta):
|
||||
if not is_knocked_back:
|
||||
_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_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():
|
||||
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()
|
||||
|
||||
# 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)
|
||||
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:
|
||||
# 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()
|
||||
entities_node.add_child(loot)
|
||||
loot.global_position = safe_spawn_pos
|
||||
loot.position_z = 1.0 if "position_z" in loot else 0.0
|
||||
loot.loot_type = loot_type
|
||||
# Set initial velocity before _ready() processes
|
||||
loot.velocity = initial_velocity
|
||||
|
||||
@@ -28,6 +28,9 @@ func _ready():
|
||||
|
||||
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)
|
||||
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
|
||||
damage = SNATCH_DAMAGE
|
||||
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_mask = 1 | 2 | 64
|
||||
|
||||
|
||||
@@ -173,6 +173,12 @@ const ANIMATIONS = {
|
||||
"loop": false,
|
||||
"nextAnimation": null
|
||||
},
|
||||
"FALL": {
|
||||
"frames": [21],
|
||||
"frameDurations": [500],
|
||||
"loop": true,
|
||||
"nextAnimation": null
|
||||
},
|
||||
"IDLE_HOLD": {
|
||||
"frames": [25],
|
||||
"frameDurations": [500],
|
||||
@@ -246,8 +252,8 @@ func _ready():
|
||||
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)
|
||||
else:
|
||||
# Deterministic based on position and type only
|
||||
seed_value = hash(str(spawn_position) + str(humanoid_type))
|
||||
# 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) + 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)
|
||||
appearance_rng.seed = seed_value
|
||||
|
||||
@@ -747,8 +753,8 @@ func _load_random_headgear():
|
||||
# Available headgears organized by category (using actual files found)
|
||||
var headgear_categories = {
|
||||
"": ["Headband.png"], # Direct files in Layer 6 - Headgears
|
||||
"Basic Assasin": [
|
||||
"AssasinBandanaBlack.png", "StalkerHoodBlack.png", "ThiefBandanaGreen.png"
|
||||
"Basic Assassin": [
|
||||
"AssassinBandanaBlack.png", "StalkerHoodBlack.png", "ThiefBandanaGreen.png"
|
||||
],
|
||||
"Basic Mage": [
|
||||
"EsperHatBlue.png", "HighMageHatCyan.png", "MageHatRed.png", "SorcererHoodCyan.png"
|
||||
@@ -1138,6 +1144,15 @@ func _ai_behavior(delta):
|
||||
else:
|
||||
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):
|
||||
velocity = Vector2.ZERO
|
||||
|
||||
@@ -1166,13 +1181,17 @@ func _idle_behavior(_delta):
|
||||
|
||||
func _wandering_behavior(_delta):
|
||||
# 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
|
||||
var random_angle = randf() * PI * 2
|
||||
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
|
||||
current_direction = _get_direction_from_vector(random_dir)
|
||||
_set_animation("RUN")
|
||||
current_direction = _get_direction_from_vector(random_dir)
|
||||
|
||||
# Check if player enters vision while patrolling
|
||||
if target_player and _is_player_in_vision(target_player):
|
||||
@@ -1315,6 +1334,12 @@ func _chasing_behavior(delta_arg):
|
||||
|
||||
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
|
||||
var speed_mult = 1.0
|
||||
if is_blocking:
|
||||
|
||||
@@ -42,16 +42,15 @@ func _ready():
|
||||
set_process(false)
|
||||
|
||||
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")
|
||||
if game_world:
|
||||
if "dungeon_tilemap_layer" in game_world:
|
||||
tilemap_layer = game_world.dungeon_tilemap_layer
|
||||
if "dungeon_tilemap_layer_decorated" in game_world:
|
||||
tilemap_layer = game_world.dungeon_tilemap_layer_decorated
|
||||
else:
|
||||
# Try to find it in Environment node
|
||||
var environment = game_world.get_node_or_null("Environment")
|
||||
if environment:
|
||||
tilemap_layer = environment.get_node_or_null("DungeonLayer0")
|
||||
tilemap_layer = environment.get_node_or_null("TileMapLayerDecoratedGround")
|
||||
|
||||
func _on_body_entered(body):
|
||||
# Object entered the switch
|
||||
|
||||
@@ -88,6 +88,8 @@ var fog_debug_lines: Array = []
|
||||
var dungeon_data: Dictionary = {}
|
||||
var dungeon_tilemap_layer: 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 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
|
||||
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
|
||||
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)
|
||||
)
|
||||
|
||||
# Sync broken interactable objects to the new client
|
||||
# Wait a bit after dungeon sync to ensure objects are spawned first
|
||||
# Sync broken interactable objects to the new client (immediate + delayed retry so joiner always gets it after objects exist)
|
||||
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
|
||||
# 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)
|
||||
entities_node.add_child(loot)
|
||||
loot.global_position = spawn_position
|
||||
loot.position_z = 1.0 if "position_z" in loot else 0.0
|
||||
loot.loot_type = loot_type
|
||||
# Ensure key loot has a deterministic name for any legacy RPCs
|
||||
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)
|
||||
entities_node.add_child(loot)
|
||||
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.item = item # Set the item instance
|
||||
# Set initial velocity before _ready() processes
|
||||
@@ -1900,6 +1929,67 @@ func _process(delta):
|
||||
peer_cleanup_timer = 0.0
|
||||
_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):
|
||||
# Check all client buffers and mark which ones should be skipped
|
||||
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()
|
||||
|
||||
# Only update facing if mouse is far enough from player
|
||||
if mouse_direction.length() > 0.1:
|
||||
# Only update facing if mouse is far enough from player (lock to down during fallout)
|
||||
if mouse_direction.length() > 0.1 and not (("fallout_state" in player) and player.fallout_state):
|
||||
player._update_facing_from_mouse(mouse_direction)
|
||||
else:
|
||||
# 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]
|
||||
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:
|
||||
var out: Array = []
|
||||
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)
|
||||
|
||||
# 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 = [
|
||||
Color(24 / 255.0, 59 / 255.0, 255 / 255.0), # 0 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(143 / 255.0, 71 / 255.0, 112 / 255.0), # 9 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(53 / 255.0, 46 / 255.0, 26 / 255.0), # 12 fallout
|
||||
Color(109 / 255.0, 33 / 255.0, 24 / 255.0), # 11 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.
|
||||
@@ -3209,7 +3561,7 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
||||
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.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)
|
||||
walls = [
|
||||
@@ -3219,7 +3571,7 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
||||
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.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)
|
||||
walls = [
|
||||
@@ -3229,7 +3581,7 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
||||
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.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
|
||||
walls = [
|
||||
@@ -3239,7 +3591,7 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
||||
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.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)
|
||||
walls = [
|
||||
@@ -3249,7 +3601,7 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
||||
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.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)
|
||||
walls = [
|
||||
@@ -3259,7 +3611,7 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
||||
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.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)
|
||||
walls = [
|
||||
@@ -3269,7 +3621,7 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
||||
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.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)
|
||||
walls = [
|
||||
@@ -3279,11 +3631,11 @@ func _get_dungeon_color_scheme(scheme_index: int) -> Array:
|
||||
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.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()
|
||||
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)
|
||||
var out: Array = []
|
||||
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
|
||||
if not shader_res:
|
||||
return
|
||||
for layer in [dungeon_tilemap_layer, dungeon_tilemap_layer_above]:
|
||||
if not layer or not is_instance_valid(layer):
|
||||
var env_node = get_node_or_null("Environment")
|
||||
if not env_node:
|
||||
return
|
||||
for child in env_node.get_children():
|
||||
if not child is TileMapLayer or not is_instance_valid(child):
|
||||
continue
|
||||
var layer = child as TileMapLayer
|
||||
var mat = layer.material
|
||||
if not mat or not (mat is ShaderMaterial):
|
||||
mat = ShaderMaterial.new()
|
||||
mat.shader = shader_res
|
||||
layer.material = mat
|
||||
var sm = mat as ShaderMaterial
|
||||
for i in range(13):
|
||||
for i in range(14):
|
||||
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("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
|
||||
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))
|
||||
else:
|
||||
sm.set_shader_parameter("tint", Color(1.0, 1.0, 1.0, 1.0))
|
||||
@@ -3339,6 +3692,8 @@ func _render_dungeon():
|
||||
if env_node:
|
||||
dungeon_tilemap_layer = env_node.get_node_or_null("DungeonLayer0")
|
||||
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:
|
||||
# Create new TileMapLayer
|
||||
@@ -3506,6 +3861,45 @@ func _render_dungeon():
|
||||
dungeon_tilemap_layer_above.set_cell(Vector2i(x, y), 0, BLACK_TILE)
|
||||
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(above_tiles_placed) + " tiles on above layer", 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)
|
||||
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
|
||||
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:
|
||||
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
|
||||
player_manager.spawn_points.append(Vector2(world_x, world_y))
|
||||
LogManager.log("GameWorld: Updated spawn points with " + str(free_tiles.size()) + " free floor tiles in room", LogManager.CATEGORY_DUNGEON)
|
||||
try_add_spawn.call(Vector2(world_x, world_y))
|
||||
LogManager.log("GameWorld: Updated spawn points with " + str(player_manager.spawn_points.size()) + " safe (non-fallout) floor tiles in room", LogManager.CATEGORY_DUNGEON)
|
||||
else:
|
||||
# Fallback: Create spawn points in a circle around the room center
|
||||
var room_center_x = (room.x + room.w / 2.0) * tile_size
|
||||
var room_center_y = (room.y + room.h / 2.0) * tile_size
|
||||
# Fallback: Create spawn points in a circle around the room center (exclude fallout)
|
||||
var num_spawn_points = 8
|
||||
for i in range(num_spawn_points):
|
||||
var angle = i * PI * 2 / num_spawn_points
|
||||
var offset = Vector2(cos(angle), sin(angle)) * 30 # 30 pixel radius
|
||||
player_manager.spawn_points.append(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)
|
||||
try_add_spawn.call(Vector2(room_center_x, room_center_y) + offset)
|
||||
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:
|
||||
# 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):
|
||||
# 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():
|
||||
return
|
||||
|
||||
_pack_dungeon_blob()
|
||||
if dungeon_blob_chunks.is_empty():
|
||||
print("GameWorld: HOST - ERROR: No dungeon blob chunks available!")
|
||||
return
|
||||
@@ -4235,6 +4654,8 @@ func _pack_dungeon_blob():
|
||||
var full_dungeon_data = {
|
||||
"tile_grid": dungeon_data.get("tile_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)),
|
||||
"rooms": dungeon_data.get("rooms", []),
|
||||
"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 # 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...")
|
||||
if not host_room.is_empty():
|
||||
print("GameWorld: Client - Using host's room for spawn points: ", host_room)
|
||||
LogManager.log("GameWorld: Using host's room for spawn points", LogManager.CATEGORY_DUNGEON)
|
||||
_update_spawn_points(host_room)
|
||||
# Move any existing players to spawn near host
|
||||
_move_players_to_host_room(host_room)
|
||||
var spawn_room = dungeon_data.start_room if dungeon_data.has("start_room") and not dungeon_data.start_room.is_empty() else host_room
|
||||
if not spawn_room.is_empty():
|
||||
print("GameWorld: Client - Using start room for joiner spawn points (avoid corridor/outside world)")
|
||||
LogManager.log("GameWorld: Using start room for joiner spawn points", LogManager.CATEGORY_DUNGEON)
|
||||
_update_spawn_points(spawn_room)
|
||||
_move_players_to_host_room(spawn_room)
|
||||
else:
|
||||
print("GameWorld: Client - Host room not available, using start room")
|
||||
LogManager.log("GameWorld: Host room not available, using start room", LogManager.CATEGORY_DUNGEON)
|
||||
print("GameWorld: Client - No start room, using default spawn points")
|
||||
LogManager.log("GameWorld: No start room for joiner, using default spawn points", LogManager.CATEGORY_DUNGEON)
|
||||
_update_spawn_points()
|
||||
# Move 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")
|
||||
LogManager.log("GameWorld: Client received " + str(defeated_enemies_list.size()) + " defeated enemy indices", LogManager.CATEGORY_NETWORK)
|
||||
|
||||
# Store broken objects BEFORE objects are spawned
|
||||
broken_objects.clear()
|
||||
# Store broken objects BEFORE objects are spawned (merge, don't replace - so _sync_broken_objects isn't overwritten if it arrived first)
|
||||
for obj_index in broken_objects_list:
|
||||
broken_objects[obj_index] = true
|
||||
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,
|
||||
"level": level,
|
||||
"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_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))
|
||||
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...")
|
||||
var host_room = dungeon_sync_metadata.host_room
|
||||
if not host_room.is_empty():
|
||||
print("GameWorld: Client - Using host's room for spawn points: ", host_room)
|
||||
_update_spawn_points(host_room)
|
||||
_move_players_to_host_room(host_room)
|
||||
var spawn_room = dungeon_data.start_room if dungeon_data.has("start_room") and not dungeon_data.start_room.is_empty() else host_room
|
||||
if not spawn_room.is_empty():
|
||||
print("GameWorld: Client - Using start room for joiner spawn points")
|
||||
_update_spawn_points(spawn_room)
|
||||
_move_players_to_host_room(spawn_room)
|
||||
else:
|
||||
print("GameWorld: Client - Host room not available, using start room")
|
||||
print("GameWorld: Client - No start room, using default spawn points")
|
||||
_update_spawn_points()
|
||||
|
||||
# Move all players to start room
|
||||
@@ -5026,15 +5447,16 @@ func _check_and_render_dungeon():
|
||||
_render_dungeon()
|
||||
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...")
|
||||
var host_room = dungeon_sync_metadata.host_room
|
||||
if not host_room.is_empty():
|
||||
print("GameWorld: Client - Using host's room for spawn points: ", host_room)
|
||||
_update_spawn_points(host_room)
|
||||
_move_players_to_host_room(host_room)
|
||||
var host_room_chunks = dungeon_sync_metadata.host_room
|
||||
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
|
||||
if not spawn_room_chunks.is_empty():
|
||||
print("GameWorld: Client - Using start room for joiner spawn points")
|
||||
_update_spawn_points(spawn_room_chunks)
|
||||
_move_players_to_host_room(spawn_room_chunks)
|
||||
else:
|
||||
print("GameWorld: Client - Host room not available, using start room")
|
||||
print("GameWorld: Client - No start room, using default spawn points")
|
||||
_update_spawn_points()
|
||||
|
||||
# Move all players to start room
|
||||
@@ -5272,15 +5694,20 @@ func _spawn_enemies():
|
||||
if "damage" in enemy_data:
|
||||
enemy.damage = enemy_data.damage
|
||||
|
||||
# If it's a humanoid enemy, set the humanoid_type
|
||||
if enemy_type.ends_with("enemy_humanoid.tscn") and "humanoid_type" in enemy_data:
|
||||
# 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"):
|
||||
if "humanoid_type" in enemy_data:
|
||||
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)
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
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)
|
||||
switch.global_position = i_position
|
||||
|
||||
# Update tilemap to show switch tile (initial inactive state)
|
||||
if dungeon_tilemap_layer:
|
||||
# Draw switch on TileMapLayerDecoratedGround (terrain 1 = walk, 2 = pillar set in tileset)
|
||||
if dungeon_tilemap_layer_decorated:
|
||||
var initial_tile: Vector2i
|
||||
if switch_type == "pillar":
|
||||
initial_tile = Vector2i(16, 9) # Pillar switch inactive
|
||||
initial_tile = Vector2i(16, 9) # Pillar switch inactive (terrain 2)
|
||||
else:
|
||||
initial_tile = Vector2i(11, 9) # Walk-on switch inactive
|
||||
dungeon_tilemap_layer.set_cell(Vector2i(tile_x, tile_y), 0, initial_tile)
|
||||
initial_tile = Vector2i(11, 9) # Walk-on switch inactive (terrain 1)
|
||||
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_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_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():
|
||||
# Make sure it's on the interactable layer
|
||||
collision_layer = 2 # Layer 2 for objects
|
||||
@@ -76,6 +81,22 @@ func _physics_process(delta):
|
||||
return
|
||||
|
||||
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
|
||||
if is_airborne:
|
||||
# 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)
|
||||
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.item = item # Set the item instance
|
||||
# Set initial velocity before _ready() processes
|
||||
|
||||
@@ -13,8 +13,9 @@ enum LootType {
|
||||
|
||||
@export var loot_type: LootType = LootType.COIN
|
||||
|
||||
# Z-axis physics (like boxes and players)
|
||||
var position_z: float = 0.0
|
||||
# Z-axis physics (like boxes and players) - start at 1 to avoid instantly falling into fallout
|
||||
const SPAWN_POSITION_Z: float = 1.0
|
||||
var position_z: float = 1.0
|
||||
var velocity_z: float = 0.0
|
||||
var acceleration_z: float = 0.0
|
||||
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
|
||||
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():
|
||||
add_to_group("loot")
|
||||
# Always start slightly above ground to prevent instantly falling into fallout
|
||||
position_z = SPAWN_POSITION_Z
|
||||
|
||||
# Setup shadow
|
||||
if shadow:
|
||||
@@ -219,6 +227,36 @@ func _physics_process(delta):
|
||||
|
||||
# Server (authority): Run physics normally
|
||||
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
|
||||
if bounce_timer > 0.0:
|
||||
bounce_timer -= delta
|
||||
@@ -410,6 +448,8 @@ func _animate_coin(delta):
|
||||
sprite.frame = frame
|
||||
|
||||
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:
|
||||
# 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"):
|
||||
@@ -430,6 +470,8 @@ func _on_pickup_area_body_entered(body):
|
||||
_pickup(body)
|
||||
|
||||
func _pickup(player: Node):
|
||||
if falling_into_fallout:
|
||||
return
|
||||
# Prevent multiple pickups
|
||||
if collected:
|
||||
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")
|
||||
return
|
||||
|
||||
if falling_into_fallout:
|
||||
return
|
||||
|
||||
# Set mutex and mark as collected IMMEDIATELY to prevent any race conditions
|
||||
processing_pickup = true
|
||||
collected = true
|
||||
|
||||
@@ -59,6 +59,7 @@ var controls_disabled: bool = false # True when player has reached exit and cont
|
||||
|
||||
# Being held state
|
||||
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 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
|
||||
@@ -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 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
|
||||
# @onready var sprite = $Sprite2D # REMOVED: Now using layered sprites
|
||||
@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 collision_shape = $CollisionShape2D
|
||||
@onready var grab_area = $GrabArea
|
||||
@onready var quicksand_area = $QuicksandArea
|
||||
@onready var interaction_indicator = $InteractionIndicator
|
||||
|
||||
# Audio
|
||||
@@ -1584,9 +1607,10 @@ func _get_collision_extent(node: Node) -> float:
|
||||
return 8.0
|
||||
|
||||
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
|
||||
if time_since_last_frame >= ANIMATIONS[current_animation]["frameDurations"][current_frame] / 1000.0:
|
||||
if time_since_last_frame >= frame_duration_sec:
|
||||
current_frame += 1
|
||||
if current_frame >= len(ANIMATIONS[current_animation]["frames"]):
|
||||
current_frame -= 1 # Prevent out of bounds
|
||||
@@ -1972,7 +1996,26 @@ func _physics_process(delta):
|
||||
was_reviving_last_frame = false
|
||||
_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
|
||||
if is_dead:
|
||||
if is_knocked_back:
|
||||
@@ -1983,6 +2026,42 @@ func _physics_process(delta):
|
||||
else:
|
||||
velocity = velocity.lerp(Vector2.ZERO, delta * 8.0)
|
||||
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)
|
||||
if is_knocked_back:
|
||||
knockback_time += delta
|
||||
@@ -1994,6 +2073,12 @@ func _physics_process(delta):
|
||||
if grabbed_by_enemy_hand:
|
||||
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)
|
||||
if movement_lock_timer > 0.0:
|
||||
movement_lock_timer -= delta
|
||||
@@ -2084,18 +2169,18 @@ func _physics_process(delta):
|
||||
burn_damage_timer = 0.0
|
||||
_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)
|
||||
# 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 skip_input = controls_disabled or spawn_landing
|
||||
if 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 or fallout_state or (fallout_respawn_stun_timer > 0.0):
|
||||
if not is_knocked_back and not entrance_walk_out:
|
||||
# Immediately stop movement when controls are disabled (e.g., inventory opened)
|
||||
# Exception: entrance walk-out - velocity is driven by game_world for cut-scene
|
||||
velocity = Vector2.ZERO
|
||||
# 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:
|
||||
_set_animation("IDLE_HOLD")
|
||||
elif is_pushing:
|
||||
@@ -2152,6 +2237,24 @@ func _physics_process(delta):
|
||||
struggle_time = 0.0 # Reset struggle timer
|
||||
struggle_direction = Vector2.ZERO
|
||||
_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_interactions()
|
||||
else:
|
||||
@@ -3301,10 +3404,9 @@ func _try_grab():
|
||||
closest_body.set_collision_mask_value(2, false)
|
||||
closest_body.set_collision_mask_value(7, true) # Keep wall collision
|
||||
elif _is_player(closest_body):
|
||||
# Players: remove from layer fully when lifted – no collision with anything
|
||||
closest_body.set_collision_layer_value(1, false)
|
||||
closest_body.set_collision_mask_value(1, false)
|
||||
closest_body.set_collision_mask_value(7, false)
|
||||
# Players: no collision layer at all while held
|
||||
closest_body.collision_layer = 0
|
||||
closest_body.collision_mask = 0
|
||||
|
||||
# When grabbing, immediately try to lift if possible
|
||||
_set_animation("IDLE")
|
||||
@@ -3363,10 +3465,14 @@ func _lift_object():
|
||||
if "is_frozen" in held_object:
|
||||
held_object.is_frozen = true
|
||||
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"):
|
||||
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"):
|
||||
held_object.on_lifted(self)
|
||||
|
||||
@@ -3453,11 +3559,12 @@ func reset_grab_state():
|
||||
if "held_by_player" in held_object:
|
||||
held_object.held_by_player = null
|
||||
elif _is_player(held_object):
|
||||
held_object.set_collision_layer_value(1, true)
|
||||
held_object.set_collision_mask_value(1, true)
|
||||
held_object.set_collision_mask_value(7, true)
|
||||
held_object.collision_layer = 1
|
||||
held_object.collision_mask = 1 | 2 | 64 # players, objects, walls
|
||||
if held_object.has_method("set_being_held"):
|
||||
held_object.set_being_held(false)
|
||||
if "position_z" in held_object:
|
||||
held_object.position_z = 0.0
|
||||
|
||||
# Stop drag sound if playing
|
||||
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(7, true) # Re-enable wall collision!
|
||||
elif _is_player(released_obj):
|
||||
# Players: back on layer 1
|
||||
released_obj.set_collision_layer_value(1, true)
|
||||
released_obj.set_collision_mask_value(1, true)
|
||||
released_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||
# Players: restore collision layer and mask (layer 1, mask 1|2|64 so we collide with players, objects, walls)
|
||||
released_obj.collision_layer = 1
|
||||
released_obj.collision_mask = 1 | 2 | 64
|
||||
|
||||
if released_obj is CharacterBody2D and released_obj.has_method("set_being_held"):
|
||||
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!
|
||||
# Do this AFTER calling on_released in case it tries to change position
|
||||
if released_obj.has_method("on_released"):
|
||||
@@ -3640,10 +3749,11 @@ func _throw_object():
|
||||
if thrown_obj.has_method("set_being_held"):
|
||||
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):
|
||||
thrown_obj.set_collision_layer_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)
|
||||
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
|
||||
@@ -3785,9 +3895,8 @@ func _force_throw_held_object(direction: Vector2):
|
||||
|
||||
# Re-add to layer DIRECTLY when thrown (no delay)
|
||||
if thrown_obj and is_instance_valid(thrown_obj):
|
||||
thrown_obj.set_collision_layer_value(1, true)
|
||||
thrown_obj.set_collision_mask_value(1, true)
|
||||
thrown_obj.set_collision_mask_value(7, true)
|
||||
thrown_obj.collision_layer = 1
|
||||
thrown_obj.collision_mask = 1 | 2 | 64 # players, objects, walls
|
||||
elif thrown_obj and thrown_obj.has_method("can_be_grabbed") and thrown_obj.can_be_grabbed():
|
||||
# Other grabbable object - handle like box
|
||||
thrown_obj.global_position = throw_start_pos
|
||||
@@ -3917,10 +4026,9 @@ func _place_down_object():
|
||||
if "velocity_z" in placed_obj:
|
||||
placed_obj.velocity_z = 0.0
|
||||
elif _is_player(placed_obj):
|
||||
# Player: back on layer 1
|
||||
placed_obj.set_collision_layer_value(1, true)
|
||||
placed_obj.set_collision_mask_value(1, true)
|
||||
placed_obj.set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||
# Player: restore collision layer and mask (1|2|64 so we collide with players, objects, walls)
|
||||
placed_obj.collision_layer = 1
|
||||
placed_obj.collision_mask = 1 | 2 | 64
|
||||
placed_obj.global_position = place_pos
|
||||
placed_obj.velocity = Vector2.ZERO
|
||||
if placed_obj.has_method("set_being_held"):
|
||||
@@ -5223,7 +5331,7 @@ func _update_lifted_object():
|
||||
held_object = null
|
||||
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
|
||||
|
||||
# Instant follow for local player, smooth for network sync
|
||||
@@ -5232,6 +5340,10 @@ func _update_lifted_object():
|
||||
else:
|
||||
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
|
||||
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)
|
||||
@@ -5301,6 +5413,12 @@ func _update_pushed_object():
|
||||
var results = space_state.intersect_point(query)
|
||||
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
|
||||
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!")
|
||||
|
||||
# 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):
|
||||
obj.set_collision_layer_value(1, true)
|
||||
obj.set_collision_mask_value(1, true)
|
||||
obj.set_collision_mask_value(2, true)
|
||||
obj.set_collision_mask_value(7, true)
|
||||
|
||||
@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(2, false)
|
||||
elif _is_player(obj):
|
||||
obj.set_collision_layer_value(1, false)
|
||||
obj.set_collision_mask_value(1, false)
|
||||
obj.set_collision_mask_value(7, false)
|
||||
obj.collision_layer = 0
|
||||
obj.collision_mask = 0
|
||||
|
||||
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:
|
||||
obj.throw_velocity = Vector2.ZERO
|
||||
elif _is_player(obj):
|
||||
obj.set_collision_layer_value(1, false)
|
||||
obj.set_collision_mask_value(1, false)
|
||||
obj.set_collision_mask_value(7, false)
|
||||
obj.collision_layer = 0
|
||||
obj.collision_mask = 0
|
||||
if obj.has_method("set_being_held"):
|
||||
obj.set_being_held(true)
|
||||
else:
|
||||
@@ -5887,9 +6004,8 @@ func _sync_grab(obj_name: String, is_lift: bool, axis: Vector2 = Vector2.ZERO):
|
||||
if "throw_velocity" in obj:
|
||||
obj.throw_velocity = Vector2.ZERO
|
||||
elif _is_player(obj):
|
||||
obj.set_collision_layer_value(1, false)
|
||||
obj.set_collision_mask_value(1, false)
|
||||
obj.set_collision_mask_value(7, false)
|
||||
obj.collision_layer = 0
|
||||
obj.collision_mask = 0
|
||||
if obj.has_method("set_being_held"):
|
||||
obj.set_being_held(true)
|
||||
|
||||
@@ -5940,6 +6056,7 @@ func _sync_release(obj_name: String):
|
||||
elif _is_player(obj):
|
||||
obj.set_collision_layer_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!
|
||||
if obj.has_method("set_being_held"):
|
||||
obj.set_being_held(false)
|
||||
@@ -6000,6 +6117,7 @@ func _sync_place_down(obj_name: String, place_pos: Vector2):
|
||||
elif _is_player(obj):
|
||||
obj.set_collision_layer_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.velocity = Vector2.ZERO
|
||||
if obj.has_method("set_being_held"):
|
||||
@@ -6126,6 +6244,7 @@ func _break_free_from_holder():
|
||||
struggle_time = 0.0
|
||||
struggle_direction = Vector2.ZERO
|
||||
being_held_by = null
|
||||
is_being_held = false
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_break_free(holder_name: String, direction: Vector2):
|
||||
@@ -6182,6 +6301,7 @@ func _force_place_down(direction: Vector2):
|
||||
elif _is_player(placed_obj):
|
||||
placed_obj.set_collision_layer_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.velocity = Vector2.ZERO
|
||||
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)
|
||||
|
||||
func set_being_held(held: bool):
|
||||
# When being held by another player, disable movement
|
||||
# But keep physics_process running for network sync
|
||||
# When being held by another player, disable movement and collision; use HELD_POSITION_Z so we're "above" ground (immune to fallout)
|
||||
is_being_held = held
|
||||
if held:
|
||||
# Just prevent input handling, don't disable physics
|
||||
velocity = Vector2.ZERO
|
||||
is_airborne = false
|
||||
position_z = 0.0
|
||||
position_z = HELD_POSITION_Z
|
||||
velocity_z = 0.0
|
||||
collision_layer = 0
|
||||
collision_mask = 0
|
||||
else:
|
||||
# Released - reset struggle state
|
||||
struggle_time = 0.0
|
||||
struggle_direction = Vector2.ZERO
|
||||
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")
|
||||
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():
|
||||
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
|
||||
if is_dead:
|
||||
return
|
||||
# Invulnerable during fallout sink (can't take damage from anything while falling)
|
||||
if fallout_state:
|
||||
return
|
||||
|
||||
# Cancel bow charging when taking damage
|
||||
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
|
||||
damage_direction_lock_timer = damage_direction_lock_duration
|
||||
|
||||
# Only apply knockback if not burn damage
|
||||
if not is_burn_damage:
|
||||
# Only apply knockback if not burn damage and not suppressed (e.g. fallout respawn)
|
||||
if not is_burn_damage and not no_knockback:
|
||||
# Calculate direction FROM attacker TO victim
|
||||
var direction_from_attacker = (global_position - attacker_position).normalized()
|
||||
|
||||
@@ -6476,6 +6602,7 @@ func _die():
|
||||
elif _is_player(released_obj):
|
||||
released_obj.set_collision_layer_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!
|
||||
if released_obj.has_method("set_being_held"):
|
||||
released_obj.set_being_held(false)
|
||||
@@ -6539,10 +6666,9 @@ func _die():
|
||||
other_player.grab_offset = Vector2.ZERO
|
||||
other_player.push_axis = Vector2.ZERO
|
||||
|
||||
# Re-enable our collision
|
||||
set_collision_layer_value(1, true)
|
||||
set_collision_mask_value(1, true)
|
||||
set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||
# Re-enable our collision (layer 1, mask 1|2|64)
|
||||
collision_layer = 1
|
||||
collision_mask = 1 | 2 | 64
|
||||
|
||||
# THEN sync to other clients
|
||||
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")
|
||||
|
||||
being_held_by = null
|
||||
is_being_held = false
|
||||
|
||||
# Replicas: no wait loop; we get _sync_respawn from 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"):
|
||||
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():
|
||||
print(name, " respawning!")
|
||||
was_revived = false
|
||||
@@ -6684,6 +6829,7 @@ func _respawn():
|
||||
# Re-enable collision in case it was disabled while being carried
|
||||
set_collision_layer_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!
|
||||
|
||||
# Reset health and state
|
||||
@@ -6815,9 +6961,10 @@ func _force_holder_to_drop_local(holder_name: String):
|
||||
holder.grab_offset = 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_mask_value(1, true)
|
||||
set_collision_mask_value(2, true)
|
||||
set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||
else:
|
||||
print(" ✗ held_object doesn't match self")
|
||||
@@ -6936,14 +7083,43 @@ func _sync_death():
|
||||
_apply_death_visual()
|
||||
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")
|
||||
func _sync_respawn(spawn_pos: Vector2):
|
||||
if not is_multiplayer_authority():
|
||||
# being_held_by already cleared via RPC in _die()
|
||||
# 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_mask_value(1, true)
|
||||
set_collision_mask_value(2, true)
|
||||
set_collision_mask_value(7, true) # Re-enable wall collision!
|
||||
|
||||
# Reset health and state
|
||||
|
||||
Reference in New Issue
Block a user