added blocking doors to paths.
This commit is contained in:
BIN
src/assets/audio/sfx/environment/keydoor/door_closes.mp3
Normal file
BIN
src/assets/audio/sfx/environment/keydoor/door_closes.mp3
Normal file
Binary file not shown.
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="mp3"
|
||||||
|
type="AudioStreamMP3"
|
||||||
|
uid="uid://c6bp156a5ggdf"
|
||||||
|
path="res://.godot/imported/door_closes.mp3-b5c10c6d847d1c83f09bcec9f8b04f75.mp3str"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/audio/sfx/environment/keydoor/door_closes.mp3"
|
||||||
|
dest_files=["res://.godot/imported/door_closes.mp3-b5c10c6d847d1c83f09bcec9f8b04f75.mp3str"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0
|
||||||
|
bpm=0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 44 KiB |
114
src/scenes/TeleporterIntoClosedRoom.tscn
Normal file
114
src/scenes/TeleporterIntoClosedRoom.tscn
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
[gd_scene format=3 uid="uid://d24xrw86pfg1s"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://b4wejvn0dfrji" path="res://scripts/teleporter_into_closed_room.gd" id="1_g3s7f"]
|
||||||
|
|
||||||
|
[sub_resource type="Gradient" id="Gradient_skeae"]
|
||||||
|
offsets = PackedFloat32Array(0, 0.9883721)
|
||||||
|
colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 0)
|
||||||
|
|
||||||
|
[sub_resource type="GradientTexture2D" id="GradientTexture2D_g3s7f"]
|
||||||
|
gradient = SubResource("Gradient_skeae")
|
||||||
|
width = 32
|
||||||
|
height = 32
|
||||||
|
fill = 1
|
||||||
|
fill_from = Vector2(0.4957265, 0.4871795)
|
||||||
|
fill_to = Vector2(0.525641, 0.42307693)
|
||||||
|
|
||||||
|
[sub_resource type="Curve" id="Curve_7twcj"]
|
||||||
|
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(0.74757284, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0]
|
||||||
|
point_count = 3
|
||||||
|
|
||||||
|
[sub_resource type="CurveTexture" id="CurveTexture_sgnaw"]
|
||||||
|
curve = SubResource("Curve_7twcj")
|
||||||
|
|
||||||
|
[sub_resource type="Gradient" id="Gradient_iemw3"]
|
||||||
|
colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 1)
|
||||||
|
|
||||||
|
[sub_resource type="GradientTexture1D" id="GradientTexture1D_paieq"]
|
||||||
|
gradient = SubResource("Gradient_iemw3")
|
||||||
|
|
||||||
|
[sub_resource type="Gradient" id="Gradient_1qst8"]
|
||||||
|
colors = PackedColorArray(1, 1, 1, 1, 1, 1, 1, 1)
|
||||||
|
|
||||||
|
[sub_resource type="GradientTexture1D" id="GradientTexture1D_cqcet"]
|
||||||
|
gradient = SubResource("Gradient_1qst8")
|
||||||
|
|
||||||
|
[sub_resource type="Curve" id="Curve_7kx0f"]
|
||||||
|
_limits = [-1.0, 1.0, 0.0, 1.0]
|
||||||
|
_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1e-05, -0.11255813), 0.0, 0.0, 0, 0, Vector2(0.055825245, -0.43906975), 0.0, 0.0, 0, 0, Vector2(0.1553398, 0.58883727), 0.0, 0.0, 0, 0, Vector2(0.30339807, -0.2697674), 0.0, 0.0, 0, 0, Vector2(0.44902915, 0.73395354), 0.0, 0.0, 0, 0, Vector2(0.657767, -0.13674414), 0.0, 0.0, 0, 0, Vector2(0.8009709, -0.4511627), 0.0, 0.0, 0, 0]
|
||||||
|
point_count = 8
|
||||||
|
|
||||||
|
[sub_resource type="CurveTexture" id="CurveTexture_g8xsh"]
|
||||||
|
curve = SubResource("Curve_7kx0f")
|
||||||
|
|
||||||
|
[sub_resource type="Curve" id="Curve_d4f27"]
|
||||||
|
_data = [Vector2(0, 0.4134884), 0.0, 0.0, 0, 0, Vector2(0.2354369, 0.49813956), 0.0, 0.0, 0, 0, Vector2(0.4830097, 0.18976748), 0.0, 0.0, 0, 0, Vector2(0.5873787, 0.8186047), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0]
|
||||||
|
point_count = 5
|
||||||
|
|
||||||
|
[sub_resource type="CurveTexture" id="CurveTexture_nhcxx"]
|
||||||
|
curve = SubResource("Curve_d4f27")
|
||||||
|
|
||||||
|
[sub_resource type="Curve" id="Curve_l8b8y"]
|
||||||
|
_limits = [0.0, 1000.0, 0.0, 1000.0]
|
||||||
|
_data = [Vector2(0, 413.48828), 0.0, 0.0, 0, 0, Vector2(67.96117, 467.90692), 0.0, 0.0, 0, 0, Vector2(189.3204, 213.95343), 0.0, 0.0, 0, 0, Vector2(269.41748, 50.69763), 0.0, 0.0, 0, 0, Vector2(456.31067, 280.46503), 0.0, 0.0, 0, 0]
|
||||||
|
point_count = 5
|
||||||
|
|
||||||
|
[sub_resource type="CurveTexture" id="CurveTexture_hd4yi"]
|
||||||
|
curve = SubResource("Curve_l8b8y")
|
||||||
|
|
||||||
|
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_n1yim"]
|
||||||
|
particle_flag_disable_z = true
|
||||||
|
direction = Vector3(0, -1, 0)
|
||||||
|
spread = 6.844
|
||||||
|
initial_velocity_min = 50.76
|
||||||
|
initial_velocity_max = 65.99
|
||||||
|
angular_velocity_min = -36.550014
|
||||||
|
angular_velocity_max = 21.929983
|
||||||
|
orbit_velocity_min = -0.77199996
|
||||||
|
orbit_velocity_max = 0.5480001
|
||||||
|
orbit_velocity_curve = SubResource("CurveTexture_g8xsh")
|
||||||
|
radial_velocity_min = -2.2351742e-05
|
||||||
|
radial_velocity_max = 101.51997
|
||||||
|
radial_velocity_curve = SubResource("CurveTexture_nhcxx")
|
||||||
|
velocity_limit_curve = SubResource("CurveTexture_hd4yi")
|
||||||
|
gravity = Vector3(0, 0, 0)
|
||||||
|
color = Color(0.61982733, 0.94004476, 1, 1)
|
||||||
|
color_ramp = SubResource("GradientTexture1D_cqcet")
|
||||||
|
color_initial_ramp = SubResource("GradientTexture1D_paieq")
|
||||||
|
alpha_curve = SubResource("CurveTexture_sgnaw")
|
||||||
|
hue_variation_min = -0.06000002
|
||||||
|
hue_variation_max = 0.049999975
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_pp12y"]
|
||||||
|
size = Vector2(16, 16)
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_7twcj"]
|
||||||
|
size = Vector2(32, 32)
|
||||||
|
|
||||||
|
[node name="TeleporterIntoClosedRoom" type="Node2D" unique_id=1871154484]
|
||||||
|
script = ExtResource("1_g3s7f")
|
||||||
|
|
||||||
|
[node name="GPUParticles2D" type="GPUParticles2D" parent="." unique_id=1725884303]
|
||||||
|
emitting = false
|
||||||
|
amount = 32
|
||||||
|
texture = SubResource("GradientTexture2D_g3s7f")
|
||||||
|
lifetime = 0.54
|
||||||
|
randomness = 1.0
|
||||||
|
process_material = SubResource("ParticleProcessMaterial_n1yim")
|
||||||
|
|
||||||
|
[node name="AreaWhichTeleportsPlayerIntoRoom" type="Area2D" parent="." unique_id=47060921]
|
||||||
|
collision_mask = 0
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="AreaWhichTeleportsPlayerIntoRoom" unique_id=1803123867]
|
||||||
|
shape = SubResource("RectangleShape2D_pp12y")
|
||||||
|
|
||||||
|
[node name="AreaToStartEmit" type="Area2D" parent="." unique_id=1219098269]
|
||||||
|
collision_mask = 0
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="AreaToStartEmit" unique_id=700191159]
|
||||||
|
shape = SubResource("RectangleShape2D_7twcj")
|
||||||
|
debug_color = Color(0.6530463, 0.21585448, 0.70196074, 0.41960785)
|
||||||
|
|
||||||
|
[connection signal="body_entered" from="AreaWhichTeleportsPlayerIntoRoom" to="." method="_on_area_which_teleports_player_into_room_body_entered"]
|
||||||
|
[connection signal="body_entered" from="AreaToStartEmit" to="." method="_on_area_to_start_emit_body_entered"]
|
||||||
|
[connection signal="body_exited" from="AreaToStartEmit" to="." method="_on_area_to_start_emit_body_exited"]
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
[ext_resource type="Texture2D" uid="uid://cckiqfs0kwuuh" path="res://assets/gfx/door_barred.png" id="1_hpvv5"]
|
[ext_resource type="Texture2D" uid="uid://cckiqfs0kwuuh" path="res://assets/gfx/door_barred.png" id="1_hpvv5"]
|
||||||
[ext_resource type="Script" uid="uid://do4062ppepheo" path="res://scripts/door.gd" id="1_uvdjg"]
|
[ext_resource type="Script" uid="uid://do4062ppepheo" path="res://scripts/door.gd" id="1_uvdjg"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://d24xrw86pfg1s" path="res://scenes/TeleporterIntoClosedRoom.tscn" id="2_q5w8r"]
|
||||||
[ext_resource type="AudioStream" uid="uid://dfolu80c534j4" path="res://assets/audio/sfx/environment/keydoor/unlock.mp3" id="3_la1wf"]
|
[ext_resource type="AudioStream" uid="uid://dfolu80c534j4" path="res://assets/audio/sfx/environment/keydoor/unlock.mp3" id="3_la1wf"]
|
||||||
[ext_resource type="AudioStream" uid="uid://2w73l4k3704x" path="res://assets/audio/sfx/environment/pot/pot_drag1.mp3" id="4_18pbm"]
|
[ext_resource type="AudioStream" uid="uid://2w73l4k3704x" path="res://assets/audio/sfx/environment/pot/pot_drag1.mp3" id="4_18pbm"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://c6bp156a5ggdf" path="res://assets/audio/sfx/environment/keydoor/door_closes.mp3" id="5_18pbm"]
|
||||||
|
|
||||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_uvdjg"]
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_uvdjg"]
|
||||||
size = Vector2(26, 14)
|
size = Vector2(26, 14)
|
||||||
@@ -15,6 +17,9 @@ size = Vector2(22, 18)
|
|||||||
collision_layer = 64
|
collision_layer = 64
|
||||||
script = ExtResource("1_uvdjg")
|
script = ExtResource("1_uvdjg")
|
||||||
|
|
||||||
|
[node name="TeleporterIntoClosedRoom" parent="." unique_id=1871154484 instance=ExtResource("2_q5w8r")]
|
||||||
|
position = Vector2(0, -16)
|
||||||
|
|
||||||
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1520856168]
|
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1520856168]
|
||||||
texture = ExtResource("1_hpvv5")
|
texture = ExtResource("1_hpvv5")
|
||||||
|
|
||||||
@@ -35,3 +40,8 @@ stream = ExtResource("4_18pbm")
|
|||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="KeyInteractionArea" unique_id=1640987231]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="KeyInteractionArea" unique_id=1640987231]
|
||||||
shape = SubResource("RectangleShape2D_la1wf")
|
shape = SubResource("RectangleShape2D_la1wf")
|
||||||
debug_color = Color(0.70196074, 0.67558956, 0.17869899, 0.41960785)
|
debug_color = Color(0.70196074, 0.67558956, 0.17869899, 0.41960785)
|
||||||
|
|
||||||
|
[node name="SfxDoorCloses" type="AudioStreamPlayer2D" parent="." unique_id=1074871158]
|
||||||
|
stream = ExtResource("5_18pbm")
|
||||||
|
max_distance = 1333.0
|
||||||
|
attenuation = 5.8563395
|
||||||
|
|||||||
43
src/scenes/door.tscn3989767106.tmp
Normal file
43
src/scenes/door.tscn3989767106.tmp
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
[gd_scene format=3 uid="uid://02opigrv0qff"]
|
||||||
|
|
||||||
|
[ext_resource type="Texture2D" uid="uid://cckiqfs0kwuuh" path="res://assets/gfx/door_barred.png" id="1_hpvv5"]
|
||||||
|
[ext_resource type="Script" uid="uid://do4062ppepheo" path="res://scripts/door.gd" id="1_uvdjg"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://dfolu80c534j4" path="res://assets/audio/sfx/environment/keydoor/unlock.mp3" id="3_la1wf"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://2w73l4k3704x" path="res://assets/audio/sfx/environment/pot/pot_drag1.mp3" id="4_18pbm"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://c6bp156a5ggdf" path="res://assets/audio/sfx/environment/keydoor/door_closes.mp3" id="5_18pbm"]
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_uvdjg"]
|
||||||
|
size = Vector2(26, 14)
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_la1wf"]
|
||||||
|
size = Vector2(22, 18)
|
||||||
|
|
||||||
|
[node name="Door" type="StaticBody2D" unique_id=371155975]
|
||||||
|
collision_layer = 64
|
||||||
|
script = ExtResource("1_uvdjg")
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1520856168]
|
||||||
|
texture = ExtResource("1_hpvv5")
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=1691515105]
|
||||||
|
shape = SubResource("RectangleShape2D_uvdjg")
|
||||||
|
|
||||||
|
[node name="SfxOpenKeyDoor" type="AudioStreamPlayer2D" parent="." unique_id=47303726]
|
||||||
|
stream = ExtResource("3_la1wf")
|
||||||
|
|
||||||
|
[node name="SfxOpenStoneDoor" type="AudioStreamPlayer2D" parent="." unique_id=885417421]
|
||||||
|
stream = ExtResource("4_18pbm")
|
||||||
|
|
||||||
|
[node name="SfxOpenGateDoor" type="AudioStreamPlayer2D" parent="." unique_id=442358170]
|
||||||
|
stream = ExtResource("4_18pbm")
|
||||||
|
|
||||||
|
[node name="KeyInteractionArea" type="Area2D" parent="." unique_id=982067740]
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="KeyInteractionArea" unique_id=1640987231]
|
||||||
|
shape = SubResource("RectangleShape2D_la1wf")
|
||||||
|
debug_color = Color(0.70196074, 0.67558956, 0.17869899, 0.41960785)
|
||||||
|
|
||||||
|
[node name="SfxDoorCloses" type="AudioStreamPlayer2D" parent="." unique_id=1074871158]
|
||||||
|
stream = ExtResource("5_18pbm")
|
||||||
|
max_distance = 1333.0
|
||||||
|
attenuation = 5.8563395
|
||||||
@@ -1,14 +1,24 @@
|
|||||||
[gd_scene format=3 uid="uid://floating_text"]
|
[gd_scene format=3 uid="uid://floating_text"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scripts/floating_text.gd" id="1"]
|
[ext_resource type="Script" path="res://scripts/floating_text.gd" id="1"]
|
||||||
|
[ext_resource type="FontFile" uid="uid://cbmcfue0ek0tk" path="res://assets/fonts/dmg_numbers.png" id="2_dmg_font"]
|
||||||
|
|
||||||
|
[sub_resource type="Theme" id="Theme_floating_text"]
|
||||||
|
default_font = ExtResource("2_dmg_font")
|
||||||
|
default_font_size = 12
|
||||||
|
|
||||||
[node name="FloatingText" type="Node2D"]
|
[node name="FloatingText" type="Node2D"]
|
||||||
script = ExtResource("1")
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="ItemSprite" type="Sprite2D" parent="."]
|
||||||
|
visible = false
|
||||||
|
offset = Vector2(0, -20)
|
||||||
|
scale = Vector2(1, 1)
|
||||||
|
|
||||||
[node name="Label" type="Label" parent="."]
|
[node name="Label" type="Label" parent="."]
|
||||||
offset_right = 64.0
|
offset_right = 64.0
|
||||||
offset_bottom = 24.0
|
offset_bottom = 24.0
|
||||||
theme_override_font_sizes/font_size = 18
|
theme = SubResource("Theme_floating_text")
|
||||||
text = "+1 coin"
|
text = "+1 coin"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
vertical_alignment = 1
|
vertical_alignment = 1
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
[ext_resource type="Script" uid="uid://bax7e73v836nx" path="res://scripts/player_manager.gd" id="1"]
|
[ext_resource type="Script" uid="uid://bax7e73v836nx" path="res://scripts/player_manager.gd" id="1"]
|
||||||
[ext_resource type="PackedScene" uid="uid://cxfvw8y7jqn2p" path="res://scenes/player.tscn" id="2"]
|
[ext_resource type="PackedScene" uid="uid://cxfvw8y7jqn2p" path="res://scenes/player.tscn" id="2"]
|
||||||
[ext_resource type="PackedScene" uid="uid://b7qx8y2jqn3r" path="res://scenes/interactable_object.tscn" id="3"]
|
|
||||||
[ext_resource type="Script" uid="uid://db58xcyo4cjk" path="res://scripts/game_world.gd" id="4"]
|
[ext_resource type="Script" uid="uid://db58xcyo4cjk" path="res://scripts/game_world.gd" id="4"]
|
||||||
[ext_resource type="Script" uid="uid://wff5063ctp7g" path="res://scripts/debug_overlay.gd" id="5"]
|
[ext_resource type="Script" uid="uid://wff5063ctp7g" path="res://scripts/debug_overlay.gd" id="5"]
|
||||||
[ext_resource type="TileSet" uid="uid://dqem5tbvooxrg" path="res://assets/gfx/RPG DUNGEON VOL 3.tres" id="9"]
|
[ext_resource type="TileSet" uid="uid://dqem5tbvooxrg" path="res://assets/gfx/RPG DUNGEON VOL 3.tres" id="9"]
|
||||||
@@ -28,46 +27,9 @@ modulate = Color(1, 1, 1, 0.77254903)
|
|||||||
z_index = 1
|
z_index = 1
|
||||||
tile_set = ExtResource("9")
|
tile_set = ExtResource("9")
|
||||||
|
|
||||||
[node name="Floor" type="Polygon2D" parent="Environment" unique_id=1715441485]
|
|
||||||
visible = false
|
|
||||||
color = Color(0.3, 0.3, 0.3, 1)
|
|
||||||
polygon = PackedVector2Array(-1000, -1000, 1000, -1000, 1000, 1000, -1000, 1000)
|
|
||||||
metadata/_edit_lock_ = true
|
|
||||||
|
|
||||||
[node name="Walls" type="StaticBody2D" parent="Environment" unique_id=336033150]
|
|
||||||
collision_layer = 4
|
|
||||||
collision_mask = 3
|
|
||||||
|
|
||||||
[node name="WallTop" type="CollisionPolygon2D" parent="Environment/Walls" unique_id=1311846641]
|
|
||||||
polygon = PackedVector2Array(-1020, -1020, 1020, -1020, 1020, -980, -1020, -980)
|
|
||||||
|
|
||||||
[node name="WallBottom" type="CollisionPolygon2D" parent="Environment/Walls" unique_id=902776066]
|
|
||||||
polygon = PackedVector2Array(-1020, 980, 1020, 980, 1020, 1020, -1020, 1020)
|
|
||||||
|
|
||||||
[node name="WallLeft" type="CollisionPolygon2D" parent="Environment/Walls" unique_id=1762713816]
|
|
||||||
polygon = PackedVector2Array(-1020, -980, -980, -980, -980, 980, -1020, 980)
|
|
||||||
|
|
||||||
[node name="WallRight" type="CollisionPolygon2D" parent="Environment/Walls" unique_id=540990153]
|
|
||||||
polygon = PackedVector2Array(980, -980, 1020, -980, 1020, 980, 980, 980)
|
|
||||||
|
|
||||||
[node name="Entities" type="Node2D" parent="." unique_id=1447395523]
|
[node name="Entities" type="Node2D" parent="." unique_id=1447395523]
|
||||||
y_sort_enabled = true
|
y_sort_enabled = true
|
||||||
|
|
||||||
[node name="Box1" parent="Entities" unique_id=2016646819 instance=ExtResource("3")]
|
|
||||||
position = Vector2(101, 66)
|
|
||||||
|
|
||||||
[node name="Box2" parent="Entities" unique_id=219568153 instance=ExtResource("3")]
|
|
||||||
position = Vector2(100, 133)
|
|
||||||
|
|
||||||
[node name="Box3" parent="Entities" unique_id=1831798906 instance=ExtResource("3")]
|
|
||||||
position = Vector2(113, 9)
|
|
||||||
|
|
||||||
[node name="Box4" parent="Entities" unique_id=140447274 instance=ExtResource("3")]
|
|
||||||
position = Vector2(198, 58)
|
|
||||||
|
|
||||||
[node name="Box5" parent="Entities" unique_id=284709248 instance=ExtResource("3")]
|
|
||||||
position = Vector2(74, 12)
|
|
||||||
|
|
||||||
[node name="DebugOverlay" type="CanvasLayer" parent="." unique_id=1325005956]
|
[node name="DebugOverlay" type="CanvasLayer" parent="." unique_id=1325005956]
|
||||||
script = ExtResource("5")
|
script = ExtResource("5")
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
[gd_scene load_steps=3 format=3 uid="uid://bqvx8y2jqn4s"]
|
[gd_scene format=3 uid="uid://bqvx8y2jqn4s"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scripts/smoke_puff.gd" id="1_puff"]
|
[ext_resource type="Script" uid="uid://px6532483e6t" path="res://scripts/smoke_puff.gd" id="1_puff"]
|
||||||
[ext_resource type="Texture2D" uid="uid://bknascfv4twmi" path="res://assets/gfx/smoke_puffs.png" id="2_smoke"]
|
[ext_resource type="Texture2D" uid="uid://bknascfv4twmi" path="res://assets/gfx/smoke_puffs.png" id="2_smoke"]
|
||||||
|
|
||||||
[node name="SmokePuff" type="Node2D"]
|
[node name="SmokePuff" type="Node2D" unique_id=243995580]
|
||||||
script = ExtResource("1_puff")
|
script = ExtResource("1_puff")
|
||||||
|
|
||||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1282738570]
|
||||||
texture = ExtResource("2_smoke")
|
texture = ExtResource("2_smoke")
|
||||||
hframes = 4
|
hframes = 4
|
||||||
vframes = 2
|
vframes = 2
|
||||||
frame = 0
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ extends Label
|
|||||||
|
|
||||||
@export var label: String = "1"
|
@export var label: String = "1"
|
||||||
|
|
||||||
@export var color := Color(1, 1, 1, 1)
|
@export var color := Color.RED # Red color for damage numbers
|
||||||
@export var direction := Vector2.ZERO # Default direction
|
@export var direction := Vector2.ZERO # Default direction (will be random if not set)
|
||||||
var fade_delay := 0.6 # When to start fading (mid-move)
|
var fade_delay := 0.6 # When to start fading (display duration)
|
||||||
var move_duration := 0.8 # Slash exists for 0.3 seconds
|
var move_duration := 1.0 # Total animation duration (includes fade)
|
||||||
var fade_duration := 0.2 # Time to fade out
|
var fade_duration := 0.4 # Time to fade out (after fade_delay)
|
||||||
|
var rise_distance: float = 20.0 # Distance to move upward
|
||||||
var stretch_amount := Vector2(1, 1.4) # How much to stretch the sprite
|
var stretch_amount := Vector2(1, 1.4) # How much to stretch the sprite
|
||||||
|
|
||||||
# Called when the node enters the scene tree for the first time.
|
# Called when the node enters the scene tree for the first time.
|
||||||
@@ -15,20 +16,35 @@ func _ready() -> void:
|
|||||||
pass # Replace with function body.
|
pass # Replace with function body.
|
||||||
|
|
||||||
func _initialize_damage_number() -> void:
|
func _initialize_damage_number() -> void:
|
||||||
|
# Set color (red by default) and text
|
||||||
self.modulate = color
|
self.modulate = color
|
||||||
self.text = label
|
self.text = label
|
||||||
var tween = create_tween()
|
|
||||||
var move_target = global_position + (direction.normalized() * 10) # Moves in given direction
|
|
||||||
tween.set_trans(Tween.TRANS_CUBIC) # Smooth acceleration & deceleration
|
|
||||||
tween.set_ease(Tween.EASE_OUT) # Fast start, then slows down
|
|
||||||
tween.tween_property(self, "global_position", move_target, move_duration)
|
|
||||||
|
|
||||||
# Wait until mid-move to start fade
|
# If direction is not set, use a random upward direction with slight variation
|
||||||
|
if direction == Vector2.ZERO:
|
||||||
|
var random_angle = randf_range(-PI/6, PI/6) # ±30 degrees from straight up
|
||||||
|
direction = Vector2(sin(random_angle), -cos(random_angle)) # Mostly upward with slight variation
|
||||||
|
|
||||||
|
# Calculate target position (move upward with slight horizontal variation)
|
||||||
|
var move_target = global_position + (direction.normalized() * rise_distance)
|
||||||
|
|
||||||
|
# Total animation duration = display (0.6s) + fade (0.4s) = 1.0s
|
||||||
|
var total_duration = fade_delay + fade_duration # 0.6 + 0.4 = 1.0s
|
||||||
|
|
||||||
|
# Create tween for movement (entire duration, continues during fade)
|
||||||
|
var move_tween = create_tween()
|
||||||
|
move_tween.set_trans(Tween.TRANS_CUBIC)
|
||||||
|
move_tween.set_ease(Tween.EASE_OUT)
|
||||||
|
move_tween.tween_property(self, "global_position", move_target, total_duration)
|
||||||
|
|
||||||
|
# Wait for display duration (0.6s), then start fading
|
||||||
await get_tree().create_timer(fade_delay).timeout
|
await get_tree().create_timer(fade_delay).timeout
|
||||||
|
|
||||||
# Start fade-out effect
|
# Fade out over fade_duration (0.4s) while still moving
|
||||||
var fade_tween = create_tween()
|
var fade_tween = create_tween()
|
||||||
fade_tween.tween_property(self, "modulate:a", 0.0, fade_duration) # Fade to transparent
|
fade_tween.tween_property(self, "modulate:a", 0.0, fade_duration)
|
||||||
|
|
||||||
|
# Wait for fade to complete, then remove
|
||||||
await fade_tween.finished
|
await fade_tween.finished
|
||||||
queue_free()
|
queue_free()
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -11,11 +11,36 @@ var is_closing:bool = false
|
|||||||
var is_opening:bool = false
|
var is_opening:bool = false
|
||||||
var time_to_move:float = 0.5
|
var time_to_move:float = 0.5
|
||||||
var move_timer:float = 0.0
|
var move_timer:float = 0.0
|
||||||
|
var animation_start_position: Vector2 = Vector2.ZERO # Position when animation started
|
||||||
|
|
||||||
var initial_position:Vector2 = Vector2.ZERO
|
var closed_position: Vector2 = Vector2.ZERO # Position when door is closed (local)
|
||||||
|
var open_offset: Vector2 = Vector2.ZERO # Offset from closed to open position (local)
|
||||||
|
|
||||||
|
# Room and puzzle state
|
||||||
|
var blocking_room: Dictionary = {} # Room this door blocks (the room you enter INTO)
|
||||||
|
var room1: Dictionary = {} # First room connected by this door (room you leave FROM)
|
||||||
|
var room2: Dictionary = {} # Second room connected by this door (room you enter INTO - the blocking_room)
|
||||||
|
var switch_room: Dictionary = {} # Room where the switch is located (before the door)
|
||||||
|
var room_trigger_area: Area2D = null # Reference to room trigger area for the blocking room
|
||||||
|
var puzzle_solved: bool = false # True when room puzzle is solved
|
||||||
|
var enemies_defeated: bool = false # True when all enemies in room are defeated
|
||||||
|
var switches_activated: bool = false # True when required switches are activated
|
||||||
|
|
||||||
|
# Key door state
|
||||||
|
var key_used: bool = false # True when key has been used
|
||||||
|
var key_indicator: Sprite2D = null # Visual indicator showing key above door
|
||||||
|
|
||||||
|
# Floor switches this door is connected to
|
||||||
|
var connected_switches: Array = [] # Array of floor switch nodes
|
||||||
|
var requires_enemies: bool = false # True if door requires defeating enemies to open
|
||||||
|
var requires_switch: bool = false # True if door requires activating switches to open
|
||||||
|
|
||||||
# Called when the node enters the scene tree for the first time.
|
# Called when the node enters the scene tree for the first time.
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
# Set texture based on door type
|
||||||
|
_update_door_texture()
|
||||||
|
|
||||||
|
# Rotate door first based on direction (original order)
|
||||||
if direction == "Left":
|
if direction == "Left":
|
||||||
self.rotate(-PI/2)
|
self.rotate(-PI/2)
|
||||||
elif direction == "Right":
|
elif direction == "Right":
|
||||||
@@ -23,66 +48,762 @@ func _ready() -> void:
|
|||||||
elif direction == "Down":
|
elif direction == "Down":
|
||||||
self.rotate(PI)
|
self.rotate(PI)
|
||||||
|
|
||||||
initial_position = global_position
|
# Calculate open offset based on direction (in WORLD space)
|
||||||
var amount = 16
|
# NEW RULES:
|
||||||
set_collision_layer_value(7, false)
|
# - Open state: door is at specific tile (UP:tile2, RIGHT:tile4, DOWN:tile2, LEFT:tile3)
|
||||||
if is_closed:
|
# - Closed state: door moves 16 pixels offset from open position
|
||||||
set_collision_layer_value(7, true)
|
# - UP: closed = open + (0, 16) = 16px down (from tile 2 to tile 5)
|
||||||
amount = 0
|
# - RIGHT: closed = open + (-16, 0) = 16px left (from tile 4 to tile 3)
|
||||||
|
# - DOWN: closed = open + (0, 16) = 16px down (from tile 2 to tile 5)
|
||||||
|
# - LEFT: closed = open + (16, 0) = 16px right (from tile 3 to tile 4)
|
||||||
|
var open_amount = 16.0
|
||||||
|
open_offset = Vector2.ZERO
|
||||||
|
$TeleporterIntoClosedRoom.is_enabled = false # disable initially (only enable on closed!)
|
||||||
|
|
||||||
if direction == "Up":
|
if direction == "Up":
|
||||||
position.y = initial_position.y - amount
|
# Door on top wall: closed state is 16px DOWN from open state
|
||||||
|
# So open_offset is positive Y (door moves down when closing, so open is up)
|
||||||
|
# Actually wait - if closed is 16px down from open, then open is 16px up from closed
|
||||||
|
# So open_offset should be negative Y (open position is above closed position)
|
||||||
|
open_offset = Vector2(0, -open_amount) # Open is 16px UP from closed
|
||||||
elif direction == "Down":
|
elif direction == "Down":
|
||||||
position.y = initial_position.y + amount
|
# Door on bottom wall:
|
||||||
|
# For StoneDoor/GateDoor: open is at (col 1, row 1), closed is at (col 1, row 0)
|
||||||
|
# So closed is 16px UP from open, open_offset = (0, -16) means open is 16px DOWN from closed
|
||||||
|
# For KeyDoor: closed is at (col 1, row 0), open is at (col 1, row 1)
|
||||||
|
# So open is 16px DOWN from closed, open_offset = (0, 16)
|
||||||
|
# NOTE: This is recalculated in _ready_after_setup() based on door type
|
||||||
|
open_offset = Vector2(0, -open_amount) # For StoneDoor/GateDoor: open is 16px DOWN from closed
|
||||||
elif direction == "Left":
|
elif direction == "Left":
|
||||||
position.x = initial_position.x - amount
|
# Door on left wall: closed state is 16px RIGHT from open state
|
||||||
|
# So open_offset is positive X (door moves right when closing, so open is left)
|
||||||
|
# Actually wait - if closed is 16px right from open, then open is 16px left from closed
|
||||||
|
# So open_offset should be negative X (open position is left of closed position)
|
||||||
|
open_offset = Vector2(-open_amount, 0) # Open is 16px LEFT from closed
|
||||||
elif direction == "Right":
|
elif direction == "Right":
|
||||||
position.x = initial_position.x + amount
|
# Door on right wall: closed state is 16px LEFT from open state
|
||||||
|
# So open_offset is negative X (door moves left when closing, so open is right)
|
||||||
|
# Actually wait - if closed is 16px left from open, then open is 16px right from closed
|
||||||
|
# So open_offset should be positive X (open position is right of closed position)
|
||||||
|
open_offset = Vector2(open_amount, 0) # Open is 16px RIGHT from closed
|
||||||
|
|
||||||
|
# Note: closed_position will be set in _ready_after_setup after door is positioned
|
||||||
|
# For now, just initialize it
|
||||||
|
closed_position = position
|
||||||
|
|
||||||
|
# Connect KeyInteractionArea signal
|
||||||
|
var key_area = get_node_or_null("KeyInteractionArea")
|
||||||
|
if key_area:
|
||||||
|
key_area.body_entered.connect(_on_key_interaction_area_body_entered)
|
||||||
|
|
||||||
pass # Replace with function body.
|
# Call setup after a frame to ensure everything is ready
|
||||||
|
call_deferred("_ready_after_setup")
|
||||||
|
|
||||||
|
func _update_door_texture():
|
||||||
|
# Update door texture based on door type
|
||||||
|
var sprite = get_node_or_null("Sprite2D")
|
||||||
|
if not sprite:
|
||||||
|
return
|
||||||
|
|
||||||
|
match type:
|
||||||
|
"KeyDoor":
|
||||||
|
var locked_texture = load("res://assets/gfx/door_locked.png")
|
||||||
|
if locked_texture:
|
||||||
|
sprite.texture = locked_texture
|
||||||
|
print("Door: Set KeyDoor texture to door_locked.png")
|
||||||
|
else:
|
||||||
|
push_error("Door: Could not load door_locked.png texture!")
|
||||||
|
"GateDoor":
|
||||||
|
var gate_texture = load("res://assets/gfx/door_gate.png")
|
||||||
|
if gate_texture:
|
||||||
|
sprite.texture = gate_texture
|
||||||
|
print("Door: Set GateDoor texture to door_gate.png")
|
||||||
|
else:
|
||||||
|
push_error("Door: Could not load door_gate.png texture!")
|
||||||
|
"StoneDoor":
|
||||||
|
# Use door_barred.png for stone doors
|
||||||
|
var barred_texture = load("res://assets/gfx/door_barred.png")
|
||||||
|
if barred_texture:
|
||||||
|
sprite.texture = barred_texture
|
||||||
|
print("Door: Set StoneDoor texture to door_barred.png")
|
||||||
|
else:
|
||||||
|
push_error("Door: Could not load door_barred.png texture!")
|
||||||
|
|
||||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
# TODO write code to open/close door here
|
# Handle door opening/closing animation
|
||||||
# when door is open, ofcourse
|
|
||||||
if is_opening or is_closing:
|
if is_opening or is_closing:
|
||||||
move_timer+=delta
|
# Safety check: ensure closed_position is valid before animating
|
||||||
#move 16 pixels in direction under 0.5 seconds
|
if closed_position == Vector2.ZERO:
|
||||||
var amount = clamp(16*(move_timer/time_to_move),0,16)
|
print("Door: ERROR - closed_position is zero during animation! Resetting...")
|
||||||
if is_closing:
|
closed_position = position - open_offset if is_opening else position
|
||||||
amount = 16-amount
|
|
||||||
if direction == "Up":
|
|
||||||
position.y = initial_position.y - amount
|
|
||||||
elif direction == "Down":
|
|
||||||
position.y = initial_position.y + amount
|
|
||||||
elif direction == "Left":
|
|
||||||
position.x = initial_position.x - amount
|
|
||||||
elif direction == "Right":
|
|
||||||
position.x = initial_position.x + amount
|
|
||||||
if move_timer >= time_to_move:
|
|
||||||
if is_opening:
|
|
||||||
is_closed = false
|
|
||||||
set_collision_layer_value(7, false)
|
|
||||||
else:
|
|
||||||
is_closed = true
|
|
||||||
set_collision_layer_value(7, true)
|
|
||||||
is_opening = false
|
is_opening = false
|
||||||
is_closing = false
|
is_closing = false
|
||||||
move_timer = 0
|
move_timer = 0.0
|
||||||
pass
|
# Only update collision for StoneDoor and GateDoor (KeyDoors handle their own state)
|
||||||
|
if type == "StoneDoor" or type == "GateDoor":
|
||||||
|
_update_collision_based_on_position()
|
||||||
|
return
|
||||||
|
|
||||||
|
move_timer += delta
|
||||||
|
var progress = clamp(move_timer / time_to_move, 0.0, 1.0)
|
||||||
|
|
||||||
|
if is_opening:
|
||||||
|
# Interpolate from closed to open position
|
||||||
|
# Start at closed_position (or animation_start_position if set), end at closed_position + open_offset (moving AWAY from closed position)
|
||||||
|
var start_pos = animation_start_position if animation_start_position != Vector2.ZERO else closed_position
|
||||||
|
var target_pos = closed_position + open_offset
|
||||||
|
position = start_pos.lerp(target_pos, progress)
|
||||||
|
global_position = position # Also update global position during animation
|
||||||
|
# Debug: log for KeyDoors to verify movement
|
||||||
|
if type == "KeyDoor" and move_timer < 0.1: # Only log once at start of animation
|
||||||
|
print("Door: KeyDoor opening animation - start: ", start_pos, ", target: ", target_pos, ", offset: ", open_offset, ", direction: ", direction)
|
||||||
|
|
||||||
|
# For KeyDoors: disable collision as soon as opening starts (allow passage immediately)
|
||||||
|
# For StoneDoor/GateDoor: update collision based on position
|
||||||
|
if type == "KeyDoor":
|
||||||
|
# KeyDoors: disable collision immediately when opening starts
|
||||||
|
if get_collision_layer_value(7):
|
||||||
|
set_collision_layer_value(7, false)
|
||||||
|
elif type == "StoneDoor" or type == "GateDoor":
|
||||||
|
# Update collision based on distance to closed position (disable when moving away)
|
||||||
|
var dist_to_closed = position.distance_to(closed_position)
|
||||||
|
if dist_to_closed > 5.0:
|
||||||
|
# Moving away from closed position - disable collision
|
||||||
|
if get_collision_layer_value(7):
|
||||||
|
set_collision_layer_value(7, false)
|
||||||
|
else:
|
||||||
|
# Still near closed position - keep collision enabled
|
||||||
|
if not get_collision_layer_value(7):
|
||||||
|
set_collision_layer_value(7, true)
|
||||||
|
elif is_closing:
|
||||||
|
# Interpolate from open to closed position
|
||||||
|
# NOTE: KeyDoors should NEVER close (only open with key)
|
||||||
|
# CRITICAL: Use stored starting position (set when animation started in _close())
|
||||||
|
# If animation_start_position wasn't set, calculate open position from closed_position + open_offset
|
||||||
|
var start_pos = animation_start_position if animation_start_position != Vector2.ZERO else (closed_position + open_offset)
|
||||||
|
position = start_pos.lerp(closed_position, progress)
|
||||||
|
global_position = position # Also update global position during animation
|
||||||
|
|
||||||
|
# Update collision for StoneDoor/GateDoor only
|
||||||
|
if type == "StoneDoor" or type == "GateDoor":
|
||||||
|
# Update collision based on distance to closed position (enable when approaching closed)
|
||||||
|
var dist_to_closed = position.distance_to(closed_position)
|
||||||
|
if dist_to_closed <= 5.0:
|
||||||
|
# At or near closed position - enable collision
|
||||||
|
if not get_collision_layer_value(7):
|
||||||
|
set_collision_layer_value(7, true)
|
||||||
|
else:
|
||||||
|
# Still away from closed position - keep collision disabled
|
||||||
|
if get_collision_layer_value(7):
|
||||||
|
set_collision_layer_value(7, false)
|
||||||
|
|
||||||
|
if move_timer >= time_to_move:
|
||||||
|
# Animation complete
|
||||||
|
if is_opening:
|
||||||
|
is_closed = false
|
||||||
|
# Move door to open position (away from closed position)
|
||||||
|
var open_position = closed_position + open_offset
|
||||||
|
position = open_position
|
||||||
|
global_position = open_position # Also set global position
|
||||||
|
# When moved from closed position (open), collision should be DISABLED
|
||||||
|
set_collision_layer_value(7, false)
|
||||||
|
print("Door: Opening animation complete - moved to open position: ", open_position, " (closed: ", closed_position, ", offset: ", open_offset, ") - collision DISABLED")
|
||||||
|
# Animation finished, reset flags
|
||||||
|
is_opening = false
|
||||||
|
is_closing = false
|
||||||
|
move_timer = 0.0
|
||||||
|
animation_start_position = Vector2.ZERO # Reset animation start position
|
||||||
|
else:
|
||||||
|
# Closing animation complete
|
||||||
|
is_closed = true
|
||||||
|
position = closed_position
|
||||||
|
global_position = closed_position # Also set global position
|
||||||
|
# When at closed position, collision should be ENABLED
|
||||||
|
set_collision_layer_value(7, true)
|
||||||
|
print("Door: Closing animation complete - moved to closed position: ", closed_position, " - collision ENABLED")
|
||||||
|
# Animation finished, reset flags
|
||||||
|
is_opening = false
|
||||||
|
is_closing = false
|
||||||
|
move_timer = 0.0
|
||||||
|
animation_start_position = Vector2.ZERO # Reset animation start position
|
||||||
|
# Now that door has finished closing, check if puzzle is solved (to open it immediately if already solved)
|
||||||
|
if type == "StoneDoor" or type == "GateDoor":
|
||||||
|
_check_puzzle_state()
|
||||||
|
|
||||||
|
# Update collision based on actual position (safety check in case position was changed externally)
|
||||||
|
# CRITICAL: KeyDoors should NEVER have their position/state changed automatically!
|
||||||
|
# Only update if not currently animating (to avoid interfering with animation)
|
||||||
|
# Only update for StoneDoor and GateDoor (NOT KeyDoors)
|
||||||
|
# IMPORTANT: Only update collision, don't change position - that could interfere with initial setup
|
||||||
|
if not is_opening and not is_closing and (type == "StoneDoor" or type == "GateDoor"):
|
||||||
|
# Only update collision based on position, don't change position or is_closed flag
|
||||||
|
# Position and is_closed should only be changed by explicit _open()/_close() calls or animation
|
||||||
|
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
|
||||||
|
if distance_to_closed <= 1.0:
|
||||||
|
# At closed position - collision should be ENABLED
|
||||||
|
if not get_collision_layer_value(7):
|
||||||
|
set_collision_layer_value(7, true)
|
||||||
|
else:
|
||||||
|
# Away from closed position (open) - collision should be DISABLED
|
||||||
|
if get_collision_layer_value(7):
|
||||||
|
set_collision_layer_value(7, false)
|
||||||
|
|
||||||
|
# For StoneDoor and GateDoor, periodically check puzzle state (only if door is closed and puzzle not solved)
|
||||||
|
# CRITICAL: Only check puzzle state if door has puzzle elements (switches or enemies)
|
||||||
|
# If door has no puzzle elements, it should never open
|
||||||
|
# Check every 10 frames (0.16 seconds at 60fps) to reduce performance impact
|
||||||
|
var check_puzzle_timer = Engine.get_process_frames() % 10
|
||||||
|
if check_puzzle_timer == 0 and (type == "StoneDoor" or type == "GateDoor") and is_closed and not puzzle_solved:
|
||||||
|
# Check if door requires enemies or switches
|
||||||
|
if requires_enemies or requires_switch:
|
||||||
|
_check_puzzle_state()
|
||||||
|
|
||||||
|
# For KeyDoors, ensure they stay at closed position if not opened
|
||||||
|
# KeyDoors should NEVER move unless explicitly opened with a key
|
||||||
|
if type == "KeyDoor" and not is_opening and not is_closing and not key_used:
|
||||||
|
# Ensure KeyDoor is at closed position and has collision enabled
|
||||||
|
if closed_position != Vector2.ZERO:
|
||||||
|
# Snap to closed position if somehow moved (shouldn't happen, but safety check)
|
||||||
|
var distance_to_closed = position.distance_to(closed_position)
|
||||||
|
if distance_to_closed > 1.0:
|
||||||
|
print("Door: KeyDoor was moved incorrectly! Resetting to closed position.")
|
||||||
|
position = closed_position
|
||||||
|
is_closed = true
|
||||||
|
set_collision_layer_value(7, true)
|
||||||
|
|
||||||
|
func _update_collision_based_on_position():
|
||||||
|
# Update collision based on whether door is at closed position or moved away
|
||||||
|
# CRITICAL: This function should NEVER be called for KeyDoors!
|
||||||
|
# Only for StoneDoor and GateDoor
|
||||||
|
# CRITICAL: This function ONLY updates collision, it does NOT change position or is_closed flag
|
||||||
|
# Position and is_closed should only be changed by explicit _open()/_close() calls or animation
|
||||||
|
if type == "KeyDoor":
|
||||||
|
return # Don't update KeyDoors - they handle their own state
|
||||||
|
|
||||||
|
# Only update collision, don't change position or is_closed flag
|
||||||
|
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
|
||||||
|
var distance_threshold = 1.0 # Consider "at closed position" if within 1 pixel
|
||||||
|
|
||||||
|
if distance_to_closed <= distance_threshold:
|
||||||
|
# Door is at closed position - collision should be ENABLED
|
||||||
|
if not get_collision_layer_value(7):
|
||||||
|
set_collision_layer_value(7, true)
|
||||||
|
else:
|
||||||
|
# Door is moved away from closed position (open) - collision should be DISABLED
|
||||||
|
if get_collision_layer_value(7):
|
||||||
|
set_collision_layer_value(7, false)
|
||||||
|
|
||||||
func _open():
|
func _open():
|
||||||
$SfxOpenKeyDoor.play()
|
$TeleporterIntoClosedRoom.is_enabled = false
|
||||||
|
# CRITICAL: For KeyDoors, ensure they start from closed position before opening
|
||||||
|
# KeyDoors should ALWAYS start from closed position when opening (never from open position)
|
||||||
|
if type == "KeyDoor":
|
||||||
|
# KeyDoors should always be at closed position when opening starts
|
||||||
|
# If somehow moved, reset to closed position first
|
||||||
|
if closed_position != Vector2.ZERO:
|
||||||
|
# Reset to closed position to ensure animation starts from correct position
|
||||||
|
position = closed_position
|
||||||
|
global_position = closed_position
|
||||||
|
is_closed = true
|
||||||
|
set_collision_layer_value(7, true) # Collision enabled at closed position
|
||||||
|
print("Door: KeyDoor _open() called - reset to closed position ", closed_position, " before opening")
|
||||||
|
else:
|
||||||
|
push_error("Door: KeyDoor _open() called but closed_position is zero!")
|
||||||
|
return
|
||||||
|
$SfxOpenKeyDoor.play()
|
||||||
|
else:
|
||||||
|
# StoneDoor/GateDoor: Only open if door is currently closed
|
||||||
|
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
|
||||||
|
var is_actually_open = distance_to_closed > 5.0
|
||||||
|
|
||||||
|
if is_actually_open:
|
||||||
|
# Door is already open - don't do anything
|
||||||
|
print("Door: _open() called but door is already open! Position: ", position, ", closed: ", closed_position, ", distance: ", distance_to_closed)
|
||||||
|
# Ensure door is at open position and collision is disabled
|
||||||
|
var open_pos = closed_position + open_offset
|
||||||
|
position = open_pos
|
||||||
|
global_position = open_pos
|
||||||
|
is_closed = false
|
||||||
|
set_collision_layer_value(7, false)
|
||||||
|
return # Don't start animation
|
||||||
|
|
||||||
|
# Door is closed - ensure it's at closed position before opening
|
||||||
|
if closed_position != Vector2.ZERO:
|
||||||
|
position = closed_position
|
||||||
|
global_position = closed_position
|
||||||
|
is_closed = true
|
||||||
|
set_collision_layer_value(7, true)
|
||||||
|
print("Door: StoneDoor/GateDoor _open() called - ensuring door is at closed position ", closed_position, " before opening")
|
||||||
|
else:
|
||||||
|
push_error("Door: StoneDoor/GateDoor _open() called but closed_position is zero!")
|
||||||
|
return
|
||||||
|
|
||||||
|
$SfxOpenStoneDoor.play()
|
||||||
|
|
||||||
|
# CRITICAL: Store starting position for animation (should be closed_position)
|
||||||
|
animation_start_position = position
|
||||||
|
print("Door: Starting open animation from ", animation_start_position, " to ", closed_position + open_offset, " (offset: ", open_offset, ")")
|
||||||
is_opening = true
|
is_opening = true
|
||||||
is_closing = false
|
is_closing = false
|
||||||
move_timer = 0.0
|
move_timer = 0.0
|
||||||
pass
|
|
||||||
|
|
||||||
func _close():
|
func _close():
|
||||||
$SfxOpenStoneDoor.play()
|
# CRITICAL: KeyDoors should NEVER be closed (they only open with a key and stay open)
|
||||||
|
if type == "KeyDoor":
|
||||||
|
print("Door: ERROR - _close() called on KeyDoor! KeyDoors should never be closed!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ensure closed_position is valid before closing
|
||||||
|
if closed_position == Vector2.ZERO:
|
||||||
|
# If closed_position wasn't set correctly, use current position
|
||||||
|
closed_position = position
|
||||||
|
print("Door: WARNING - closed_position was zero, using current position: ", closed_position)
|
||||||
|
|
||||||
|
# Check both flag and actual position to determine door state
|
||||||
|
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
|
||||||
|
var is_actually_at_closed = distance_to_closed < 5.0 # Within 5 pixels of closed position
|
||||||
|
|
||||||
|
print("Door: _close() called - is_closed: ", is_closed, ", is_actually_at_closed: ", is_actually_at_closed, ", position: ", position, ", closed: ", closed_position, ", distance: ", distance_to_closed)
|
||||||
|
|
||||||
|
# If door is already at closed position (both visually and by flag), don't do anything
|
||||||
|
if is_closed and is_actually_at_closed and not is_opening and not is_closing:
|
||||||
|
print("Door: Already closed (both flag and position match), not closing again")
|
||||||
|
return # Already closed, don't do anything
|
||||||
|
|
||||||
|
# CRITICAL: If door is at closed position but flag says open, just fix the state - don't animate
|
||||||
|
if is_actually_at_closed and not is_closed:
|
||||||
|
# Door is visually at closed position but flag says open - fix state only
|
||||||
|
print("Door: Door is at closed position but flag says open! Fixing state only (no animation)")
|
||||||
|
position = closed_position # Ensure exact position
|
||||||
|
is_closed = true
|
||||||
|
set_collision_layer_value(7, true)
|
||||||
|
return # Don't start animation
|
||||||
|
|
||||||
|
# Door is actually open (position is away from closed position) - start closing animation
|
||||||
|
# CRITICAL: Store starting position BEFORE starting animation
|
||||||
|
# Calculate expected open position (closed_position + open_offset)
|
||||||
|
var expected_open_pos = closed_position + open_offset
|
||||||
|
var distance_to_open = position.distance_to(expected_open_pos)
|
||||||
|
|
||||||
|
# Use current position as start (it should already be at open position)
|
||||||
|
# If door is significantly away from expected open position, snap to open position first
|
||||||
|
if distance_to_open > 10.0:
|
||||||
|
# Door is very far from expected open position - reset to open position first
|
||||||
|
print("Door: WARNING - Door is far from expected open position! Resetting to open: ", expected_open_pos, " (was at: ", position, ", distance: ", distance_to_open, ")")
|
||||||
|
animation_start_position = expected_open_pos
|
||||||
|
position = expected_open_pos
|
||||||
|
global_position = expected_open_pos
|
||||||
|
is_closed = false
|
||||||
|
set_collision_layer_value(7, false)
|
||||||
|
else:
|
||||||
|
# Door is at or near open position - use current position as start
|
||||||
|
animation_start_position = position
|
||||||
|
|
||||||
|
print("Door: Starting close animation from ", animation_start_position, " to ", closed_position, " (offset: ", open_offset, ")")
|
||||||
|
$SfxDoorCloses.play()
|
||||||
is_opening = false
|
is_opening = false
|
||||||
is_closing = true
|
is_closing = true
|
||||||
move_timer = 0.0
|
move_timer = 0.0
|
||||||
|
$TeleporterIntoClosedRoom.is_enabled = true
|
||||||
|
|
||||||
|
func _ready_after_setup():
|
||||||
|
# Called after door is fully set up with room references and positioned
|
||||||
|
# NEW LOGIC: Door is positioned at OPEN tile position by game_world
|
||||||
|
# The position set by game_world is the OPEN position (initial state for blocking doors)
|
||||||
|
var open_position = position # Current position is the OPEN position (from tile coordinates)
|
||||||
|
|
||||||
|
print("Door: _ready_after_setup() called - type: ", type, ", direction: ", direction, ", is_closed: ", is_closed, ", open_position: ", open_position)
|
||||||
|
|
||||||
|
# CRITICAL: Calculate closed position based on direction
|
||||||
|
# For StoneDoor/GateDoor: They start OPEN, then CLOSE when entering room
|
||||||
|
# For KeyDoor: They start CLOSED, then OPEN when key is used
|
||||||
|
# - UP: closed = open + (0, 16) = 16px down (from tile 2 to tile 5)
|
||||||
|
# - RIGHT: closed = open + (-16, 0) = 16px left (from tile 4 to tile 3)
|
||||||
|
# - DOWN: For StoneDoor/GateDoor: closed = open + (0, -16) = 16px UP (from row 1 to row 0)
|
||||||
|
# For KeyDoor: open = closed + (0, 16) = 16px DOWN (from row 0 to row 1)
|
||||||
|
# - LEFT: closed = open + (16, 0) = 16px right (from tile 3 to tile 4)
|
||||||
|
var closed_offset = Vector2.ZERO
|
||||||
|
match direction:
|
||||||
|
"Up":
|
||||||
|
closed_offset = Vector2(0, 16) # Closed is 16px DOWN from open
|
||||||
|
"Down":
|
||||||
|
# CRITICAL: For StoneDoor/GateDoor, they start OPEN at (col 1, row 1) and close to (col 1, row 0)
|
||||||
|
# So closed is 16px UP from open (negative Y)
|
||||||
|
# For KeyDoor, they start CLOSED at (col 1, row 0) and open to (col 1, row 1)
|
||||||
|
# But KeyDoor logic is handled separately in _ready_after_setup()
|
||||||
|
if type == "KeyDoor":
|
||||||
|
# KeyDoor: closed is at row 0, open is at row 1 (16px down)
|
||||||
|
# But we calculate from open_position, so this won't be used for KeyDoor
|
||||||
|
closed_offset = Vector2(0, -16) # Won't be used - KeyDoor uses different logic
|
||||||
|
else:
|
||||||
|
# StoneDoor/GateDoor: open is at row 1, closed is at row 0 (16px up)
|
||||||
|
closed_offset = Vector2(0, -16) # Closed is 16px UP from open
|
||||||
|
"Left":
|
||||||
|
closed_offset = Vector2(16, 0) # Closed is 16px RIGHT from open
|
||||||
|
"Right":
|
||||||
|
closed_offset = Vector2(-16, 0) # Closed is 16px LEFT from open
|
||||||
|
|
||||||
|
closed_position = open_position + closed_offset
|
||||||
|
|
||||||
|
# Update open_offset for animation logic (offset from closed to open)
|
||||||
|
# This is used when opening from closed position
|
||||||
|
open_offset = -closed_offset # open_offset = (0, -16) means open is 16px up from closed
|
||||||
|
|
||||||
|
print("Door: Calculated positions - open: ", open_position, ", closed: ", closed_position, ", closed_offset: ", closed_offset, ", open_offset: ", open_offset)
|
||||||
|
|
||||||
|
# CRITICAL: KeyDoors should ALWAYS start closed, regardless of is_closed value
|
||||||
|
# KeyDoors should NEVER be moved until opened with a key
|
||||||
|
# For KeyDoors: game_world positions them at CLOSED position (row 0 for Down doors)
|
||||||
|
# When opened, they move to OPEN position (row 1 for Down doors) - 16px DOWN
|
||||||
|
if type == "KeyDoor":
|
||||||
|
# For KeyDoors, the position from game_world is the CLOSED position
|
||||||
|
# Calculate open position from closed position
|
||||||
|
var keydoor_closed_position = position # Current position is CLOSED (from game_world)
|
||||||
|
|
||||||
|
# Calculate open position based on direction
|
||||||
|
var keydoor_open_offset = Vector2.ZERO
|
||||||
|
match direction:
|
||||||
|
"Up":
|
||||||
|
keydoor_open_offset = Vector2(0, -16) # Open is 16px UP from closed
|
||||||
|
"Down":
|
||||||
|
keydoor_open_offset = Vector2(0, 16) # Open is 16px DOWN from closed (row 0 to row 1)
|
||||||
|
"Left":
|
||||||
|
keydoor_open_offset = Vector2(-16, 0) # Open is 16px LEFT from closed
|
||||||
|
"Right":
|
||||||
|
keydoor_open_offset = Vector2(16, 0) # Open is 16px RIGHT from closed
|
||||||
|
|
||||||
|
# Set positions correctly for KeyDoor
|
||||||
|
closed_position = keydoor_closed_position # Closed is where game_world placed it
|
||||||
|
open_offset = keydoor_open_offset # Offset to move from closed to open
|
||||||
|
|
||||||
|
# KeyDoor starts CLOSED
|
||||||
|
is_closed = true
|
||||||
|
position = closed_position
|
||||||
|
global_position = closed_position
|
||||||
|
set_collision_layer_value(7, true) # Collision enabled when closed
|
||||||
|
print("Door: KeyDoor starting CLOSED at position ", position, " (direction: ", direction, "), will open to ", closed_position + open_offset, " - collision ENABLED")
|
||||||
|
# Create key indicator sprite for KeyDoor
|
||||||
|
_create_key_indicator()
|
||||||
|
return # Exit early for KeyDoors
|
||||||
|
elif is_closed:
|
||||||
|
# StoneDoor/GateDoor starting closed (shouldn't happen for blocking doors, but handle it)
|
||||||
|
position = closed_position
|
||||||
|
global_position = closed_position
|
||||||
|
is_closed = true # Ensure state matches position
|
||||||
|
set_collision_layer_value(7, true)
|
||||||
|
print("Door: Starting CLOSED at position ", position, " (type: ", type, ", direction: ", direction, ") - collision ENABLED")
|
||||||
|
else:
|
||||||
|
# StoneDoor/GateDoor starting OPEN (default for blocking doors)
|
||||||
|
# CRITICAL: Door MUST start at open position (which is where game_world placed it)
|
||||||
|
# Ensure position is EXACTLY at open_position (don't assume game_world set it correctly)
|
||||||
|
if position.distance_to(open_position) > 1.0:
|
||||||
|
# Position doesn't match open_position - force it to open position
|
||||||
|
print("Door: WARNING - Position doesn't match open_position! Forcing to open: ", open_position, " (was: ", position, ")")
|
||||||
|
position = open_position
|
||||||
|
|
||||||
|
global_position = position # Ensure global_position matches position
|
||||||
|
is_closed = false # CRITICAL: State MUST be false (open) when at open position
|
||||||
|
set_collision_layer_value(7, false) # CRITICAL: Collision MUST be DISABLED when open
|
||||||
|
print("Door: Starting OPEN at position ", position, " (closed: ", closed_position, ", open: ", open_position, ", open_offset: ", open_offset, ", type: ", type, ", direction: ", direction, ") - collision DISABLED, is_closed: ", is_closed)
|
||||||
|
|
||||||
|
# CRITICAL: Verify the door is actually at open position after setting it
|
||||||
|
var actual_distance = position.distance_to(closed_position)
|
||||||
|
var expected_distance = 16.0 # Should be 16 pixels away
|
||||||
|
if abs(actual_distance - expected_distance) > 2.0:
|
||||||
|
push_error("Door: ERROR - Door open/closed distance is wrong! Position: ", position, ", closed: ", closed_position, ", distance: ", actual_distance, " (expected: ", expected_distance, ")")
|
||||||
|
# Force it to correct open position
|
||||||
|
position = open_position
|
||||||
|
global_position = open_position
|
||||||
|
is_closed = false # CRITICAL: Ensure state is false when at open position
|
||||||
|
set_collision_layer_value(7, false)
|
||||||
|
print("Door: FORCED door to open position: ", position, " (distance to closed: ", position.distance_to(closed_position), ", is_closed: ", is_closed, ")")
|
||||||
|
|
||||||
|
# FINAL VERIFICATION: Double-check state matches position
|
||||||
|
var distance_to_closed = position.distance_to(closed_position)
|
||||||
|
var should_be_open = distance_to_closed > 8.0 # If more than 8px from closed, should be open
|
||||||
|
if should_be_open and is_closed:
|
||||||
|
push_error("Door: ERROR - Door is at open position but is_closed is true! Fixing state...")
|
||||||
|
is_closed = false
|
||||||
|
set_collision_layer_value(7, false)
|
||||||
|
print("Door: Fixed state - door is now OPEN (is_closed: ", is_closed, ", collision: ", get_collision_layer_value(7), ")")
|
||||||
|
elif not should_be_open and not is_closed:
|
||||||
|
push_error("Door: ERROR - Door is at closed position but is_closed is false! Fixing state...")
|
||||||
|
is_closed = true
|
||||||
|
set_collision_layer_value(7, true)
|
||||||
|
print("Door: Fixed state - door is now CLOSED (is_closed: ", is_closed, ", collision: ", get_collision_layer_value(7), ")")
|
||||||
|
|
||||||
|
# NOTE: Doors are NOT connected via signals to room triggers
|
||||||
|
# Instead, room triggers call door._on_room_entered() directly
|
||||||
|
# This prevents doors from reacting to ALL room entries, only their own blocking room
|
||||||
|
|
||||||
|
func _create_key_indicator():
|
||||||
|
# Create visual indicator for key above door
|
||||||
|
if key_indicator:
|
||||||
|
return # Already created
|
||||||
|
|
||||||
|
key_indicator = Sprite2D.new()
|
||||||
|
# Load key texture from loot system
|
||||||
|
var key_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
|
if key_texture:
|
||||||
|
key_indicator.texture = key_texture
|
||||||
|
key_indicator.hframes = 20
|
||||||
|
key_indicator.vframes = 14
|
||||||
|
key_indicator.frame = (13 * 20) + 10 # Key frame from loot system
|
||||||
|
key_indicator.position = Vector2(0, -24) # Above door
|
||||||
|
key_indicator.visible = false # Hidden until key is used
|
||||||
|
add_child(key_indicator)
|
||||||
|
|
||||||
|
func _on_room_entered(body):
|
||||||
|
# Player entered the room - close this door if puzzle not solved
|
||||||
|
# This door is IN the room that was just entered (room1 == entered room OR blocking_room == entered room)
|
||||||
|
if not body.is_in_group("player"):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Verify this door is in the room we just entered
|
||||||
|
if not room_trigger_area:
|
||||||
|
return # No trigger set, don't do anything
|
||||||
|
|
||||||
|
var trigger_room = room_trigger_area.room if room_trigger_area.room else {}
|
||||||
|
var door_room1 = room1 if room1 else {}
|
||||||
|
var door_blocking_room = blocking_room if blocking_room else {}
|
||||||
|
|
||||||
|
# Check if door is IN the trigger room (door starts FROM trigger room OR blocking_room == trigger room)
|
||||||
|
var door_in_trigger_room = false
|
||||||
|
if trigger_room and not trigger_room.is_empty():
|
||||||
|
# Check room1 first (door starts FROM this room)
|
||||||
|
if door_room1 and not door_room1.is_empty():
|
||||||
|
door_in_trigger_room = (door_room1.x == trigger_room.x and door_room1.y == trigger_room.y and \
|
||||||
|
door_room1.w == trigger_room.w and door_room1.h == trigger_room.h)
|
||||||
|
|
||||||
|
# Also check blocking_room (should match the puzzle room)
|
||||||
|
if not door_in_trigger_room and door_blocking_room and not door_blocking_room.is_empty():
|
||||||
|
door_in_trigger_room = (door_blocking_room.x == trigger_room.x and door_blocking_room.y == trigger_room.y and \
|
||||||
|
door_blocking_room.w == trigger_room.w and door_blocking_room.h == trigger_room.h)
|
||||||
|
|
||||||
|
if not door_in_trigger_room:
|
||||||
|
# This door is NOT in the trigger room - ignore
|
||||||
|
return
|
||||||
|
|
||||||
|
# This door is IN the room that was just entered - close it if puzzle not solved
|
||||||
|
if type == "StoneDoor" or type == "GateDoor":
|
||||||
|
# Close door if puzzle not solved and door is currently open
|
||||||
|
if not puzzle_solved:
|
||||||
|
# Check both is_closed flag AND actual position to determine door state
|
||||||
|
var distance_to_closed = position.distance_to(closed_position) if closed_position != Vector2.ZERO else 999.0
|
||||||
|
var is_actually_open = distance_to_closed > 5.0 # If door is more than 5 pixels away from closed_position, it's open
|
||||||
|
|
||||||
|
print("Door: _on_room_entered() - type: ", type, ", is_closed: ", is_closed, ", is_actually_open: ", is_actually_open, ", position: ", position, ", closed: ", closed_position, ", distance: ", distance_to_closed)
|
||||||
|
|
||||||
|
# CRITICAL: Only close if door is actually open (both flag and position must indicate open)
|
||||||
|
# If door is already closed, don't do anything
|
||||||
|
if is_actually_open and not is_closing and not is_opening:
|
||||||
|
# Door is actually open (position is away from closed position) - close it
|
||||||
|
print("Door: Closing door on room entry - was at position ", position, " (closed: ", closed_position, ", is_closed: ", is_closed, ", distance: ", distance_to_closed, ")")
|
||||||
|
|
||||||
|
# Ensure door is at open position before closing
|
||||||
|
var expected_open_pos = closed_position + open_offset
|
||||||
|
var dist_to_open = position.distance_to(expected_open_pos)
|
||||||
|
if dist_to_open > 5.0:
|
||||||
|
# Door is not at expected open position - reset to open position first
|
||||||
|
print("Door: WARNING - Door is not at expected open position! Resetting to open: ", expected_open_pos, " (was at: ", position, ")")
|
||||||
|
position = expected_open_pos
|
||||||
|
global_position = expected_open_pos
|
||||||
|
is_closed = false
|
||||||
|
set_collision_layer_value(7, false)
|
||||||
|
|
||||||
|
_close()
|
||||||
|
# Don't check puzzle state immediately - wait for door to finish closing
|
||||||
|
# Puzzle state will be checked when closing animation completes (in _process)
|
||||||
|
return # Exit early, don't check puzzle state yet
|
||||||
|
elif is_actually_open:
|
||||||
|
# Door is open but animation already in progress - don't interfere
|
||||||
|
print("Door: Door is open but animation in progress, not closing")
|
||||||
|
return
|
||||||
|
elif not is_actually_open:
|
||||||
|
# Door is already at closed position - but for StoneDoor/GateDoor, this shouldn't happen on room entry
|
||||||
|
# They should start OPEN and then CLOSE when entering room
|
||||||
|
# If door is at closed position, it might have been closed already - don't do anything
|
||||||
|
print("Door: WARNING - Door is already at closed position when entering room! This shouldn't happen for StoneDoor/GateDoor that start open.")
|
||||||
|
if closed_position != Vector2.ZERO:
|
||||||
|
# Ensure exact position and state match
|
||||||
|
position = closed_position
|
||||||
|
global_position = closed_position
|
||||||
|
is_closed = true
|
||||||
|
set_collision_layer_value(7, true) # Collision ENABLED when closed
|
||||||
|
print("Door: Door was already closed - ensuring state is correct, position: ", position, ", closed: ", closed_position)
|
||||||
|
# Now that door is confirmed closed, check if puzzle is already solved
|
||||||
|
# CRITICAL: Only check puzzle state if door is closed - don't check if puzzle is already solved
|
||||||
|
if not puzzle_solved:
|
||||||
|
_check_puzzle_state()
|
||||||
|
# If door is already closing (animation in progress), don't check puzzle state yet
|
||||||
|
# Puzzle state will be checked when closing animation completes (in _process)
|
||||||
|
|
||||||
|
func _on_room_exited(body):
|
||||||
|
# Player left the room
|
||||||
|
if not body.is_in_group("player"):
|
||||||
|
return
|
||||||
|
# Doors stay in their current state
|
||||||
|
|
||||||
|
func _check_puzzle_state():
|
||||||
|
# Check if room puzzle is solved
|
||||||
|
# IMPORTANT: Only check puzzle state if we're in the blocking room
|
||||||
|
if puzzle_solved:
|
||||||
|
return # Already solved
|
||||||
|
|
||||||
|
# Check if all enemies are defeated (enemies in blocking room)
|
||||||
|
if requires_enemies and _are_all_enemies_defeated():
|
||||||
|
print("Door: All enemies defeated! Opening door ", name, " (type: ", type, ", room: ", blocking_room.get("x", "?") if blocking_room and not blocking_room.is_empty() else "?", ",", blocking_room.get("y", "?") if blocking_room and not blocking_room.is_empty() else "?", ")")
|
||||||
|
enemies_defeated = true
|
||||||
|
puzzle_solved = true
|
||||||
|
if is_closed:
|
||||||
|
_open()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if all required switches are activated (switches in switch_room, before the door)
|
||||||
|
if _are_all_switches_activated():
|
||||||
|
switches_activated = true
|
||||||
|
puzzle_solved = true
|
||||||
|
if is_closed:
|
||||||
|
_open()
|
||||||
|
return
|
||||||
|
|
||||||
|
func _are_all_enemies_defeated() -> bool:
|
||||||
|
# Check if all enemies spawned from spawners in the puzzle room are defeated
|
||||||
|
# CRITICAL: Only check enemies that were SPAWNED from spawners (not pre-spawned enemies)
|
||||||
|
# Use room1 (the room this door is IN) or blocking_room for checking enemies
|
||||||
|
var target_room = room1 if room1 and not room1.is_empty() else blocking_room
|
||||||
|
if target_room.is_empty():
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Find all enemies in the room that were spawned from spawners
|
||||||
|
var entities_node = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if not entities_node:
|
||||||
|
entities_node = get_node("/root/GameWorld/Entities")
|
||||||
|
|
||||||
|
if not entities_node:
|
||||||
|
return false
|
||||||
|
|
||||||
|
var room_spawned_enemies = []
|
||||||
|
for child in entities_node.get_children():
|
||||||
|
if child.is_in_group("enemy"):
|
||||||
|
# CRITICAL: Only check enemies that were spawned from spawners (not pre-spawned)
|
||||||
|
if not child.has_meta("spawned_from_spawner") or not child.get_meta("spawned_from_spawner"):
|
||||||
|
continue # Skip pre-spawned enemies
|
||||||
|
|
||||||
|
# Check if enemy is in this room (use position-based check, more reliable)
|
||||||
|
var enemy_in_room = false
|
||||||
|
var tile_size = 16
|
||||||
|
var enemy_tile_x = int(child.global_position.x / tile_size)
|
||||||
|
var enemy_tile_y = int(child.global_position.y / tile_size)
|
||||||
|
var room_min_x = target_room.x + 2
|
||||||
|
var room_max_x = target_room.x + target_room.w - 2
|
||||||
|
var room_min_y = target_room.y + 2
|
||||||
|
var room_max_y = target_room.y + target_room.h - 2
|
||||||
|
|
||||||
|
if enemy_tile_x >= room_min_x and enemy_tile_x < room_max_x and \
|
||||||
|
enemy_tile_y >= room_min_y and enemy_tile_y < room_max_y:
|
||||||
|
enemy_in_room = true
|
||||||
|
# Also check spawner metadata - if enemy has spawner_name matching this room's spawners
|
||||||
|
if child.has_meta("spawner_name"):
|
||||||
|
var spawner_name = child.get_meta("spawner_name")
|
||||||
|
# Spawner names are like "EnemySpawner_<room_x>_<room_y>"
|
||||||
|
if str(target_room.x) in spawner_name and str(target_room.y) in spawner_name:
|
||||||
|
enemy_in_room = true # Confirmed by spawner name
|
||||||
|
|
||||||
|
if enemy_in_room:
|
||||||
|
room_spawned_enemies.append(child)
|
||||||
|
print("Door: Found spawned enemy in room: ", child.name, " (spawner: ", child.get_meta("spawner_name") if child.has_meta("spawner_name") else "unknown", ", is_dead: ", child.is_dead if "is_dead" in child else "unknown", ")")
|
||||||
|
|
||||||
|
# Check if all spawned enemies are dead
|
||||||
|
print("Door: _are_all_enemies_defeated() - Found ", room_spawned_enemies.size(), " spawned enemies in room (", target_room.get("x", "?") if target_room and not target_room.is_empty() else "?", ",", target_room.get("y", "?") if target_room and not target_room.is_empty() else "?", ")")
|
||||||
|
|
||||||
|
if room_spawned_enemies.size() == 0:
|
||||||
|
# No spawned enemies found - if door requires enemies, puzzle is not solved
|
||||||
|
# But if there were never any enemies, this might mean they haven't spawned yet or all are already dead/removed
|
||||||
|
print("Door: No spawned enemies found in room - puzzle not solved yet (enemies may not have spawned or already removed)")
|
||||||
|
return false
|
||||||
|
|
||||||
|
for enemy in room_spawned_enemies:
|
||||||
|
if is_instance_valid(enemy):
|
||||||
|
var enemy_is_dead = false
|
||||||
|
if "is_dead" in enemy:
|
||||||
|
enemy_is_dead = enemy.is_dead
|
||||||
|
else:
|
||||||
|
# Check if enemy is queued for deletion or removed from scene
|
||||||
|
enemy_is_dead = enemy.is_queued_for_deletion() or not enemy.is_inside_tree()
|
||||||
|
|
||||||
|
if not enemy_is_dead:
|
||||||
|
print("Door: Enemy ", enemy.name, " is still alive (is_dead: ", enemy_is_dead, ", is_queued: ", enemy.is_queued_for_deletion(), ", in_tree: ", enemy.is_inside_tree(), ")")
|
||||||
|
return false
|
||||||
|
else:
|
||||||
|
# Enemy is no longer valid (removed from scene) - consider it dead
|
||||||
|
print("Door: Enemy is no longer valid (removed from scene) - counting as dead")
|
||||||
|
|
||||||
|
print("Door: All ", room_spawned_enemies.size(), " spawned enemies are dead! Puzzle solved!")
|
||||||
|
return true # All enemies are dead
|
||||||
|
|
||||||
|
func _are_all_switches_activated() -> bool:
|
||||||
|
# Check if all required switches are activated
|
||||||
|
# CRITICAL: ONLY check connected_switches - switches are explicitly connected when spawned
|
||||||
|
# Do NOT use position-based fallback checks - they cause cross-room door triggering!
|
||||||
|
if connected_switches.size() > 0:
|
||||||
|
# Check all connected switches (these are the switches in THIS door's puzzle room)
|
||||||
|
print("Door: _are_all_switches_activated() - Checking ", connected_switches.size(), " connected switches for door ", name, " (room: ", blocking_room.get("x", "?"), ",", blocking_room.get("y", "?"), ")")
|
||||||
|
for switch in connected_switches:
|
||||||
|
if not is_instance_valid(switch):
|
||||||
|
continue
|
||||||
|
# is_activated is a variable, not a method
|
||||||
|
if not switch.is_activated:
|
||||||
|
print("Door: Switch ", switch.name, " is NOT activated")
|
||||||
|
return false
|
||||||
|
print("Door: All connected switches are activated!")
|
||||||
|
return true # All connected switches are activated
|
||||||
|
|
||||||
|
# CRITICAL: If no switches are connected, the puzzle is NOT solved!
|
||||||
|
# Switches should ALWAYS be connected when spawned - if they're not, it's an error
|
||||||
|
print("Door: WARNING - Door ", name, " has no connected switches! Puzzle cannot be solved!")
|
||||||
|
return false # No connected switches means puzzle is NOT solved
|
||||||
|
|
||||||
|
func _on_key_interaction_area_body_entered(body):
|
||||||
|
# Player entered key interaction area
|
||||||
|
if not body.is_in_group("player"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if type == "KeyDoor" and is_closed and not key_used:
|
||||||
|
# Check if player has a key
|
||||||
|
if body.has_method("has_key") and body.has_method("use_key"):
|
||||||
|
if body.has_key():
|
||||||
|
# Use key and open door
|
||||||
|
body.use_key()
|
||||||
|
key_used = true
|
||||||
|
_show_key_indicator()
|
||||||
|
_open()
|
||||||
|
print("KeyDoor opened with key!")
|
||||||
|
|
||||||
|
func _show_key_indicator():
|
||||||
|
# Show key indicator above door
|
||||||
|
if key_indicator:
|
||||||
|
key_indicator.visible = true
|
||||||
|
# Make sure it's on top (higher z-index or add to front)
|
||||||
|
key_indicator.z_index = 10
|
||||||
|
move_child(key_indicator, get_child_count() - 1) # Move to front
|
||||||
|
else:
|
||||||
|
# Create key indicator if it doesn't exist yet
|
||||||
|
_create_key_indicator()
|
||||||
|
if key_indicator:
|
||||||
|
key_indicator.visible = true
|
||||||
|
|
||||||
|
func teleportPlayer(body: Node2D):
|
||||||
|
var keydoor_open_offset = Vector2.ZERO
|
||||||
|
match direction:
|
||||||
|
"Up":
|
||||||
|
keydoor_open_offset = Vector2(0, 16) # Open is 16px UP from closed
|
||||||
|
"Down":
|
||||||
|
keydoor_open_offset = Vector2(0, -16) # Open is 16px DOWN from closed (row 0 to row 1)
|
||||||
|
"Left":
|
||||||
|
keydoor_open_offset = Vector2(16, 0) # Open is 16px LEFT from closed
|
||||||
|
"Right":
|
||||||
|
keydoor_open_offset = Vector2(-16, 0) # Open is 16px RIGHT from closed
|
||||||
|
body.position = self.global_position + keydoor_open_offset
|
||||||
pass
|
pass
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -45,6 +45,11 @@ func _ready():
|
|||||||
# Top-down physics
|
# Top-down physics
|
||||||
motion_mode = MOTION_MODE_FLOATING
|
motion_mode = MOTION_MODE_FLOATING
|
||||||
|
|
||||||
|
# CRITICAL: Set collision mask to include interactable objects (layer 2) and walls (layer 7)
|
||||||
|
# This allows enemies to collide with interactable objects so they can path around them
|
||||||
|
# Walls are on layer 7 (bit 6 = 64), not layer 4!
|
||||||
|
collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
|
||||||
|
|
||||||
func _physics_process(delta):
|
func _physics_process(delta):
|
||||||
if is_dead:
|
if is_dead:
|
||||||
# Even when dead, allow knockback to continue briefly
|
# Even when dead, allow knockback to continue briefly
|
||||||
@@ -90,6 +95,9 @@ func _physics_process(delta):
|
|||||||
# Check collisions with players
|
# Check collisions with players
|
||||||
_check_player_collision()
|
_check_player_collision()
|
||||||
|
|
||||||
|
# Check collisions with interactable objects
|
||||||
|
_check_interactable_object_collision()
|
||||||
|
|
||||||
# Sync position and animation to clients (only server sends)
|
# Sync position and animation to clients (only server sends)
|
||||||
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
|
if multiplayer.has_multiplayer_peer() and is_multiplayer_authority():
|
||||||
# Get state value if enemy has a state variable (for bats/slimes)
|
# Get state value if enemy has a state variable (for bats/slimes)
|
||||||
@@ -125,6 +133,60 @@ func _check_player_collision():
|
|||||||
if collider and collider.is_in_group("player"):
|
if collider and collider.is_in_group("player"):
|
||||||
_attack_player(collider)
|
_attack_player(collider)
|
||||||
|
|
||||||
|
func _check_interactable_object_collision():
|
||||||
|
# Check collisions with interactable objects and handle pathfinding around them
|
||||||
|
var blocked_objects = []
|
||||||
|
|
||||||
|
for i in get_slide_collision_count():
|
||||||
|
var collision = get_slide_collision(i)
|
||||||
|
var collider = collision.get_collider()
|
||||||
|
|
||||||
|
if collider and collider.is_in_group("interactable_object"):
|
||||||
|
var obj = collider
|
||||||
|
|
||||||
|
# CRITICAL: Enemies cannot move objects that cannot be lifted
|
||||||
|
# If object is not liftable, enemy should try to path around it
|
||||||
|
if obj.has_method("can_be_lifted") and not obj.can_be_lifted():
|
||||||
|
# Object cannot be lifted - store for pathfinding
|
||||||
|
blocked_objects.append({"object": obj, "collision": collision})
|
||||||
|
# If object is liftable but not currently being held, we can still try to push it
|
||||||
|
# but enemies don't actively push liftable objects (only players do)
|
||||||
|
elif obj.has_method("is_being_held") and obj.is_being_held():
|
||||||
|
# Object is being held by someone - treat as obstacle
|
||||||
|
blocked_objects.append({"object": obj, "collision": collision})
|
||||||
|
|
||||||
|
# Handle pathfinding around blocked objects
|
||||||
|
if blocked_objects.size() > 0 and not is_knocked_back:
|
||||||
|
var collision_normal = blocked_objects[0].collision.get_normal()
|
||||||
|
var _obj_pos = blocked_objects[0].object.global_position
|
||||||
|
|
||||||
|
# Try to path around the object by moving perpendicular to collision normal
|
||||||
|
# This creates a side-stepping behavior to go around obstacles
|
||||||
|
var perpendicular = Vector2(-collision_normal.y, collision_normal.x) # Rotate 90 degrees
|
||||||
|
|
||||||
|
# Choose perpendicular direction that moves toward target (if we have one)
|
||||||
|
if target_player and is_instance_valid(target_player):
|
||||||
|
var to_target = (target_player.global_position - global_position).normalized()
|
||||||
|
# If perpendicular dot product with target direction is negative, flip it
|
||||||
|
if perpendicular.dot(to_target) < 0:
|
||||||
|
perpendicular = -perpendicular
|
||||||
|
|
||||||
|
# Apply perpendicular movement (side-step around object)
|
||||||
|
var side_step_velocity = perpendicular * move_speed * 0.7 # 70% of move speed for side-step
|
||||||
|
velocity = velocity.lerp(side_step_velocity, 0.3) # Smoothly blend with existing velocity
|
||||||
|
|
||||||
|
# Also add some push-away from object to create clearance
|
||||||
|
var push_away = collision_normal * move_speed * 0.3
|
||||||
|
velocity = velocity + push_away
|
||||||
|
|
||||||
|
# Limit total velocity to move_speed
|
||||||
|
if velocity.length() > move_speed:
|
||||||
|
velocity = velocity.normalized() * move_speed
|
||||||
|
|
||||||
|
# For humanoid enemies, sometimes try to destroy the object
|
||||||
|
if has_method("_try_attack_object") and randf() < 0.1: # 10% chance per frame when blocked
|
||||||
|
call("_try_attack_object", blocked_objects[0].object)
|
||||||
|
|
||||||
func _attack_player(player):
|
func _attack_player(player):
|
||||||
# Attack cooldown
|
# Attack cooldown
|
||||||
if attack_timer > 0:
|
if attack_timer > 0:
|
||||||
@@ -199,6 +261,11 @@ func take_damage(amount: float, from_position: Vector2):
|
|||||||
# Flash red (even if dying, show the hit)
|
# Flash red (even if dying, show the hit)
|
||||||
_flash_damage()
|
_flash_damage()
|
||||||
|
|
||||||
|
# Show damage number (red, using dmg_numbers.png font) above enemy
|
||||||
|
# Only show if damage > 0
|
||||||
|
if amount > 0:
|
||||||
|
_show_damage_number(amount, from_position)
|
||||||
|
|
||||||
# Sync damage visual to clients
|
# Sync damage visual to clients
|
||||||
# Use game_world to route damage visual sync instead of direct RPC to avoid node path issues
|
# Use game_world to route damage visual sync instead of direct RPC to avoid node path issues
|
||||||
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and is_inside_tree():
|
||||||
@@ -221,6 +288,44 @@ func rpc_take_damage(amount: float, from_position: Vector2):
|
|||||||
if is_multiplayer_authority():
|
if is_multiplayer_authority():
|
||||||
take_damage(amount, from_position)
|
take_damage(amount, from_position)
|
||||||
|
|
||||||
|
func _show_damage_number(amount: float, from_position: Vector2):
|
||||||
|
# Show damage number (red, using dmg_numbers.png font) above enemy
|
||||||
|
# Only show if damage > 0
|
||||||
|
if amount <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
||||||
|
if not damage_number_scene:
|
||||||
|
return
|
||||||
|
|
||||||
|
var damage_label = damage_number_scene.instantiate()
|
||||||
|
if not damage_label:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set damage text and red color
|
||||||
|
damage_label.label = str(int(amount))
|
||||||
|
damage_label.color = Color.RED
|
||||||
|
|
||||||
|
# Calculate direction from attacker (slight upward variation)
|
||||||
|
var direction_from_attacker = (global_position - from_position).normalized()
|
||||||
|
# Add slight upward bias
|
||||||
|
direction_from_attacker = direction_from_attacker.lerp(Vector2(0, -1), 0.5).normalized()
|
||||||
|
damage_label.direction = direction_from_attacker
|
||||||
|
|
||||||
|
# Position above enemy's head
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world:
|
||||||
|
var entities_node = game_world.get_node_or_null("Entities")
|
||||||
|
if entities_node:
|
||||||
|
entities_node.add_child(damage_label)
|
||||||
|
damage_label.global_position = global_position + Vector2(0, -16) # Above enemy head
|
||||||
|
else:
|
||||||
|
get_tree().current_scene.add_child(damage_label)
|
||||||
|
damage_label.global_position = global_position + Vector2(0, -16)
|
||||||
|
else:
|
||||||
|
get_tree().current_scene.add_child(damage_label)
|
||||||
|
damage_label.global_position = global_position + Vector2(0, -16)
|
||||||
|
|
||||||
func _flash_damage():
|
func _flash_damage():
|
||||||
# Flash red visual effect
|
# Flash red visual effect
|
||||||
if sprite:
|
if sprite:
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ func _ready():
|
|||||||
|
|
||||||
state_timer = idle_duration
|
state_timer = idle_duration
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
func _physics_process(delta):
|
func _physics_process(delta):
|
||||||
# Always update animation (even when dead, and on clients)
|
# Always update animation (even when dead, and on clients)
|
||||||
_update_animation(delta)
|
_update_animation(delta)
|
||||||
|
|||||||
@@ -107,6 +107,9 @@ var spawn_position: Vector2
|
|||||||
func _ready():
|
func _ready():
|
||||||
super._ready()
|
super._ready()
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
# Override sprite reference (we use layered sprites, not single sprite)
|
# Override sprite reference (we use layered sprites, not single sprite)
|
||||||
sprite = null # Don't use base class sprite
|
sprite = null # Don't use base class sprite
|
||||||
|
|
||||||
@@ -364,9 +367,13 @@ func _physics_process(delta):
|
|||||||
if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
|
if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update attack timer
|
# Update attack timer and reset attack flags when cooldown is over
|
||||||
if attack_timer > 0:
|
if attack_timer > 0:
|
||||||
attack_timer -= delta
|
attack_timer -= delta
|
||||||
|
if attack_timer <= 0:
|
||||||
|
# Attack cooldown finished - reset attack flags
|
||||||
|
can_attack = true
|
||||||
|
is_attacking = false
|
||||||
|
|
||||||
# Handle knockback
|
# Handle knockback
|
||||||
if is_knocked_back:
|
if is_knocked_back:
|
||||||
@@ -551,10 +558,23 @@ func _chasing_behavior(_delta):
|
|||||||
# Chase player (get close enough to attack)
|
# Chase player (get close enough to attack)
|
||||||
var desired_distance = 45.0 # Stop this far from player (attack range)
|
var desired_distance = 45.0 # Stop this far from player (attack range)
|
||||||
if dist > desired_distance:
|
if dist > desired_distance:
|
||||||
|
# Still too far - chase player
|
||||||
velocity = to_player * move_speed * 0.8 # Reduce chase speed to 80% (was 100%)
|
velocity = to_player * move_speed * 0.8 # Reduce chase speed to 80% (was 100%)
|
||||||
else:
|
else:
|
||||||
# Already close enough, stop and wait for attack cooldown
|
# Close enough to attack - but only stop if we can attack soon
|
||||||
velocity = Vector2.ZERO
|
# If attack is on cooldown, keep following at reduced speed to maintain distance
|
||||||
|
if can_attack:
|
||||||
|
# Can attack - stop and wait for attack opportunity
|
||||||
|
velocity = Vector2.ZERO
|
||||||
|
else:
|
||||||
|
# Attack on cooldown - keep moving slowly to maintain position
|
||||||
|
# Move slightly away if too close, or maintain distance
|
||||||
|
if dist < desired_distance * 0.8:
|
||||||
|
# Too close - back away slightly
|
||||||
|
velocity = -to_player * move_speed * 0.3
|
||||||
|
else:
|
||||||
|
# Good distance - just face player
|
||||||
|
velocity = Vector2.ZERO
|
||||||
current_direction = _get_direction_from_vector(to_player)
|
current_direction = _get_direction_from_vector(to_player)
|
||||||
|
|
||||||
# Set animation based on movement
|
# Set animation based on movement
|
||||||
@@ -592,7 +612,9 @@ func _attacking_behavior(delta):
|
|||||||
return # Don't return to chasing yet
|
return # Don't return to chasing yet
|
||||||
|
|
||||||
# Return to chasing after attack completes
|
# Return to chasing after attack completes
|
||||||
if state_timer <= 0 and not is_attacking and not is_charging_attack:
|
# Check if attack animation is done (not in SWORD animation anymore) and cooldown is over
|
||||||
|
var attack_animation_done = (current_animation != "SWORD")
|
||||||
|
if state_timer <= 0 and attack_animation_done and not is_charging_attack:
|
||||||
ai_state = AIState.CHASING
|
ai_state = AIState.CHASING
|
||||||
state_timer = 3.0
|
state_timer = 3.0
|
||||||
|
|
||||||
@@ -665,6 +687,9 @@ func _perform_attack():
|
|||||||
is_attacking = true
|
is_attacking = true
|
||||||
is_charging_attack = false # Reset charging flag
|
is_charging_attack = false # Reset charging flag
|
||||||
|
|
||||||
|
# CRITICAL: Set attack timer for cooldown (this will reset can_attack when it expires)
|
||||||
|
attack_timer = attack_cooldown
|
||||||
|
|
||||||
# Play attack animation
|
# Play attack animation
|
||||||
_set_animation("SWORD")
|
_set_animation("SWORD")
|
||||||
|
|
||||||
@@ -701,16 +726,42 @@ func _perform_attack():
|
|||||||
# Spawn sword projectile (only on server/authority)
|
# Spawn sword projectile (only on server/authority)
|
||||||
if sword_projectile_scene and is_multiplayer_authority():
|
if sword_projectile_scene and is_multiplayer_authority():
|
||||||
var projectile = sword_projectile_scene.instantiate()
|
var projectile = sword_projectile_scene.instantiate()
|
||||||
get_parent().add_child(projectile)
|
if projectile:
|
||||||
projectile.setup(attack_direction, self)
|
# CRITICAL: Setup projectile with direction and owner BEFORE adding to scene
|
||||||
var spawn_offset = attack_direction * 10.0
|
projectile.setup(attack_direction, self)
|
||||||
projectile.global_position = global_position + spawn_offset
|
var spawn_offset = attack_direction * 10.0
|
||||||
print(name, " attacked with sword projectile at ", global_position)
|
projectile.global_position = global_position + spawn_offset
|
||||||
|
|
||||||
# Reset attack cooldown
|
# Add to scene tree
|
||||||
await get_tree().create_timer(attack_cooldown).timeout
|
var parent = get_parent()
|
||||||
can_attack = true
|
if parent:
|
||||||
is_attacking = false
|
parent.add_child(projectile)
|
||||||
|
else:
|
||||||
|
push_error("EnemyHumanoid: ERROR - No parent node to add projectile to!")
|
||||||
|
projectile.queue_free()
|
||||||
|
|
||||||
|
func _try_attack_object(obj: Node):
|
||||||
|
# Humanoid enemies can sometimes try to attack/destroy interactable objects
|
||||||
|
# Only try if we're not already attacking and object is destroyable
|
||||||
|
if is_attacking or not can_attack:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only try on server/authority
|
||||||
|
if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if object can be destroyed
|
||||||
|
if obj.has_method("can_be_destroyed") and obj.can_be_destroyed():
|
||||||
|
# 30% chance to try attacking the object (less frequent than player attacks to avoid spam)
|
||||||
|
if randf() < 0.3:
|
||||||
|
# Face the object
|
||||||
|
var to_object = (obj.global_position - global_position).normalized()
|
||||||
|
current_direction = _get_direction_from_vector(to_object)
|
||||||
|
|
||||||
|
# Perform attack - sword projectile will damage the object if it hits
|
||||||
|
# The object will handle damage from sword projectiles (sword_projectile.gd already handles this)
|
||||||
|
_perform_attack()
|
||||||
|
print(name, " is attacking object ", obj.name, "!")
|
||||||
|
|
||||||
@rpc("authority", "reliable")
|
@rpc("authority", "reliable")
|
||||||
func _sync_attack(direction: int, attack_dir: Vector2):
|
func _sync_attack(direction: int, attack_dir: Vector2):
|
||||||
@@ -739,6 +790,7 @@ func _update_animation(delta):
|
|||||||
# Update animation frame timing (even when dead, to play death animation)
|
# Update animation frame timing (even when dead, to play death animation)
|
||||||
time_since_last_frame += delta
|
time_since_last_frame += delta
|
||||||
if time_since_last_frame >= ANIMATIONS[current_animation]["frameDurations"][current_frame] / 1000.0:
|
if time_since_last_frame >= ANIMATIONS[current_animation]["frameDurations"][current_frame] / 1000.0:
|
||||||
|
var was_attacking = (current_animation == "SWORD" and current_frame == len(ANIMATIONS[current_animation]["frames"]) - 1)
|
||||||
current_frame += 1
|
current_frame += 1
|
||||||
if current_frame >= len(ANIMATIONS[current_animation]["frames"]):
|
if current_frame >= len(ANIMATIONS[current_animation]["frames"]):
|
||||||
current_frame -= 1 # Stay on last frame
|
current_frame -= 1 # Stay on last frame
|
||||||
@@ -747,7 +799,15 @@ func _update_animation(delta):
|
|||||||
if ANIMATIONS[current_animation]["nextAnimation"] != null and not is_dead:
|
if ANIMATIONS[current_animation]["nextAnimation"] != null and not is_dead:
|
||||||
# Don't transition to next animation if dead
|
# Don't transition to next animation if dead
|
||||||
current_frame = 0
|
current_frame = 0
|
||||||
|
var old_animation = current_animation
|
||||||
current_animation = ANIMATIONS[current_animation]["nextAnimation"]
|
current_animation = ANIMATIONS[current_animation]["nextAnimation"]
|
||||||
|
|
||||||
|
# CRITICAL: If SWORD animation just completed, reset attack flags
|
||||||
|
if old_animation == "SWORD" and was_attacking:
|
||||||
|
# Attack animation finished - reset attack state
|
||||||
|
# Note: can_attack will be reset when attack_timer expires
|
||||||
|
# But we can reset is_attacking now since animation is done
|
||||||
|
is_attacking = false
|
||||||
time_since_last_frame = 0.0
|
time_since_last_frame = 0.0
|
||||||
|
|
||||||
# Calculate frame index (8 directions, 35 frames per direction)
|
# Calculate frame index (8 directions, 35 frames per direction)
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ func _ready():
|
|||||||
|
|
||||||
state_timer = idle_duration
|
state_timer = idle_duration
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
func _ai_behavior(delta):
|
func _ai_behavior(delta):
|
||||||
# Update state timer
|
# Update state timer
|
||||||
state_timer -= delta
|
state_timer -= delta
|
||||||
|
|||||||
@@ -27,11 +27,15 @@ func _ready():
|
|||||||
|
|
||||||
max_health = 20.0
|
max_health = 20.0
|
||||||
current_health = max_health
|
current_health = max_health
|
||||||
move_speed = 35.0 # Slow normally (reduced from 60)
|
move_speed = 20.0 # Very slow (reduced from 35)
|
||||||
damage = 6.0
|
damage = 6.0
|
||||||
|
|
||||||
state_timer = idle_duration
|
state_timer = idle_duration
|
||||||
|
|
||||||
|
# CRITICAL: Ensure collision mask is set correctly after super._ready()
|
||||||
|
# Walls are on layer 7 (bit 6 = 64), objects on layer 2, players on layer 1
|
||||||
|
collision_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
|
||||||
|
|
||||||
# Slime is small - adjust collision
|
# Slime is small - adjust collision
|
||||||
if collision_shape and collision_shape.shape:
|
if collision_shape and collision_shape.shape:
|
||||||
collision_shape.shape.radius = 6.0 # 12x12 effective size
|
collision_shape.shape.radius = 6.0 # 12x12 effective size
|
||||||
@@ -120,11 +124,11 @@ func _start_jump():
|
|||||||
# Jump towards player if nearby
|
# Jump towards player if nearby
|
||||||
if target_player and is_instance_valid(target_player):
|
if target_player and is_instance_valid(target_player):
|
||||||
var direction = (target_player.global_position - global_position).normalized()
|
var direction = (target_player.global_position - global_position).normalized()
|
||||||
velocity = direction * (move_speed * 1.8) # Faster during jump
|
velocity = direction * (move_speed * 1.4) # Slightly faster during jump (reduced from 1.8)
|
||||||
else:
|
else:
|
||||||
# Random jump direction
|
# Random jump direction
|
||||||
var random_dir = Vector2(randf() - 0.5, randf() - 0.5).normalized()
|
var random_dir = Vector2(randf() - 0.5, randf() - 0.5).normalized()
|
||||||
velocity = random_dir * (move_speed * 1.8)
|
velocity = random_dir * (move_speed * 1.4) # Slightly faster during jump (reduced from 1.8)
|
||||||
|
|
||||||
func _jumping_behavior(_delta):
|
func _jumping_behavior(_delta):
|
||||||
# Continue moving in jump direction
|
# Continue moving in jump direction
|
||||||
|
|||||||
@@ -16,10 +16,16 @@ func _ready():
|
|||||||
print(" Position: ", global_position)
|
print(" Position: ", global_position)
|
||||||
print(" Is server: ", multiplayer.is_server())
|
print(" Is server: ", multiplayer.is_server())
|
||||||
print(" Has multiplayer peer: ", multiplayer.has_multiplayer_peer())
|
print(" Has multiplayer peer: ", multiplayer.has_multiplayer_peer())
|
||||||
|
print(" Is authority: ", is_multiplayer_authority() if multiplayer.has_multiplayer_peer() else "N/A")
|
||||||
print(" spawn_on_ready: ", spawn_on_ready)
|
print(" spawn_on_ready: ", spawn_on_ready)
|
||||||
print(" max_enemies: ", max_enemies)
|
print(" max_enemies: ", max_enemies)
|
||||||
|
print(" enemy_scenes.size(): ", enemy_scenes.size())
|
||||||
print(" Parent: ", get_parent())
|
print(" Parent: ", get_parent())
|
||||||
|
|
||||||
|
# Verify enemy_scenes is set
|
||||||
|
if enemy_scenes.size() == 0:
|
||||||
|
push_error("EnemySpawner: ERROR - enemy_scenes array is EMPTY! Spawner will not be able to spawn enemies!")
|
||||||
|
|
||||||
# Spawn on server, or in single player (no multiplayer peer)
|
# Spawn on server, or in single player (no multiplayer peer)
|
||||||
var should_spawn = spawn_on_ready and (multiplayer.is_server() or not multiplayer.has_multiplayer_peer())
|
var should_spawn = spawn_on_ready and (multiplayer.is_server() or not multiplayer.has_multiplayer_peer())
|
||||||
print(" Should spawn? ", should_spawn)
|
print(" Should spawn? ", should_spawn)
|
||||||
@@ -28,7 +34,7 @@ func _ready():
|
|||||||
print(" Calling spawn_enemy()...")
|
print(" Calling spawn_enemy()...")
|
||||||
call_deferred("spawn_enemy") # Use call_deferred to ensure scene is ready
|
call_deferred("spawn_enemy") # Use call_deferred to ensure scene is ready
|
||||||
else:
|
else:
|
||||||
print(" NOT spawning - conditions not met")
|
print(" NOT spawning - conditions not met (spawn_on_ready=", spawn_on_ready, ", will spawn when player enters room)")
|
||||||
print("========================================")
|
print("========================================")
|
||||||
|
|
||||||
func _process(delta):
|
func _process(delta):
|
||||||
@@ -48,6 +54,23 @@ func _process(delta):
|
|||||||
|
|
||||||
func spawn_enemy():
|
func spawn_enemy():
|
||||||
print(">>> spawn_enemy() CALLED <<<")
|
print(">>> spawn_enemy() CALLED <<<")
|
||||||
|
print(" Spawner: ", name, " at ", global_position)
|
||||||
|
print(" enemy_scenes.size(): ", enemy_scenes.size())
|
||||||
|
print(" spawned_enemies.size(): ", spawned_enemies.size(), " / max_enemies: ", max_enemies)
|
||||||
|
print(" spawn_on_ready: ", spawn_on_ready)
|
||||||
|
|
||||||
|
# CRITICAL: Check if we can spawn (don't spawn if already at max)
|
||||||
|
# Clean up dead enemies first
|
||||||
|
spawned_enemies = spawned_enemies.filter(func(e): return is_instance_valid(e) and not e.is_dead)
|
||||||
|
|
||||||
|
if spawned_enemies.size() >= max_enemies:
|
||||||
|
print(" ERROR: Cannot spawn - already at max enemies (", spawned_enemies.size(), " >= ", max_enemies, ")")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only spawn on server (authority)
|
||||||
|
if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
|
||||||
|
print(" ERROR: Cannot spawn - not multiplayer authority!")
|
||||||
|
return
|
||||||
|
|
||||||
# Choose enemy scene to spawn
|
# Choose enemy scene to spawn
|
||||||
var scene_to_spawn: PackedScene = null
|
var scene_to_spawn: PackedScene = null
|
||||||
@@ -55,15 +78,39 @@ func spawn_enemy():
|
|||||||
# Use random scene from list
|
# Use random scene from list
|
||||||
scene_to_spawn = enemy_scenes[randi() % enemy_scenes.size()]
|
scene_to_spawn = enemy_scenes[randi() % enemy_scenes.size()]
|
||||||
print(" Selected enemy scene from list: ", scene_to_spawn)
|
print(" Selected enemy scene from list: ", scene_to_spawn)
|
||||||
|
else:
|
||||||
|
push_error("ERROR: enemy_scenes array is EMPTY! Spawner has no enemy scenes to spawn!")
|
||||||
|
return
|
||||||
|
|
||||||
if not scene_to_spawn:
|
if not scene_to_spawn:
|
||||||
push_error("ERROR: No enemy scene set for spawner! Add scenes to enemy_scenes array.")
|
push_error("ERROR: Failed to select enemy scene!")
|
||||||
return
|
return
|
||||||
|
|
||||||
print(" Spawning enemy at ", global_position)
|
print(" Spawning enemy at ", global_position)
|
||||||
|
|
||||||
# Spawn smoke puff effect
|
# CRITICAL: Spawn 3-4 smoke puffs first, wait for them to finish, THEN spawn enemy
|
||||||
_spawn_smoke_puff()
|
var num_puffs = randi_range(3, 4) # 3 or 4 smoke puffs
|
||||||
|
print(" Spawning ", num_puffs, " smoke puffs before enemy...")
|
||||||
|
|
||||||
|
# Spawn multiple smoke puffs at slightly different positions
|
||||||
|
var smoke_puffs = []
|
||||||
|
var puff_spawn_radius = 8.0 # Pixels - spawn puffs in a small area around spawner
|
||||||
|
|
||||||
|
for i in range(num_puffs):
|
||||||
|
var puff_offset = Vector2(
|
||||||
|
randf_range(-puff_spawn_radius, puff_spawn_radius),
|
||||||
|
randf_range(-puff_spawn_radius, puff_spawn_radius)
|
||||||
|
)
|
||||||
|
var puff = _spawn_smoke_puff_at_position(global_position + puff_offset)
|
||||||
|
if puff:
|
||||||
|
smoke_puffs.append(puff)
|
||||||
|
|
||||||
|
# Wait for smoke puffs to finish animating before spawning enemy
|
||||||
|
# Smoke puff animation: 4 frames * (1/10.0) seconds per frame = 0.4s, plus move_duration 1.5s, plus fade_duration 0.5s = ~2.4s total
|
||||||
|
var smoke_animation_duration = (4.0 / 10.0) + 1.5 + 0.5 # Total animation time
|
||||||
|
await get_tree().create_timer(smoke_animation_duration).timeout
|
||||||
|
|
||||||
|
print(" Smoke puffs finished - now spawning enemy...")
|
||||||
|
|
||||||
print(" Instantiating enemy scene...")
|
print(" Instantiating enemy scene...")
|
||||||
var enemy = scene_to_spawn.instantiate()
|
var enemy = scene_to_spawn.instantiate()
|
||||||
@@ -79,6 +126,18 @@ func spawn_enemy():
|
|||||||
enemy.spawn_position = global_position
|
enemy.spawn_position = global_position
|
||||||
print(" Set enemy position to: ", global_position)
|
print(" Set enemy position to: ", global_position)
|
||||||
|
|
||||||
|
# CRITICAL: Mark this enemy as spawned from a spawner (for door puzzle tracking)
|
||||||
|
enemy.set_meta("spawned_from_spawner", true)
|
||||||
|
enemy.set_meta("spawner_name", name)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Set multiplayer authority BEFORE adding to scene tree (CRITICAL for RPC to work!)
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
enemy.set_multiplayer_authority(1)
|
||||||
|
|
||||||
# Add to YSort node for automatic Y-sorting
|
# Add to YSort node for automatic Y-sorting
|
||||||
var ysort = get_parent().get_node_or_null("Entities")
|
var ysort = get_parent().get_node_or_null("Entities")
|
||||||
var parent = ysort if ysort else get_parent()
|
var parent = ysort if ysort else get_parent()
|
||||||
@@ -91,9 +150,9 @@ func spawn_enemy():
|
|||||||
print(" Adding enemy as child...")
|
print(" Adding enemy as child...")
|
||||||
parent.add_child(enemy)
|
parent.add_child(enemy)
|
||||||
|
|
||||||
# Set multiplayer authority to server (peer 1)
|
# CRITICAL: Re-verify collision_mask AFTER adding to scene (in case _ready() or scene file overrides it)
|
||||||
if multiplayer.has_multiplayer_peer():
|
# Use call_deferred to ensure _ready() has completed first, then set the entire mask
|
||||||
enemy.set_multiplayer_authority(1)
|
call_deferred("_verify_enemy_collision_mask", enemy)
|
||||||
|
|
||||||
# Determine which scene index was used (for syncing to clients)
|
# Determine which scene index was used (for syncing to clients)
|
||||||
var scene_index = -1
|
var scene_index = -1
|
||||||
@@ -112,18 +171,32 @@ func spawn_enemy():
|
|||||||
print(" ✓ Successfully spawned enemy: ", enemy.name, " at ", global_position, " scene_index: ", scene_index)
|
print(" ✓ Successfully spawned enemy: ", enemy.name, " at ", global_position, " scene_index: ", scene_index)
|
||||||
print(" Total spawned enemies: ", spawned_enemies.size())
|
print(" Total spawned enemies: ", spawned_enemies.size())
|
||||||
|
|
||||||
|
# If this spawner is marked for one-time spawn, destroy it after spawning
|
||||||
|
if has_meta("spawn_once") and get_meta("spawn_once"):
|
||||||
|
print(" Spawner marked for one-time spawn - destroying after spawn")
|
||||||
|
call_deferred("queue_free") # Destroy spawner after spawning once
|
||||||
|
|
||||||
# Sync spawn to all clients via GameWorld
|
# Sync spawn to all clients via GameWorld
|
||||||
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||||
# Get GameWorld directly since spawner is a child of GameWorld
|
# Get GameWorld by traversing up the tree (spawner is child of Entities, which is child of GameWorld)
|
||||||
var game_world = get_parent()
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
print(" DEBUG: game_world=", game_world, " spawner name=", name)
|
if not game_world:
|
||||||
|
# Fallback: traverse up the tree to find GameWorld
|
||||||
|
var node = get_parent()
|
||||||
|
while node:
|
||||||
|
if node.has_method("_sync_enemy_spawn"):
|
||||||
|
game_world = node
|
||||||
|
break
|
||||||
|
node = node.get_parent()
|
||||||
|
|
||||||
if game_world and game_world.has_method("_sync_enemy_spawn"):
|
if game_world and game_world.has_method("_sync_enemy_spawn"):
|
||||||
# Use spawner name (relative to GameWorld) since it's a direct child
|
# Use spawner name for identification
|
||||||
print(" DEBUG: Calling _sync_enemy_spawn.rpc with name=", name, " pos=", global_position, " scene_index=", scene_index)
|
print(" DEBUG: Calling _sync_enemy_spawn.rpc with name=", name, " pos=", global_position, " scene_index=", scene_index)
|
||||||
game_world._sync_enemy_spawn.rpc(name, global_position, scene_index)
|
game_world._sync_enemy_spawn.rpc(name, global_position, scene_index)
|
||||||
print(" Sent RPC to sync enemy spawn to clients: spawner=", name, " pos=", global_position, " scene_index=", scene_index)
|
print(" Sent RPC to sync enemy spawn to clients: spawner=", name, " pos=", global_position, " scene_index=", scene_index)
|
||||||
else:
|
else:
|
||||||
push_error("ERROR: Could not find GameWorld or _sync_enemy_spawn method! game_world=", game_world, " has_method=", game_world.has_method("_sync_enemy_spawn") if game_world else "N/A")
|
var has_method_str = str(game_world.has_method("_sync_enemy_spawn")) if game_world else "N/A"
|
||||||
|
push_error("ERROR: Could not find GameWorld or _sync_enemy_spawn method! game_world=", game_world, " has_method=", has_method_str)
|
||||||
|
|
||||||
func spawn_enemy_at_position(spawn_pos: Vector2, scene_index: int = -1):
|
func spawn_enemy_at_position(spawn_pos: Vector2, scene_index: int = -1):
|
||||||
# This method is called by GameWorld RPC to spawn enemies on clients
|
# This method is called by GameWorld RPC to spawn enemies on clients
|
||||||
@@ -158,6 +231,14 @@ func spawn_enemy_at_position(spawn_pos: Vector2, scene_index: int = -1):
|
|||||||
if "spawn_position" in enemy:
|
if "spawn_position" in enemy:
|
||||||
enemy.spawn_position = spawn_pos
|
enemy.spawn_position = spawn_pos
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Set multiplayer authority BEFORE adding to scene tree (CRITICAL!)
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
enemy.set_multiplayer_authority(1)
|
||||||
|
|
||||||
# Add to YSort node for automatic Y-sorting
|
# Add to YSort node for automatic Y-sorting
|
||||||
var ysort = get_parent().get_node_or_null("Entities")
|
var ysort = get_parent().get_node_or_null("Entities")
|
||||||
var parent = ysort if ysort else get_parent()
|
var parent = ysort if ysort else get_parent()
|
||||||
@@ -167,9 +248,9 @@ func spawn_enemy_at_position(spawn_pos: Vector2, scene_index: int = -1):
|
|||||||
|
|
||||||
parent.add_child(enemy)
|
parent.add_child(enemy)
|
||||||
|
|
||||||
# Set multiplayer authority to server (peer 1)
|
# CRITICAL: Re-verify collision_mask AFTER adding to scene (in case _ready() or scene file overrides it)
|
||||||
if multiplayer.has_multiplayer_peer():
|
# Use call_deferred to ensure _ready() has completed first, then set the entire mask
|
||||||
enemy.set_multiplayer_authority(1)
|
call_deferred("_verify_enemy_collision_mask", enemy)
|
||||||
|
|
||||||
print(" ✓ Client spawned enemy: ", enemy.name, " at ", spawn_pos)
|
print(" ✓ Client spawned enemy: ", enemy.name, " at ", spawn_pos)
|
||||||
|
|
||||||
@@ -184,18 +265,50 @@ func get_spawned_enemy_positions() -> Array:
|
|||||||
enemy_data.append({"position": enemy.global_position, "scene_index": scene_index})
|
enemy_data.append({"position": enemy.global_position, "scene_index": scene_index})
|
||||||
return enemy_data
|
return enemy_data
|
||||||
|
|
||||||
|
func _verify_enemy_collision_mask(enemy: Node):
|
||||||
|
# Verify and correct enemy collision_mask after _ready() has completed
|
||||||
|
# This ensures enemies always collide with walls (layer 7 = bit 6 = 64), not layer 3 or 4
|
||||||
|
if not is_instance_valid(enemy):
|
||||||
|
return
|
||||||
|
|
||||||
|
var expected_mask = 1 | 2 | 64 # Collide with players (layer 1), objects (layer 2), and walls (layer 7 = bit 6 = 64)
|
||||||
|
if enemy.collision_mask != expected_mask:
|
||||||
|
print("EnemySpawner: WARNING - Enemy ", enemy.name, " collision_mask was ", enemy.collision_mask, ", expected ", expected_mask, "! Correcting...")
|
||||||
|
enemy.collision_mask = expected_mask
|
||||||
|
|
||||||
|
# Double-check by setting individual layers to be absolutely sure
|
||||||
|
enemy.set_collision_mask_value(1, true) # Players
|
||||||
|
enemy.set_collision_mask_value(2, true) # Objects
|
||||||
|
enemy.set_collision_mask_value(7, true) # Walls (layer 7)
|
||||||
|
enemy.set_collision_mask_value(3, false) # Ensure layer 3 is NOT set
|
||||||
|
enemy.set_collision_mask_value(4, false) # Ensure layer 4 is NOT set
|
||||||
|
print("EnemySpawner: Corrected enemy ", enemy.name, " collision_mask to ", enemy.collision_mask)
|
||||||
|
|
||||||
func _spawn_smoke_puff():
|
func _spawn_smoke_puff():
|
||||||
print(" _spawn_smoke_puff() called")
|
# Legacy function - use _spawn_smoke_puff_at_position instead
|
||||||
|
_spawn_smoke_puff_at_position(global_position)
|
||||||
|
|
||||||
|
func _spawn_smoke_puff_at_position(puff_position: Vector2) -> Node:
|
||||||
|
print(" _spawn_smoke_puff_at_position() called at ", puff_position)
|
||||||
print(" smoke_puff_scene: ", smoke_puff_scene)
|
print(" smoke_puff_scene: ", smoke_puff_scene)
|
||||||
|
|
||||||
if smoke_puff_scene:
|
if smoke_puff_scene:
|
||||||
print(" Instantiating smoke puff...")
|
print(" Instantiating smoke puff...")
|
||||||
var puff = smoke_puff_scene.instantiate()
|
var puff = smoke_puff_scene.instantiate()
|
||||||
if puff:
|
if puff:
|
||||||
puff.global_position = global_position
|
puff.global_position = puff_position
|
||||||
get_parent().add_child(puff)
|
var parent = get_parent()
|
||||||
print(" ✓ Smoke puff spawned at ", global_position)
|
if parent:
|
||||||
|
parent.add_child(puff)
|
||||||
|
print(" ✓ Smoke puff spawned at ", puff_position)
|
||||||
|
return puff
|
||||||
|
else:
|
||||||
|
print(" ERROR: No parent node for smoke puff!")
|
||||||
|
puff.queue_free()
|
||||||
|
return null
|
||||||
else:
|
else:
|
||||||
print(" ERROR: Failed to instantiate smoke puff")
|
print(" ERROR: Failed to instantiate smoke puff")
|
||||||
|
return null
|
||||||
else:
|
else:
|
||||||
print(" WARNING: No smoke puff scene loaded")
|
print(" WARNING: No smoke puff scene loaded")
|
||||||
|
return null
|
||||||
|
|||||||
@@ -1,35 +1,99 @@
|
|||||||
extends Node2D
|
extends Node2D
|
||||||
|
|
||||||
# Floating text that rises and fades out
|
# Floating text and item graphic that rises and fades out
|
||||||
|
|
||||||
@onready var label = $Label
|
@onready var label = $Label
|
||||||
|
@onready var item_sprite = $ItemSprite # Sprite2D for item graphic (optional)
|
||||||
|
|
||||||
var text: String = ""
|
var text: String = ""
|
||||||
var color: Color = Color.WHITE
|
var color: Color = Color.WHITE
|
||||||
var duration: float = 1.0
|
var display_duration: float = 0.5 # How long to show text/graphic before fading
|
||||||
|
var fade_duration: float = 0.5 # How long fade out takes
|
||||||
var rise_distance: float = 30.0
|
var rise_distance: float = 30.0
|
||||||
|
var is_coin: bool = false # Track if this is a coin (to animate)
|
||||||
|
|
||||||
func setup(text_value: String, text_color: Color):
|
func setup(text_value: String, text_color: Color, show_time: float = 0.5, fade_time: float = 0.5, item_texture: Texture2D = null, sprite_hframes: int = 1, sprite_vframes: int = 1, sprite_frame: int = 0):
|
||||||
text = text_value
|
text = text_value
|
||||||
color = text_color
|
color = text_color
|
||||||
|
display_duration = show_time
|
||||||
|
fade_duration = fade_time
|
||||||
|
|
||||||
if label:
|
if label:
|
||||||
label.text = text
|
label.text = text
|
||||||
label.modulate = color
|
label.modulate = color
|
||||||
|
label.modulate.a = 1.0 # Start fully visible
|
||||||
|
|
||||||
|
# Setup item sprite if texture provided
|
||||||
|
if item_sprite and item_texture:
|
||||||
|
item_sprite.visible = true
|
||||||
|
item_sprite.texture = item_texture
|
||||||
|
item_sprite.hframes = sprite_hframes
|
||||||
|
item_sprite.vframes = sprite_vframes
|
||||||
|
item_sprite.frame = sprite_frame
|
||||||
|
item_sprite.modulate = Color.WHITE
|
||||||
|
item_sprite.modulate.a = 1.0
|
||||||
|
# Position sprite above label (if label exists) or centered
|
||||||
|
if label:
|
||||||
|
item_sprite.position = Vector2(0, -24) # Above the text (sprite is ~16px tall)
|
||||||
|
else:
|
||||||
|
item_sprite.position = Vector2(0, 0)
|
||||||
|
|
||||||
|
# Check if this is a coin (6 frames horizontal, 1 frame vertical) - animate it
|
||||||
|
if sprite_hframes == 6 and sprite_vframes == 1:
|
||||||
|
is_coin = true
|
||||||
|
else:
|
||||||
|
# Hide sprite if no texture provided
|
||||||
|
if item_sprite:
|
||||||
|
item_sprite.visible = false
|
||||||
|
is_coin = false
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
# Animate rising and fading
|
# Start coin animation if needed
|
||||||
var tween = create_tween()
|
if is_coin and item_sprite:
|
||||||
tween.set_parallel(true)
|
_animate_coin()
|
||||||
|
|
||||||
# Move upward
|
# Show text/graphic for display_duration, then fade out over fade_duration
|
||||||
|
# Wait for display duration (text/graphic stays visible)
|
||||||
|
await get_tree().create_timer(display_duration).timeout
|
||||||
|
|
||||||
|
# Then fade out over fade_duration
|
||||||
|
var fade_tween = create_tween()
|
||||||
|
fade_tween.set_parallel(true)
|
||||||
|
|
||||||
|
# Move upward while fading
|
||||||
var start_pos = global_position
|
var start_pos = global_position
|
||||||
var end_pos = start_pos + Vector2(0, -rise_distance)
|
var end_pos = start_pos + Vector2(0, -rise_distance)
|
||||||
tween.tween_property(self, "global_position", end_pos, duration)
|
fade_tween.tween_property(self, "global_position", end_pos, fade_duration)
|
||||||
|
|
||||||
# Fade out
|
# Fade out label
|
||||||
tween.tween_property(label, "modulate:a", 0.0, duration)
|
if label:
|
||||||
|
fade_tween.tween_property(label, "modulate:a", 0.0, fade_duration)
|
||||||
|
|
||||||
# Remove after animation
|
# Fade out sprite
|
||||||
tween.tween_callback(queue_free).set_delay(duration)
|
if item_sprite and item_sprite.visible:
|
||||||
|
fade_tween.tween_property(item_sprite, "modulate:a", 0.0, fade_duration)
|
||||||
|
|
||||||
|
# Remove after fade animation completes
|
||||||
|
fade_tween.tween_callback(queue_free).set_delay(fade_duration)
|
||||||
|
|
||||||
|
func _animate_coin():
|
||||||
|
# Animate coin rotation during display (similar to loot coin animation)
|
||||||
|
if not is_coin or not item_sprite or not item_sprite.visible:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use _process to animate coin frames continuously
|
||||||
|
# We'll animate at 10 frames per second during the display
|
||||||
|
var coin_anim_speed = 10.0 # Frames per second
|
||||||
|
var coin_frame_time = 1.0 / coin_anim_speed # Time per frame
|
||||||
|
|
||||||
|
# Create a tween that cycles through frames
|
||||||
|
var total_time = display_duration + fade_duration # Total display time
|
||||||
|
var frames_to_cycle = int(total_time * coin_anim_speed)
|
||||||
|
|
||||||
|
# Animate coin frames
|
||||||
|
for i in range(frames_to_cycle):
|
||||||
|
var target_frame = i % item_sprite.hframes # Cycle through 0-5
|
||||||
|
await get_tree().create_timer(coin_frame_time).timeout
|
||||||
|
if item_sprite and is_instance_valid(item_sprite) and item_sprite.visible:
|
||||||
|
item_sprite.frame = target_frame
|
||||||
|
|
||||||
|
|||||||
256
src/scripts/floor_switch.gd
Normal file
256
src/scripts/floor_switch.gd
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
extends Area2D
|
||||||
|
|
||||||
|
# Floor Switch - Activates when enough weight is placed on it
|
||||||
|
|
||||||
|
@export_enum("walk", "pillar") var switch_type: String = "walk" # "walk" = walk-on switch (weight 1), "pillar" = requires pillar (weight 5)
|
||||||
|
@export var required_weight: float = 1.0 # Required weight to activate (automatically set based on switch_type)
|
||||||
|
|
||||||
|
var is_activated: bool = false
|
||||||
|
var current_weight: float = 0.0
|
||||||
|
var objects_on_switch: Array = [] # Track objects currently on the switch
|
||||||
|
|
||||||
|
var tilemap_layer: TileMapLayer = null
|
||||||
|
var switch_tile_position: Vector2i = Vector2i.ZERO # Tile position in the tilemap
|
||||||
|
var check_timer: float = 0.0 # Timer for periodic checks (pillar switches only)
|
||||||
|
var check_interval: float = 0.2 # Check every 0.2 seconds
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Set required weight based on switch type
|
||||||
|
if switch_type == "walk":
|
||||||
|
required_weight = 1.0 # Player weight only
|
||||||
|
elif switch_type == "pillar":
|
||||||
|
required_weight = 5.0 # Requires pillar (weight 5)
|
||||||
|
|
||||||
|
# Set collision mask to detect players and objects
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 1 | 2 # Detect players (layer 1) and objects (layer 2)
|
||||||
|
|
||||||
|
# Connect signals
|
||||||
|
body_entered.connect(_on_body_entered)
|
||||||
|
body_exited.connect(_on_body_exited)
|
||||||
|
|
||||||
|
# Find tilemap layer to update switch visual (deferred to ensure game_world is ready)
|
||||||
|
call_deferred("_find_tilemap_layer")
|
||||||
|
call_deferred("_update_visual")
|
||||||
|
|
||||||
|
# For pillar switches, enable _process to periodically check if pillars are still not being held
|
||||||
|
# This handles the case where a player picks up a pillar that's already on the switch
|
||||||
|
# Walk switches don't need this check, so _process is disabled by default
|
||||||
|
if switch_type == "pillar":
|
||||||
|
set_process(true)
|
||||||
|
else:
|
||||||
|
set_process(false)
|
||||||
|
|
||||||
|
func _find_tilemap_layer():
|
||||||
|
# Find tilemap layer to update switch visual
|
||||||
|
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
|
||||||
|
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")
|
||||||
|
|
||||||
|
func _on_body_entered(body):
|
||||||
|
# Object entered the switch
|
||||||
|
# CRITICAL: For pillar switches, only count pillars (not being held), ignore players
|
||||||
|
# For walk switches, count players and any objects they're carrying
|
||||||
|
if switch_type == "pillar":
|
||||||
|
# Pillar switch: Only count pillars that are NOT being held
|
||||||
|
if body.has_method("can_be_grabbed") and body.can_be_grabbed():
|
||||||
|
# Check if object is a pillar and not being held
|
||||||
|
var object_type = body.get("object_type") if "object_type" in body else ""
|
||||||
|
var is_being_held = body.get("is_being_held") if "is_being_held" in body else false
|
||||||
|
|
||||||
|
# Only count pillars that are placed (not being held)
|
||||||
|
if object_type == "Pillar" and not is_being_held:
|
||||||
|
var weight = _get_object_weight(body)
|
||||||
|
if weight >= required_weight: # Pillar must have weight >= 5.0
|
||||||
|
objects_on_switch.append(body)
|
||||||
|
current_weight += weight
|
||||||
|
print("FloorSwitch: Pillar entered switch (not held), weight: ", weight, ", total: ", current_weight)
|
||||||
|
# Enable _process to periodically check if pillar is picked up
|
||||||
|
if not is_processing():
|
||||||
|
set_process(true)
|
||||||
|
_check_activation()
|
||||||
|
# Ignore players completely for pillar switches
|
||||||
|
else:
|
||||||
|
# Walk switch: Count players and any objects they're carrying
|
||||||
|
if body.is_in_group("player") or (body.has_method("can_be_grabbed") and body.can_be_grabbed()):
|
||||||
|
var weight = _get_object_weight(body)
|
||||||
|
if weight > 0:
|
||||||
|
objects_on_switch.append(body)
|
||||||
|
current_weight += weight
|
||||||
|
_check_activation()
|
||||||
|
|
||||||
|
func _on_body_exited(body):
|
||||||
|
# Object left the switch
|
||||||
|
if body in objects_on_switch:
|
||||||
|
# For pillar switches, verify the object is still valid (not being held now)
|
||||||
|
if switch_type == "pillar":
|
||||||
|
var object_type = body.get("object_type") if "object_type" in body else ""
|
||||||
|
var is_being_held = body.get("is_being_held") if "is_being_held" in body else false
|
||||||
|
|
||||||
|
# Only remove if it was a pillar (and might now be held)
|
||||||
|
if object_type == "Pillar":
|
||||||
|
var weight = _get_object_weight(body)
|
||||||
|
if weight > 0:
|
||||||
|
objects_on_switch.erase(body)
|
||||||
|
current_weight -= weight
|
||||||
|
print("FloorSwitch: Pillar left switch, weight: ", weight, ", total: ", current_weight)
|
||||||
|
_check_activation()
|
||||||
|
else:
|
||||||
|
# Walk switch: Remove any object
|
||||||
|
var weight = _get_object_weight(body)
|
||||||
|
if weight > 0:
|
||||||
|
objects_on_switch.erase(body)
|
||||||
|
current_weight -= weight
|
||||||
|
_check_activation()
|
||||||
|
|
||||||
|
func _get_object_weight(body: Node) -> float:
|
||||||
|
# Get weight of an object
|
||||||
|
# CRITICAL: For pillar switches, this function should NOT be called for players
|
||||||
|
# (they are filtered out in _on_body_entered)
|
||||||
|
if body.is_in_group("player"):
|
||||||
|
# Player base weight = 1
|
||||||
|
var weight = 1.0
|
||||||
|
# If player is carrying another player or object, add their weight
|
||||||
|
# held_object is a property in player.gd, so access directly
|
||||||
|
# NOTE: For pillar switches, players should never activate, so held objects don't matter
|
||||||
|
if body.held_object:
|
||||||
|
if body.held_object.is_in_group("player"):
|
||||||
|
weight += 1.0
|
||||||
|
else:
|
||||||
|
# Check if held object has weight property (for interactable objects)
|
||||||
|
# But only for walk switches - pillar switches don't count held objects
|
||||||
|
if switch_type == "walk":
|
||||||
|
var held_weight = _get_object_weight(body.held_object)
|
||||||
|
if held_weight > 0:
|
||||||
|
weight += held_weight
|
||||||
|
return weight
|
||||||
|
else:
|
||||||
|
# For interactable objects, weight is an exported variable
|
||||||
|
# Try to access it directly - if it doesn't exist, will return 0
|
||||||
|
var weight_value = body.get("weight")
|
||||||
|
if weight_value != null:
|
||||||
|
return weight_value as float
|
||||||
|
# Fallback: check if object has get_weight method
|
||||||
|
if body.has_method("get_weight"):
|
||||||
|
return body.get_weight()
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
func _check_activation():
|
||||||
|
# CRITICAL: For pillar switches, verify that objects on switch are NOT being held
|
||||||
|
# If any pillar on switch is now being held, remove it from weight calculation
|
||||||
|
if switch_type == "pillar":
|
||||||
|
var valid_objects = []
|
||||||
|
var new_weight = 0.0
|
||||||
|
|
||||||
|
for obj in objects_on_switch:
|
||||||
|
if not is_instance_valid(obj):
|
||||||
|
continue
|
||||||
|
|
||||||
|
var object_type = obj.get("object_type") if "object_type" in obj else ""
|
||||||
|
var is_being_held = obj.get("is_being_held") if "is_being_held" in obj else false
|
||||||
|
|
||||||
|
# Only count pillars that are NOT being held
|
||||||
|
if object_type == "Pillar" and not is_being_held:
|
||||||
|
var weight = _get_object_weight(obj)
|
||||||
|
if weight >= required_weight:
|
||||||
|
valid_objects.append(obj)
|
||||||
|
new_weight += weight
|
||||||
|
else:
|
||||||
|
# Object is being held or not a pillar - remove it
|
||||||
|
print("FloorSwitch: Removing held/invalid object from switch: ", obj.name if obj else "null")
|
||||||
|
|
||||||
|
# Update objects list and weight
|
||||||
|
objects_on_switch = valid_objects
|
||||||
|
current_weight = new_weight
|
||||||
|
|
||||||
|
# Check if switch should be activated
|
||||||
|
var should_activate = current_weight >= required_weight
|
||||||
|
|
||||||
|
if should_activate != is_activated:
|
||||||
|
is_activated = should_activate
|
||||||
|
_update_visual()
|
||||||
|
|
||||||
|
# Notify connected doors
|
||||||
|
_notify_doors()
|
||||||
|
|
||||||
|
func _update_visual():
|
||||||
|
# Update tile visual to show activated/inactive state
|
||||||
|
if not tilemap_layer:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Choose tiles based on switch type
|
||||||
|
var tile_coord: Vector2i
|
||||||
|
if switch_type == "walk":
|
||||||
|
# Walk-on switch: 11,9 (inactive) and 12,9 (active)
|
||||||
|
tile_coord = Vector2i(12, 9) if is_activated else Vector2i(11, 9)
|
||||||
|
elif switch_type == "pillar":
|
||||||
|
# Pillar switch: 16,9 (inactive) and 17,9 (active)
|
||||||
|
tile_coord = Vector2i(17, 9) if is_activated else Vector2i(16, 9)
|
||||||
|
else:
|
||||||
|
# Fallback to walk-on switch
|
||||||
|
tile_coord = Vector2i(12, 9) if is_activated else Vector2i(11, 9)
|
||||||
|
|
||||||
|
tilemap_layer.set_cell(switch_tile_position, 0, tile_coord)
|
||||||
|
|
||||||
|
func _process(delta):
|
||||||
|
# For pillar switches, periodically check if pillars on switch are still not being held
|
||||||
|
# This handles the case where a player picks up a pillar that's already on the switch
|
||||||
|
# Only check if there are objects on the switch (optimization)
|
||||||
|
if objects_on_switch.size() > 0:
|
||||||
|
check_timer += delta
|
||||||
|
if check_timer >= check_interval:
|
||||||
|
check_timer = 0.0
|
||||||
|
_check_activation()
|
||||||
|
else:
|
||||||
|
# No objects on switch - disable _process to save performance
|
||||||
|
# It will be re-enabled when an object enters the switch
|
||||||
|
set_process(false)
|
||||||
|
|
||||||
|
func _notify_doors():
|
||||||
|
# Notify only doors that are explicitly connected to this switch
|
||||||
|
# CRITICAL: Only notify doors that have this switch in their connected_switches list
|
||||||
|
# Do NOT use position-based checks - that causes cross-room door triggering!
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if not game_world:
|
||||||
|
return
|
||||||
|
|
||||||
|
var entities_node = game_world.get_node_or_null("Entities")
|
||||||
|
if not entities_node:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get this switch's room for verification
|
||||||
|
var switch_room = get_meta("switch_room") if has_meta("switch_room") else {}
|
||||||
|
|
||||||
|
# Find all blocking doors and check if they are connected to this switch
|
||||||
|
for child in entities_node.get_children():
|
||||||
|
if child.is_in_group("blocking_door") or child.name.begins_with("BlockingDoor_"):
|
||||||
|
if not is_instance_valid(child):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# CRITICAL SAFETY CHECK: Verify door is in the same room as this switch
|
||||||
|
# Only notify doors that are in the SAME room as the switch
|
||||||
|
var door_blocking_room = child.blocking_room if "blocking_room" in child else {}
|
||||||
|
var rooms_match = false
|
||||||
|
if switch_room and not switch_room.is_empty() and door_blocking_room and not door_blocking_room.is_empty():
|
||||||
|
rooms_match = (switch_room.x == door_blocking_room.x and \
|
||||||
|
switch_room.y == door_blocking_room.y and \
|
||||||
|
switch_room.w == door_blocking_room.w and \
|
||||||
|
switch_room.h == door_blocking_room.h)
|
||||||
|
|
||||||
|
# CRITICAL: Only notify doors that have this switch in their connected_switches list
|
||||||
|
# AND verify rooms match (double-check to prevent cross-room triggering)
|
||||||
|
if child.connected_switches.has(self):
|
||||||
|
if rooms_match:
|
||||||
|
# This switch is explicitly connected to this door AND in the same room - trigger puzzle check
|
||||||
|
if child.has_method("_check_puzzle_state"):
|
||||||
|
var door_room_info = "room:(" + str(door_blocking_room.get("x", "?")) + "," + str(door_blocking_room.get("y", "?")) + ")" if door_blocking_room and not door_blocking_room.is_empty() else "no room"
|
||||||
|
print("FloorSwitch: Notifying door ", child.name, " (", door_room_info, ") that switch ", name, " (room: ", switch_room.get("x", "?") if switch_room and not switch_room.is_empty() else "?", ",", switch_room.get("y", "?") if switch_room and not switch_room.is_empty() else "?", ") state changed to ", is_activated)
|
||||||
|
child._check_puzzle_state()
|
||||||
|
else:
|
||||||
|
push_error("FloorSwitch: ERROR - Switch ", name, " is connected to door ", child.name, " but rooms don't match! Switch room: (", switch_room.get("x", "?") if switch_room and not switch_room.is_empty() else "?", ",", switch_room.get("y", "?") if switch_room and not switch_room.is_empty() else "?", "), Door room: (", door_blocking_room.get("x", "?") if door_blocking_room and not door_blocking_room.is_empty() else "?", ",", door_blocking_room.get("y", "?") if door_blocking_room and not door_blocking_room.is_empty() else "?", ") - NOT notifying!")
|
||||||
1
src/scripts/floor_switch.gd.uid
Normal file
1
src/scripts/floor_switch.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dfigqc0flmid5
|
||||||
@@ -22,7 +22,7 @@ var level_exp_collected: float = 0.0
|
|||||||
var level_coins_collected: int = 0
|
var level_coins_collected: int = 0
|
||||||
|
|
||||||
# Client ready tracking (server only)
|
# Client ready tracking (server only)
|
||||||
var clients_ready: Dictionary = {} # peer_id -> bool
|
var clients_ready: Dictionary = {} # peer_id -> bool
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
# Add to group for easy access
|
# Add to group for easy access
|
||||||
@@ -222,7 +222,7 @@ func _sync_enemy_position(enemy_name: String, enemy_index: int, pos: Vector2, ve
|
|||||||
# Clients receive enemy position updates from server
|
# Clients receive enemy position updates from server
|
||||||
# Find the enemy by name or index
|
# Find the enemy by name or index
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
return # Server ignores this (it's the sender)
|
return # Server ignores this (it's the sender)
|
||||||
|
|
||||||
var entities_node = get_node_or_null("Entities")
|
var entities_node = get_node_or_null("Entities")
|
||||||
if not entities_node:
|
if not entities_node:
|
||||||
@@ -248,7 +248,7 @@ func _sync_enemy_death(enemy_name: String, enemy_index: int):
|
|||||||
# Clients receive enemy death sync from server
|
# Clients receive enemy death sync from server
|
||||||
# Find the enemy by name or index
|
# Find the enemy by name or index
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
return # Server ignores this (it's the sender)
|
return # Server ignores this (it's the sender)
|
||||||
|
|
||||||
var entities_node = get_node_or_null("Entities")
|
var entities_node = get_node_or_null("Entities")
|
||||||
if not entities_node:
|
if not entities_node:
|
||||||
@@ -278,7 +278,7 @@ func _sync_enemy_damage_visual(enemy_name: String, enemy_index: int):
|
|||||||
# Clients receive enemy damage visual sync from server
|
# Clients receive enemy damage visual sync from server
|
||||||
# Find the enemy by name or index
|
# Find the enemy by name or index
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
return # Server ignores this (it's the sender)
|
return # Server ignores this (it's the sender)
|
||||||
|
|
||||||
var entities_node = get_node_or_null("Entities")
|
var entities_node = get_node_or_null("Entities")
|
||||||
if not entities_node:
|
if not entities_node:
|
||||||
@@ -337,7 +337,7 @@ func _request_loot_pickup(loot_id: int, loot_position: Vector2, player_peer_id:
|
|||||||
func _sync_show_level_complete(enemies_defeated: int, times_downed: int, exp_collected: float, coins_collected: int):
|
func _sync_show_level_complete(enemies_defeated: int, times_downed: int, exp_collected: float, coins_collected: int):
|
||||||
# Clients receive level complete UI sync from server
|
# Clients receive level complete UI sync from server
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
return # Server ignores this (it's the sender)
|
return # Server ignores this (it's the sender)
|
||||||
|
|
||||||
# Update stats before showing
|
# Update stats before showing
|
||||||
level_enemies_defeated = enemies_defeated
|
level_enemies_defeated = enemies_defeated
|
||||||
@@ -352,7 +352,7 @@ func _sync_show_level_complete(enemies_defeated: int, times_downed: int, exp_col
|
|||||||
func _sync_hide_level_complete():
|
func _sync_hide_level_complete():
|
||||||
# Clients receive hide level complete UI sync from server
|
# Clients receive hide level complete UI sync from server
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
return # Server ignores this (it's the sender)
|
return # Server ignores this (it's the sender)
|
||||||
|
|
||||||
var level_complete_ui = get_node_or_null("LevelCompleteUI")
|
var level_complete_ui = get_node_or_null("LevelCompleteUI")
|
||||||
if level_complete_ui:
|
if level_complete_ui:
|
||||||
@@ -362,7 +362,7 @@ func _sync_hide_level_complete():
|
|||||||
func _sync_show_level_number(level: int):
|
func _sync_show_level_number(level: int):
|
||||||
# Clients receive level number UI sync from server
|
# Clients receive level number UI sync from server
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
return # Server ignores this (it's the sender)
|
return # Server ignores this (it's the sender)
|
||||||
|
|
||||||
current_level = level
|
current_level = level
|
||||||
_show_level_number()
|
_show_level_number()
|
||||||
@@ -372,7 +372,7 @@ func _sync_loot_remove(loot_id: int, loot_position: Vector2):
|
|||||||
# Clients receive loot removal sync from server
|
# Clients receive loot removal sync from server
|
||||||
# Find the loot by ID or position
|
# Find the loot by ID or position
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
return # Server ignores this (it's the sender)
|
return # Server ignores this (it's the sender)
|
||||||
|
|
||||||
var entities_node = get_node_or_null("Entities")
|
var entities_node = get_node_or_null("Entities")
|
||||||
if not entities_node:
|
if not entities_node:
|
||||||
@@ -469,6 +469,12 @@ func _generate_dungeon():
|
|||||||
# Spawn interactable objects
|
# Spawn interactable objects
|
||||||
_spawn_interactable_objects()
|
_spawn_interactable_objects()
|
||||||
|
|
||||||
|
# Spawn blocking doors
|
||||||
|
_spawn_blocking_doors()
|
||||||
|
|
||||||
|
# Spawn room triggers
|
||||||
|
_spawn_room_triggers()
|
||||||
|
|
||||||
# Wait a frame to ensure enemies and objects are properly in scene tree before syncing
|
# Wait a frame to ensure enemies and objects are properly in scene tree before syncing
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
|
|
||||||
@@ -483,7 +489,7 @@ func _generate_dungeon():
|
|||||||
_move_all_players_to_start_room()
|
_move_all_players_to_start_room()
|
||||||
|
|
||||||
# Update camera immediately to ensure it's looking at the players
|
# Update camera immediately to ensure it's looking at the players
|
||||||
await get_tree().process_frame # Wait a frame for players to be fully in scene tree
|
await get_tree().process_frame # Wait a frame for players to be fully in scene tree
|
||||||
_update_camera()
|
_update_camera()
|
||||||
|
|
||||||
# Show level number (for initial level generation only - not when called from level completion)
|
# Show level number (for initial level generation only - not when called from level completion)
|
||||||
@@ -815,7 +821,7 @@ func _is_safe_spawn_position(world_pos: Vector2) -> bool:
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
# Check if it's a floor tile
|
# Check if it's a floor tile
|
||||||
if grid[tile_x][tile_y] == 1: # Floor
|
if grid[tile_x][tile_y] == 1: # Floor
|
||||||
return true
|
return true
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -824,14 +830,13 @@ func _find_nearby_safe_spawn_position(world_pos: Vector2, max_distance: float =
|
|||||||
# Find a nearby safe spawn position (on a floor tile)
|
# Find a nearby safe spawn position (on a floor tile)
|
||||||
# Returns the original position if it's safe, otherwise finds the nearest safe position
|
# Returns the original position if it's safe, otherwise finds the nearest safe position
|
||||||
# max_distance: Maximum distance to search for a safe position
|
# max_distance: Maximum distance to search for a safe position
|
||||||
|
|
||||||
# First check if the original position is safe
|
# First check if the original position is safe
|
||||||
if _is_safe_spawn_position(world_pos):
|
if _is_safe_spawn_position(world_pos):
|
||||||
return world_pos
|
return world_pos
|
||||||
|
|
||||||
# Search in expanding circles around the position
|
# Search in expanding circles around the position
|
||||||
var tile_size = 16
|
var tile_size = 16
|
||||||
var search_radius = 1 # Start with 1 tile radius
|
var search_radius = 1 # Start with 1 tile radius
|
||||||
var max_radius = int(max_distance / tile_size) + 1
|
var max_radius = int(max_distance / tile_size) + 1
|
||||||
|
|
||||||
while search_radius <= max_radius:
|
while search_radius <= max_radius:
|
||||||
@@ -898,7 +903,7 @@ func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, h
|
|||||||
|
|
||||||
dungeon_data = dungeon_data_sync
|
dungeon_data = dungeon_data_sync
|
||||||
dungeon_seed = seed_value
|
dungeon_seed = seed_value
|
||||||
current_level = level # Update current_level FIRST before showing level number
|
current_level = level # Update current_level FIRST before showing level number
|
||||||
print("GameWorld: Client updated current_level to ", current_level, " from sync")
|
print("GameWorld: Client updated current_level to ", current_level, " from sync")
|
||||||
|
|
||||||
# Clear previous level on client
|
# Clear previous level on client
|
||||||
@@ -906,7 +911,7 @@ func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, h
|
|||||||
|
|
||||||
# Wait for old entities to be fully freed before spawning new ones
|
# Wait for old entities to be fully freed before spawning new ones
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete
|
await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete
|
||||||
|
|
||||||
# Render dungeon on client
|
# Render dungeon on client
|
||||||
_render_dungeon()
|
_render_dungeon()
|
||||||
@@ -920,9 +925,15 @@ func _sync_dungeon(dungeon_data_sync: Dictionary, seed_value: int, level: int, h
|
|||||||
# Spawn interactable objects on client
|
# Spawn interactable objects on client
|
||||||
_spawn_interactable_objects()
|
_spawn_interactable_objects()
|
||||||
|
|
||||||
|
# Spawn blocking doors on client
|
||||||
|
_spawn_blocking_doors()
|
||||||
|
|
||||||
|
# Spawn room triggers on client
|
||||||
|
_spawn_room_triggers()
|
||||||
|
|
||||||
# Wait a frame to ensure all enemies and objects are properly added to scene tree and initialized
|
# Wait a frame to ensure all enemies and objects are properly added to scene tree and initialized
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
await get_tree().process_frame # Wait an extra frame to ensure enemies are fully ready
|
await get_tree().process_frame # Wait an extra frame to ensure enemies are fully ready
|
||||||
|
|
||||||
# Update spawn points - use host's room if available, otherwise use start room
|
# Update spawn points - use host's room if available, otherwise use start room
|
||||||
if not host_room.is_empty():
|
if not host_room.is_empty():
|
||||||
@@ -1072,10 +1083,20 @@ func _spawn_enemies():
|
|||||||
if "damage" in enemy_data:
|
if "damage" in enemy_data:
|
||||||
enemy.damage = enemy_data.damage
|
enemy.damage = enemy_data.damage
|
||||||
|
|
||||||
|
# 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
|
# Add to scene tree AFTER setting authority and stats
|
||||||
entities_node.add_child(enemy)
|
entities_node.add_child(enemy)
|
||||||
enemy.global_position = enemy_data.position
|
enemy.global_position = enemy_data.position
|
||||||
|
|
||||||
|
# CRITICAL: Re-verify collision_mask AFTER adding to scene (in case scene file overrides it)
|
||||||
|
# This ensures enemies always collide with walls (layer 7 = bit 6 = 64)
|
||||||
|
if enemy.collision_mask != (1 | 2 | 64):
|
||||||
|
print("GameWorld: WARNING - Enemy ", enemy.name, " collision_mask was ", enemy.collision_mask, ", expected ", (1 | 2 | 64), "! Correcting...")
|
||||||
|
enemy.collision_mask = 1 | 2 | 64
|
||||||
|
|
||||||
# Verify authority is still set after adding to tree
|
# Verify authority is still set after adding to tree
|
||||||
if multiplayer.has_multiplayer_peer():
|
if multiplayer.has_multiplayer_peer():
|
||||||
var auth_after = enemy.get_multiplayer_authority()
|
var auth_after = enemy.get_multiplayer_authority()
|
||||||
@@ -1228,7 +1249,7 @@ func _sync_dungeon_enemy_spawn(enemy_data: Dictionary):
|
|||||||
if child.is_in_group("enemy") and child.has_meta("dungeon_spawned"):
|
if child.is_in_group("enemy") and child.has_meta("dungeon_spawned"):
|
||||||
# Check if it's a duplicate by position
|
# Check if it's a duplicate by position
|
||||||
var child_pos = child.global_position
|
var child_pos = child.global_position
|
||||||
if child_pos.distance_to(enemy_data.position) < 1.0: # Same position
|
if child_pos.distance_to(enemy_data.position) < 1.0: # Same position
|
||||||
# Also check if it's dead - if so, remove it first
|
# Also check if it's dead - if so, remove it first
|
||||||
if "is_dead" in child and child.is_dead:
|
if "is_dead" in child and child.is_dead:
|
||||||
print("GameWorld: Removing dead duplicate enemy at ", enemy_data.position)
|
print("GameWorld: Removing dead duplicate enemy at ", enemy_data.position)
|
||||||
@@ -1275,10 +1296,20 @@ func _sync_dungeon_enemy_spawn(enemy_data: Dictionary):
|
|||||||
if "damage" in enemy_data:
|
if "damage" in enemy_data:
|
||||||
enemy.damage = enemy_data.damage
|
enemy.damage = enemy_data.damage
|
||||||
|
|
||||||
|
# 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
|
# Add to scene tree AFTER setting authority and stats
|
||||||
entities_node.add_child(enemy)
|
entities_node.add_child(enemy)
|
||||||
enemy.global_position = enemy_data.position
|
enemy.global_position = enemy_data.position
|
||||||
|
|
||||||
|
# CRITICAL: Re-verify collision_mask AFTER adding to scene (in case scene file overrides it)
|
||||||
|
# This ensures enemies always collide with walls (layer 7 = bit 6 = 64)
|
||||||
|
if enemy.collision_mask != (1 | 2 | 64):
|
||||||
|
print("GameWorld: WARNING - Enemy ", enemy.name, " collision_mask was ", enemy.collision_mask, ", expected ", (1 | 2 | 64), "! Correcting...")
|
||||||
|
enemy.collision_mask = 1 | 2 | 64
|
||||||
|
|
||||||
# Verify authority is still set
|
# Verify authority is still set
|
||||||
if multiplayer.has_multiplayer_peer():
|
if multiplayer.has_multiplayer_peer():
|
||||||
var auth_after = enemy.get_multiplayer_authority()
|
var auth_after = enemy.get_multiplayer_authority()
|
||||||
@@ -1338,12 +1369,12 @@ func _clear_level():
|
|||||||
# Free all entities immediately (not queue_free) to ensure they're gone before spawning new ones
|
# Free all entities immediately (not queue_free) to ensure they're gone before spawning new ones
|
||||||
for entity in entities_to_remove:
|
for entity in entities_to_remove:
|
||||||
if is_instance_valid(entity):
|
if is_instance_valid(entity):
|
||||||
entity.free() # Use free() instead of queue_free() for immediate removal
|
entity.free() # Use free() instead of queue_free() for immediate removal
|
||||||
|
|
||||||
# Remove stairs area
|
# Remove stairs area
|
||||||
var stairs_area = get_node_or_null("StairsArea")
|
var stairs_area = get_node_or_null("StairsArea")
|
||||||
if stairs_area:
|
if stairs_area:
|
||||||
stairs_area.free() # Use free() for immediate removal
|
stairs_area.free() # Use free() for immediate removal
|
||||||
|
|
||||||
# Clear dungeon data (but keep it for now until new one is generated)
|
# Clear dungeon data (but keep it for now until new one is generated)
|
||||||
# dungeon_data = {} # Don't clear yet, wait for new generation
|
# dungeon_data = {} # Don't clear yet, wait for new generation
|
||||||
@@ -1394,7 +1425,7 @@ func _create_stairs_area():
|
|||||||
|
|
||||||
# Set collision layer/mask BEFORE adding to scene
|
# Set collision layer/mask BEFORE adding to scene
|
||||||
stairs_area.collision_layer = 0
|
stairs_area.collision_layer = 0
|
||||||
stairs_area.collision_mask = 1 # Detect players (layer 1)
|
stairs_area.collision_mask = 1 # Detect players (layer 1)
|
||||||
|
|
||||||
# Add script BEFORE adding to scene (so _ready() is called properly)
|
# Add script BEFORE adding to scene (so _ready() is called properly)
|
||||||
var stairs_script = load("res://scripts/stairs.gd")
|
var stairs_script = load("res://scripts/stairs.gd")
|
||||||
@@ -1419,7 +1450,7 @@ func _create_stairs_area():
|
|||||||
func _on_player_reached_stairs(player: Node):
|
func _on_player_reached_stairs(player: Node):
|
||||||
# Player reached stairs - trigger level complete
|
# Player reached stairs - trigger level complete
|
||||||
if not multiplayer.is_server() and multiplayer.has_multiplayer_peer():
|
if not multiplayer.is_server() and multiplayer.has_multiplayer_peer():
|
||||||
return # Only server handles this
|
return # Only server handles this
|
||||||
|
|
||||||
print("GameWorld: Player ", player.name, " reached stairs!")
|
print("GameWorld: Player ", player.name, " reached stairs!")
|
||||||
|
|
||||||
@@ -1443,7 +1474,7 @@ func _on_player_reached_stairs(player: Node):
|
|||||||
_sync_show_level_complete.rpc(level_enemies_defeated, level_times_downed, level_exp_collected, level_coins_collected)
|
_sync_show_level_complete.rpc(level_enemies_defeated, level_times_downed, level_exp_collected, level_coins_collected)
|
||||||
|
|
||||||
# After delay, hide UI and generate new level
|
# After delay, hide UI and generate new level
|
||||||
await get_tree().create_timer(5.0).timeout # Show stats for 5 seconds
|
await get_tree().create_timer(5.0).timeout # Show stats for 5 seconds
|
||||||
|
|
||||||
# Hide level complete UI (server and clients)
|
# Hide level complete UI (server and clients)
|
||||||
var level_complete_ui = get_node_or_null("LevelCompleteUI")
|
var level_complete_ui = get_node_or_null("LevelCompleteUI")
|
||||||
@@ -1458,7 +1489,7 @@ func _on_player_reached_stairs(player: Node):
|
|||||||
|
|
||||||
# Wait for old entities to be fully freed before generating new level
|
# Wait for old entities to be fully freed before generating new level
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete
|
await get_tree().process_frame # Wait an extra frame to ensure cleanup is complete
|
||||||
|
|
||||||
# Generate next level
|
# Generate next level
|
||||||
current_level += 1
|
current_level += 1
|
||||||
@@ -1471,7 +1502,7 @@ func _on_player_reached_stairs(player: Node):
|
|||||||
# We need to wait for all the async operations in _generate_dungeon() to finish
|
# We need to wait for all the async operations in _generate_dungeon() to finish
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
await get_tree().process_frame # Extra frame to ensure everything is done
|
await get_tree().process_frame # Extra frame to ensure everything is done
|
||||||
|
|
||||||
# Verify current_level is still correct
|
# Verify current_level is still correct
|
||||||
print("GameWorld: After dungeon generation, current_level = ", current_level)
|
print("GameWorld: After dungeon generation, current_level = ", current_level)
|
||||||
@@ -1493,7 +1524,7 @@ func _on_player_reached_stairs(player: Node):
|
|||||||
# Sync new level to all clients - use start room since all players should be there
|
# Sync new level to all clients - use start room since all players should be there
|
||||||
# IMPORTANT: Wait multiple frames to ensure dungeon generation and enemy spawning is complete before syncing
|
# IMPORTANT: Wait multiple frames to ensure dungeon generation and enemy spawning is complete before syncing
|
||||||
await get_tree().process_frame
|
await get_tree().process_frame
|
||||||
await get_tree().process_frame # Wait extra frames to ensure enemies are fully initialized
|
await get_tree().process_frame # Wait extra frames to ensure enemies are fully initialized
|
||||||
|
|
||||||
if multiplayer.has_multiplayer_peer():
|
if multiplayer.has_multiplayer_peer():
|
||||||
var start_room = dungeon_data.start_room if not dungeon_data.is_empty() and dungeon_data.has("start_room") else {}
|
var start_room = dungeon_data.start_room if not dungeon_data.is_empty() and dungeon_data.has("start_room") else {}
|
||||||
@@ -1603,21 +1634,27 @@ func _fade_in_player(player: Node):
|
|||||||
|
|
||||||
for sprite_layer in sprite_layers:
|
for sprite_layer in sprite_layers:
|
||||||
if sprite_layer:
|
if sprite_layer:
|
||||||
sprite_layer.modulate.a = 0.0 # Start invisible
|
sprite_layer.modulate.a = 0.0 # Start invisible
|
||||||
fade_tween.tween_property(sprite_layer, "modulate:a", 1.0, 1.0)
|
fade_tween.tween_property(sprite_layer, "modulate:a", 1.0, 1.0)
|
||||||
|
|
||||||
func _show_level_complete_ui():
|
func _show_level_complete_ui():
|
||||||
# Create or show level complete UI
|
# Create or show level complete UI
|
||||||
var level_complete_ui = get_node_or_null("LevelCompleteUI")
|
var level_complete_ui = get_node_or_null("LevelCompleteUI")
|
||||||
if not level_complete_ui:
|
if not level_complete_ui:
|
||||||
# Try to load scene, but fall back to programmatic creation if it doesn't exist
|
# Try to load scene if it exists, but fall back to programmatic creation if it doesn't
|
||||||
var level_complete_scene = load("res://scenes/level_complete_ui.tscn")
|
var scene_path = "res://scenes/level_complete_ui.tscn"
|
||||||
if level_complete_scene:
|
if ResourceLoader.exists(scene_path):
|
||||||
level_complete_ui = level_complete_scene.instantiate()
|
var level_complete_scene = load(scene_path)
|
||||||
level_complete_ui.name = "LevelCompleteUI"
|
if level_complete_scene:
|
||||||
add_child(level_complete_ui)
|
level_complete_ui = level_complete_scene.instantiate()
|
||||||
|
level_complete_ui.name = "LevelCompleteUI"
|
||||||
|
add_child(level_complete_ui)
|
||||||
|
else:
|
||||||
|
# Scene file exists but failed to load - fall back to programmatic creation
|
||||||
|
print("GameWorld: Warning - level_complete_ui.tscn exists but failed to load, creating programmatically")
|
||||||
|
level_complete_ui = _create_level_complete_ui_programmatically()
|
||||||
else:
|
else:
|
||||||
# Create UI programmatically if scene doesn't exist
|
# Scene file doesn't exist - create UI programmatically (expected behavior)
|
||||||
level_complete_ui = _create_level_complete_ui_programmatically()
|
level_complete_ui = _create_level_complete_ui_programmatically()
|
||||||
|
|
||||||
if level_complete_ui:
|
if level_complete_ui:
|
||||||
@@ -1634,14 +1671,20 @@ func _show_level_number():
|
|||||||
print("GameWorld: _show_level_number() called with current_level = ", current_level)
|
print("GameWorld: _show_level_number() called with current_level = ", current_level)
|
||||||
var level_text_ui = get_node_or_null("LevelTextUI")
|
var level_text_ui = get_node_or_null("LevelTextUI")
|
||||||
if not level_text_ui:
|
if not level_text_ui:
|
||||||
# Try to load scene, but fall back to programmatic creation if it doesn't exist
|
# Try to load scene if it exists, but fall back to programmatic creation if it doesn't
|
||||||
var level_text_scene = load("res://scenes/level_text_ui.tscn")
|
var scene_path = "res://scenes/level_text_ui.tscn"
|
||||||
if level_text_scene:
|
if ResourceLoader.exists(scene_path):
|
||||||
level_text_ui = level_text_scene.instantiate()
|
var level_text_scene = load(scene_path)
|
||||||
level_text_ui.name = "LevelTextUI"
|
if level_text_scene:
|
||||||
add_child(level_text_ui)
|
level_text_ui = level_text_scene.instantiate()
|
||||||
|
level_text_ui.name = "LevelTextUI"
|
||||||
|
add_child(level_text_ui)
|
||||||
|
else:
|
||||||
|
# Scene file exists but failed to load - fall back to programmatic creation
|
||||||
|
print("GameWorld: Warning - level_text_ui.tscn exists but failed to load, creating programmatically")
|
||||||
|
level_text_ui = _create_level_text_ui_programmatically()
|
||||||
else:
|
else:
|
||||||
# Create UI programmatically if scene doesn't exist
|
# Scene file doesn't exist - create UI programmatically (expected behavior)
|
||||||
level_text_ui = _create_level_text_ui_programmatically()
|
level_text_ui = _create_level_text_ui_programmatically()
|
||||||
|
|
||||||
if level_text_ui:
|
if level_text_ui:
|
||||||
@@ -1664,7 +1707,7 @@ func _create_level_complete_ui_programmatically() -> Node:
|
|||||||
|
|
||||||
var vbox = VBoxContainer.new()
|
var vbox = VBoxContainer.new()
|
||||||
vbox.set_anchors_preset(Control.PRESET_CENTER)
|
vbox.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
vbox.offset_top = -200 # Position a bit up from center
|
vbox.offset_top = -200 # Position a bit up from center
|
||||||
canvas_layer.add_child(vbox)
|
canvas_layer.add_child(vbox)
|
||||||
|
|
||||||
# Title
|
# Title
|
||||||
@@ -1757,3 +1800,649 @@ func _move_players_to_host_room(host_room: Dictionary):
|
|||||||
player.position = new_pos
|
player.position = new_pos
|
||||||
print("GameWorld: Moved player ", player.name, " to ", new_pos)
|
print("GameWorld: Moved player ", player.name, " to ", new_pos)
|
||||||
spawn_index += 1
|
spawn_index += 1
|
||||||
|
|
||||||
|
func _spawn_blocking_doors():
|
||||||
|
# Spawn blocking doors from dungeon data
|
||||||
|
if dungeon_data.is_empty() or not dungeon_data.has("blocking_doors"):
|
||||||
|
return
|
||||||
|
|
||||||
|
var blocking_doors = dungeon_data.blocking_doors
|
||||||
|
if blocking_doors == null or not blocking_doors is Array:
|
||||||
|
return
|
||||||
|
|
||||||
|
var door_scene = preload("res://scenes/door.tscn")
|
||||||
|
if not door_scene:
|
||||||
|
push_error("ERROR: Could not load door scene!")
|
||||||
|
return
|
||||||
|
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if not entities_node:
|
||||||
|
push_error("ERROR: Could not find Entities node!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("GameWorld: Spawning ", blocking_doors.size(), " blocking doors")
|
||||||
|
|
||||||
|
for i in range(blocking_doors.size()):
|
||||||
|
var door_data = blocking_doors[i]
|
||||||
|
if not door_data is Dictionary:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var door = door_scene.instantiate()
|
||||||
|
door.name = "BlockingDoor_%d" % i
|
||||||
|
door.add_to_group("blocking_door")
|
||||||
|
|
||||||
|
# Set door properties BEFORE adding to scene (so _ready() has correct values)
|
||||||
|
door.type = door_data.type if "type" in door_data else "StoneDoor"
|
||||||
|
door.direction = door_data.direction if "direction" in door_data else "Up"
|
||||||
|
door.is_closed = door_data.is_closed if "is_closed" in door_data else true
|
||||||
|
|
||||||
|
# CRITICAL: Set puzzle requirements based on door_data
|
||||||
|
if "puzzle_type" in door_data:
|
||||||
|
if door_data.puzzle_type == "enemy":
|
||||||
|
door.requires_enemies = true
|
||||||
|
door.requires_switch = false
|
||||||
|
print("GameWorld: Door ", door.name, " requires enemies to open (puzzle_type: enemy)")
|
||||||
|
elif door_data.puzzle_type in ["switch_walk", "switch_pillar"]:
|
||||||
|
door.requires_enemies = false
|
||||||
|
door.requires_switch = true
|
||||||
|
print("GameWorld: Door ", door.name, " requires switch to open (puzzle_type: ", door_data.puzzle_type, ")")
|
||||||
|
door.blocking_room = door_data.blocking_room if "blocking_room" in door_data else {}
|
||||||
|
door.switch_room = door_data.switch_room if "switch_room" in door_data else {}
|
||||||
|
|
||||||
|
# CRITICAL: Verify door has blocking_room set - StoneDoor/GateDoor MUST be in a puzzle room
|
||||||
|
if (door_data.type == "StoneDoor" or door_data.type == "GateDoor"):
|
||||||
|
if not "blocking_room" in door_data or door_data.blocking_room.is_empty():
|
||||||
|
push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no blocking_room! This door should not exist! Removing it.")
|
||||||
|
door.queue_free()
|
||||||
|
continue
|
||||||
|
|
||||||
|
# CRITICAL: Verify door has puzzle_type - StoneDoor/GateDoor MUST have a puzzle
|
||||||
|
if not "puzzle_type" in door_data:
|
||||||
|
push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no puzzle_type! This door should not exist! Removing it.")
|
||||||
|
door.queue_free()
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("GameWorld: Creating blocking door ", door.name, " (", door_data.type, ") for puzzle room (", door_data.blocking_room.x, ", ", door_data.blocking_room.y, "), puzzle_type: ", door_data.puzzle_type)
|
||||||
|
|
||||||
|
# CRITICAL: Store original door connection info from door_data.door
|
||||||
|
# For blocking doors: room1 = puzzle room (where door is IN / leads FROM)
|
||||||
|
# room2 = other room (where door leads TO)
|
||||||
|
# blocking_room = puzzle room (same as room1, where puzzle is)
|
||||||
|
if "door" in door_data and door_data.door is Dictionary:
|
||||||
|
var original_door = door_data.door
|
||||||
|
if "room1" in original_door and original_door.room1:
|
||||||
|
door.room1 = original_door.room1
|
||||||
|
if "room2" in original_door and original_door.room2:
|
||||||
|
door.room2 = original_door.room2
|
||||||
|
|
||||||
|
# CRITICAL: For StoneDoor/GateDoor, verify door.room1 matches blocking_room
|
||||||
|
# The door should be IN the puzzle room (room1 == blocking_room)
|
||||||
|
if (door_data.type == "StoneDoor" or door_data.type == "GateDoor") and door.blocking_room and not door.blocking_room.is_empty():
|
||||||
|
if not door.room1 or door.room1.is_empty():
|
||||||
|
push_error("GameWorld: ERROR - Blocking door ", door.name, " has no room1! Cannot verify it's in puzzle room! Removing it.")
|
||||||
|
door.queue_free()
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Verify room1 (where door is) matches blocking_room (puzzle room)
|
||||||
|
var room1_matches_blocking = (door.room1.x == door.blocking_room.x and \
|
||||||
|
door.room1.y == door.blocking_room.y and \
|
||||||
|
door.room1.w == door.blocking_room.w and \
|
||||||
|
door.room1.h == door.blocking_room.h)
|
||||||
|
|
||||||
|
if not room1_matches_blocking:
|
||||||
|
push_error("GameWorld: ERROR - Blocking door ", door.name, " room1 (", door.room1.x, ",", door.room1.y, ") doesn't match blocking_room (", door.blocking_room.x, ",", door.blocking_room.y, ")! This door is NOT in the puzzle room! Removing it.")
|
||||||
|
door.queue_free()
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("GameWorld: Blocking door ", door.name, " verified - room1 (", door.room1.x, ",", door.room1.y, ") == blocking_room (", door.blocking_room.x, ",", door.blocking_room.y, ") - door is IN puzzle room")
|
||||||
|
|
||||||
|
# Set multiplayer authority BEFORE adding to scene
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
door.set_multiplayer_authority(1)
|
||||||
|
|
||||||
|
# CRITICAL: Set position BEFORE adding to scene tree (so _ready() can use it)
|
||||||
|
door.global_position = door_data.position if "position" in door_data else Vector2.ZERO
|
||||||
|
|
||||||
|
# Add to scene (this triggers _ready() which will use the position we just set)
|
||||||
|
entities_node.add_child(door)
|
||||||
|
|
||||||
|
# NOTE: Doors are connected to room triggers automatically by room_trigger._find_room_entities()
|
||||||
|
# No need to manually connect them here
|
||||||
|
|
||||||
|
# CRITICAL SAFETY CHECK: Verify door is for a puzzle room (StoneDoor/GateDoor should ONLY exist in puzzle rooms)
|
||||||
|
if door_data.type == "StoneDoor" or door_data.type == "GateDoor":
|
||||||
|
if not "blocking_room" in door_data or door_data.blocking_room.is_empty():
|
||||||
|
push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no blocking_room! This door should not exist!")
|
||||||
|
door.queue_free()
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not "puzzle_type" in door_data:
|
||||||
|
push_error("GameWorld: ERROR - Blocking door ", door.name, " (", door_data.type, ") has no puzzle_type! This door should not exist!")
|
||||||
|
door.queue_free()
|
||||||
|
continue
|
||||||
|
|
||||||
|
# CRITICAL: Verify that this door actually has puzzle elements
|
||||||
|
# Puzzle elements should already be created in dungeon_generator, but verify they exist
|
||||||
|
var has_puzzle_element = false
|
||||||
|
|
||||||
|
# Spawn floor switch if this door requires one (puzzle_type is "switch_walk" or "switch_pillar")
|
||||||
|
if "puzzle_type" in door_data and (door_data.puzzle_type == "switch_walk" or door_data.puzzle_type == "switch_pillar"):
|
||||||
|
if "floor_switch_position" in door_data or ("switch_data" in door_data and door_data.switch_data.has("position")):
|
||||||
|
var switch_pos = door_data.floor_switch_position if "floor_switch_position" in door_data else door_data.switch_data.position
|
||||||
|
var switch_tile_x = door_data.switch_tile_x if "switch_tile_x" in door_data else door_data.switch_data.tile_x
|
||||||
|
var switch_tile_y = door_data.switch_tile_y if "switch_tile_y" in door_data else door_data.switch_data.tile_y
|
||||||
|
var switch_type = door_data.switch_type if "switch_type" in door_data else ("walk" if door_data.puzzle_type == "switch_walk" else "pillar")
|
||||||
|
var switch_weight = door_data.switch_required_weight if "switch_required_weight" in door_data else (1.0 if switch_type == "walk" else 5.0)
|
||||||
|
|
||||||
|
# CRITICAL: Check if switch already exists for THIS SPECIFIC ROOM (to avoid duplicates)
|
||||||
|
# Only connect to switches in the SAME blocking_room - never connect across rooms!
|
||||||
|
var existing_switch = null
|
||||||
|
var door_blocking_room = door_data.blocking_room if "blocking_room" in door_data else {}
|
||||||
|
|
||||||
|
# CRITICAL: Verify door has valid blocking_room before searching for switches
|
||||||
|
if door_blocking_room.is_empty():
|
||||||
|
push_error("GameWorld: ERROR - Door ", door.name, " has empty blocking_room! Cannot find switches!")
|
||||||
|
continue
|
||||||
|
|
||||||
|
for existing in get_tree().get_nodes_in_group("floor_switch"):
|
||||||
|
if not is_instance_valid(existing):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# CRITICAL: Check ROOM FIRST (most important), then position
|
||||||
|
# Switches MUST have switch_room metadata set when spawned
|
||||||
|
if not existing.has_meta("switch_room"):
|
||||||
|
continue # Switch has no room metadata - skip it (can't verify it's in the right room)
|
||||||
|
|
||||||
|
var existing_switch_room = existing.get_meta("switch_room")
|
||||||
|
if existing_switch_room.is_empty():
|
||||||
|
continue # Invalid room data
|
||||||
|
|
||||||
|
# CRITICAL: Verify switch is in the SAME room as door (check room FIRST)
|
||||||
|
var room_match = (existing_switch_room.x == door_blocking_room.x and \
|
||||||
|
existing_switch_room.y == door_blocking_room.y and \
|
||||||
|
existing_switch_room.w == door_blocking_room.w and \
|
||||||
|
existing_switch_room.h == door_blocking_room.h)
|
||||||
|
|
||||||
|
if not room_match:
|
||||||
|
# Switch is in a different room - DO NOT connect, skip it
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Room matches - now check position (must be exact match)
|
||||||
|
var pos_match = existing.global_position.distance_to(switch_pos) < 1.0
|
||||||
|
if pos_match:
|
||||||
|
# Both room AND position match - this is the correct switch
|
||||||
|
existing_switch = existing
|
||||||
|
print("GameWorld: Found existing switch ", existing.name, " in room (", door_blocking_room.x, ",", door_blocking_room.y, ") at position ", existing.global_position, " matching door room and position")
|
||||||
|
break
|
||||||
|
|
||||||
|
if existing_switch:
|
||||||
|
# CRITICAL: Double-check room match before connecting
|
||||||
|
var existing_switch_room_final = existing_switch.get_meta("switch_room")
|
||||||
|
var final_room_match = false
|
||||||
|
if existing_switch_room_final and not existing_switch_room_final.is_empty() and door_blocking_room and not door_blocking_room.is_empty():
|
||||||
|
final_room_match = (existing_switch_room_final.x == door_blocking_room.x and \
|
||||||
|
existing_switch_room_final.y == door_blocking_room.y and \
|
||||||
|
existing_switch_room_final.w == door_blocking_room.w and \
|
||||||
|
existing_switch_room_final.h == door_blocking_room.h)
|
||||||
|
|
||||||
|
if final_room_match:
|
||||||
|
# Switch already exists in the SAME room - connect door to existing switch
|
||||||
|
door.connected_switches.append(existing_switch)
|
||||||
|
has_puzzle_element = true
|
||||||
|
print("GameWorld: Connected door ", door.name, " (room: ", door_blocking_room.get("x", "?"), ",", door_blocking_room.get("y", "?"), ") to existing switch ", existing_switch.name, " in SAME room")
|
||||||
|
else:
|
||||||
|
push_error("GameWorld: ERROR - Attempted to connect door ", door.name, " to switch ", existing_switch.name, " but rooms don't match! Door room: (", door_blocking_room.get("x", "?"), ",", door_blocking_room.get("y", "?"), "), Switch room: (", existing_switch_room_final.get("x", "?") if existing_switch_room_final else "?", ",", existing_switch_room_final.get("y", "?") if existing_switch_room_final else "?", ")")
|
||||||
|
# Don't connect - spawn a new switch instead
|
||||||
|
existing_switch = null
|
||||||
|
else:
|
||||||
|
# Spawn new switch - CRITICAL: Only spawn if we have valid room data
|
||||||
|
if not door_blocking_room or door_blocking_room.is_empty():
|
||||||
|
push_error("GameWorld: ERROR - Cannot spawn switch for door ", door.name, " - no blocking_room!")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# CRITICAL: Verify switch position matches door_data switch position exactly
|
||||||
|
# If switch_room in door_data doesn't match blocking_room, it's an error
|
||||||
|
if "switch_room" in door_data:
|
||||||
|
var door_switch_room = door_data.switch_room
|
||||||
|
if door_switch_room and not door_switch_room.is_empty():
|
||||||
|
var switch_room_matches = (door_switch_room.x == door_blocking_room.x and \
|
||||||
|
door_switch_room.y == door_blocking_room.y and \
|
||||||
|
door_switch_room.w == door_blocking_room.w and \
|
||||||
|
door_switch_room.h == door_blocking_room.h)
|
||||||
|
if not switch_room_matches:
|
||||||
|
push_error("GameWorld: ERROR - Door ", door.name, " switch_room (", door_switch_room.x, ",", door_switch_room.y, ") doesn't match blocking_room (", door_blocking_room.x, ",", door_blocking_room.y, ")! This is a bug!")
|
||||||
|
door.queue_free()
|
||||||
|
continue
|
||||||
|
|
||||||
|
var switch = _spawn_floor_switch(switch_pos, switch_weight, switch_tile_x, switch_tile_y, switch_type, door_blocking_room)
|
||||||
|
if switch:
|
||||||
|
# CRITICAL: Verify switch has room metadata set (should be set in _spawn_floor_switch)
|
||||||
|
if not switch.has_meta("switch_room"):
|
||||||
|
push_error("GameWorld: ERROR - Switch ", switch.name, " was spawned without switch_room metadata! Setting it now as fallback.")
|
||||||
|
switch.set_meta("switch_room", door_blocking_room) # Set it now as fallback
|
||||||
|
|
||||||
|
# CRITICAL: Verify switch room matches door blocking_room before connecting
|
||||||
|
# This ensures switches are ONLY connected to doors in the SAME room
|
||||||
|
var switch_room_check = switch.get_meta("switch_room")
|
||||||
|
if switch_room_check and not switch_room_check.is_empty() and door_blocking_room and not door_blocking_room.is_empty():
|
||||||
|
var room_match_before_connect = (switch_room_check.x == door_blocking_room.x and \
|
||||||
|
switch_room_check.y == door_blocking_room.y and \
|
||||||
|
switch_room_check.w == door_blocking_room.w and \
|
||||||
|
switch_room_check.h == door_blocking_room.h)
|
||||||
|
|
||||||
|
if room_match_before_connect:
|
||||||
|
# Connect switch to door ONLY if rooms match exactly
|
||||||
|
door.connected_switches.append(switch)
|
||||||
|
has_puzzle_element = true
|
||||||
|
print("GameWorld: Spawned switch ", switch.name, " in room (", door_blocking_room.x, ",", door_blocking_room.y, ") and connected to door ", door.name, " in SAME room")
|
||||||
|
|
||||||
|
# If this is a pillar switch, place a pillar in the same room
|
||||||
|
if switch_type == "pillar":
|
||||||
|
_place_pillar_in_room(door_blocking_room, switch_pos)
|
||||||
|
else:
|
||||||
|
push_error("GameWorld: ERROR - Switch ", switch.name, " room (", switch_room_check.x, ",", switch_room_check.y, ") doesn't match door blocking_room (", door_blocking_room.x, ",", door_blocking_room.y, ")! NOT connecting! Removing switch.")
|
||||||
|
switch.queue_free() # Remove the switch since it's in wrong room
|
||||||
|
has_puzzle_element = false # Don't count this as puzzle element
|
||||||
|
else:
|
||||||
|
push_error("GameWorld: ERROR - Switch ", switch.name, " or door ", door.name, " has invalid room data! Switch room: ", switch_room_check, ", Door room: ", door_blocking_room)
|
||||||
|
switch.queue_free() # Remove invalid switch
|
||||||
|
has_puzzle_element = false
|
||||||
|
else:
|
||||||
|
push_warning("GameWorld: WARNING - Failed to spawn floor switch for door ", door.name, "!")
|
||||||
|
|
||||||
|
# Place key in room if this is a KeyDoor
|
||||||
|
if door_data.type == "KeyDoor" and "key_room" in door_data:
|
||||||
|
_place_key_in_room(door_data.key_room)
|
||||||
|
has_puzzle_element = true # KeyDoors are always valid
|
||||||
|
|
||||||
|
# Spawn enemy spawners if this door requires enemies (puzzle_type is "enemy")
|
||||||
|
if "puzzle_type" in door_data and door_data.puzzle_type == "enemy":
|
||||||
|
print("GameWorld: ===== Door ", door.name, " has puzzle_type 'enemy' - checking for enemy_spawners =====")
|
||||||
|
if "enemy_spawners" in door_data and door_data.enemy_spawners is Array:
|
||||||
|
print("GameWorld: Door has enemy_spawners array with ", door_data.enemy_spawners.size(), " spawners")
|
||||||
|
var spawner_created = false
|
||||||
|
for spawner_data in door_data.enemy_spawners:
|
||||||
|
if spawner_data is Dictionary and spawner_data.has("position"):
|
||||||
|
# Check if spawner already exists for this room (to avoid duplicates)
|
||||||
|
var existing_spawner = null
|
||||||
|
for existing in get_tree().get_nodes_in_group("enemy_spawner"):
|
||||||
|
if existing.global_position.distance_to(spawner_data.position) < 1.0:
|
||||||
|
existing_spawner = existing
|
||||||
|
break
|
||||||
|
|
||||||
|
if existing_spawner:
|
||||||
|
# Spawner already exists - just verify it's set up correctly
|
||||||
|
existing_spawner.set_meta("blocking_room", door_data.blocking_room)
|
||||||
|
spawner_created = true
|
||||||
|
print("GameWorld: Found existing spawner ", existing_spawner.name, " for door ", door.name)
|
||||||
|
else:
|
||||||
|
# Spawn new spawner
|
||||||
|
var spawner = _spawn_enemy_spawner(
|
||||||
|
spawner_data.position,
|
||||||
|
spawner_data.room if spawner_data.has("room") else door_data.blocking_room,
|
||||||
|
spawner_data # Pass spawner_data to access spawn_once flag
|
||||||
|
)
|
||||||
|
if spawner:
|
||||||
|
# Store reference to door for spawner (optional - spawner will be found by room trigger)
|
||||||
|
spawner.set_meta("blocking_room", door_data.blocking_room)
|
||||||
|
spawner_created = true
|
||||||
|
print("GameWorld: Spawned enemy spawner ", spawner.name, " for door ", door.name, " at ", spawner_data.position)
|
||||||
|
if spawner_created:
|
||||||
|
has_puzzle_element = true
|
||||||
|
else:
|
||||||
|
push_warning("GameWorld: WARNING - Failed to spawn enemy spawner for door ", door.name, "!")
|
||||||
|
if "enemy_spawners" not in door_data:
|
||||||
|
push_warning("GameWorld: Reason: door_data has no 'enemy_spawners' key!")
|
||||||
|
elif not door_data.enemy_spawners is Array:
|
||||||
|
push_warning("GameWorld: Reason: door_data.enemy_spawners is not an Array! Type: ", typeof(door_data.enemy_spawners))
|
||||||
|
elif door_data.enemy_spawners.size() == 0:
|
||||||
|
push_warning("GameWorld: Reason: door_data.enemy_spawners array is empty!")
|
||||||
|
else:
|
||||||
|
if "puzzle_type" in door_data:
|
||||||
|
print("GameWorld: Door ", door.name, " has puzzle_type '", door_data.puzzle_type, "' (not 'enemy')")
|
||||||
|
|
||||||
|
# CRITICAL: If door has no puzzle elements (neither switch nor spawner), this is an error
|
||||||
|
# This should never happen if dungeon_generator logic is correct, but add safety check
|
||||||
|
if door_data.type != "KeyDoor" and not has_puzzle_element:
|
||||||
|
push_error("GameWorld: ERROR - Blocking door ", door.name, " (type: ", door_data.type, ") has no puzzle elements! This door should not exist!")
|
||||||
|
print("GameWorld: Door data keys: ", door_data.keys())
|
||||||
|
print("GameWorld: Door puzzle_type: ", door_data.get("puzzle_type", "MISSING"))
|
||||||
|
print("GameWorld: Door has requires_switch: ", door_data.get("requires_switch", false))
|
||||||
|
print("GameWorld: Door has requires_enemies: ", door_data.get("requires_enemies", false))
|
||||||
|
print("GameWorld: Door has floor_switch_position: ", "floor_switch_position" in door_data)
|
||||||
|
print("GameWorld: Door has enemy_spawners: ", "enemy_spawners" in door_data)
|
||||||
|
# Remove the door since it's invalid - it was created without puzzle elements
|
||||||
|
door.queue_free()
|
||||||
|
print("GameWorld: Removed invalid blocking door ", door.name, " - it had no puzzle elements!")
|
||||||
|
continue # Skip to next door
|
||||||
|
|
||||||
|
print("GameWorld: Spawned ", blocking_doors.size(), " blocking doors")
|
||||||
|
|
||||||
|
func _spawn_floor_switch(i_position: Vector2, required_weight: float, tile_x: int, tile_y: int, switch_type: String = "walk", switch_room: Dictionary = {}) -> Node:
|
||||||
|
# Spawn a floor switch
|
||||||
|
# switch_type: "walk" for walk-on switch (weight 1), "pillar" for pillar switch (weight 5)
|
||||||
|
var switch_script = load("res://scripts/floor_switch.gd")
|
||||||
|
if not switch_script:
|
||||||
|
push_error("ERROR: Could not load floor_switch script!")
|
||||||
|
return null
|
||||||
|
|
||||||
|
var switch = Area2D.new()
|
||||||
|
switch.set_script(switch_script)
|
||||||
|
switch.name = "FloorSwitch_%d_%d" % [tile_x, tile_y]
|
||||||
|
switch.add_to_group("floor_switch")
|
||||||
|
|
||||||
|
# Set properties
|
||||||
|
switch.switch_type = switch_type if switch_type == "walk" or switch_type == "pillar" else "walk"
|
||||||
|
switch.required_weight = required_weight # Will be overridden in _ready() based on switch_type, but set it here too
|
||||||
|
switch.switch_tile_position = Vector2i(tile_x, tile_y)
|
||||||
|
|
||||||
|
# Create collision shape
|
||||||
|
var collision_shape = CollisionShape2D.new()
|
||||||
|
var circle_shape = CircleShape2D.new()
|
||||||
|
circle_shape.radius = 8.0 # 16 pixel diameter
|
||||||
|
collision_shape.shape = circle_shape
|
||||||
|
switch.add_child(collision_shape)
|
||||||
|
|
||||||
|
# Set multiplayer authority
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
switch.set_multiplayer_authority(1)
|
||||||
|
|
||||||
|
# CRITICAL: Store switch_room metadata BEFORE adding to scene
|
||||||
|
# This ensures switches can be matched to doors in the same room
|
||||||
|
if switch_room and not switch_room.is_empty():
|
||||||
|
switch.set_meta("switch_room", switch_room)
|
||||||
|
print("GameWorld: Set switch_room metadata for switch - room (", switch_room.x, ", ", switch_room.y, ")")
|
||||||
|
else:
|
||||||
|
push_warning("GameWorld: WARNING - Spawning switch without switch_room metadata! This may cause cross-room connections!")
|
||||||
|
|
||||||
|
# Add to scene
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if entities_node:
|
||||||
|
entities_node.add_child(switch)
|
||||||
|
switch.global_position = i_position
|
||||||
|
|
||||||
|
# Update tilemap to show switch tile (initial inactive state)
|
||||||
|
if dungeon_tilemap_layer:
|
||||||
|
var initial_tile: Vector2i
|
||||||
|
if switch_type == "pillar":
|
||||||
|
initial_tile = Vector2i(16, 9) # Pillar switch inactive
|
||||||
|
else:
|
||||||
|
initial_tile = Vector2i(11, 9) # Walk-on switch inactive
|
||||||
|
dungeon_tilemap_layer.set_cell(Vector2i(tile_x, tile_y), 0, initial_tile)
|
||||||
|
|
||||||
|
print("GameWorld: Spawned ", switch_type, " floor switch at ", i_position, " tile (", tile_x, ", ", tile_y, "), room: (", switch_room.get("x", "?") if switch_room and not switch_room.is_empty() else "?", ", ", switch_room.get("y", "?") if switch_room and not switch_room.is_empty() else "?", ")")
|
||||||
|
return switch
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
func _spawn_enemy_spawner(i_position: Vector2, room: Dictionary, spawner_data: Dictionary = {}) -> Node:
|
||||||
|
# Spawn an enemy spawner for a blocking room
|
||||||
|
var spawner_script = load("res://scripts/enemy_spawner.gd")
|
||||||
|
if not spawner_script:
|
||||||
|
push_error("ERROR: Could not load enemy_spawner script!")
|
||||||
|
return null
|
||||||
|
|
||||||
|
var spawner = Node2D.new()
|
||||||
|
spawner.set_script(spawner_script)
|
||||||
|
spawner.name = "EnemySpawner_%d_%d" % [room.x, room.y] if room and not room.is_empty() else "EnemySpawner_%d_%d" % [int(i_position.x), int(i_position.y)]
|
||||||
|
spawner.add_to_group("enemy_spawner")
|
||||||
|
|
||||||
|
# Set spawner properties - IMPORTANT: spawn_on_ready = false so enemies only spawn when player enters room
|
||||||
|
spawner.spawn_on_ready = false # Don't spawn on ready - wait for room trigger
|
||||||
|
spawner.respawn_time = 0.0 # Don't respawn - enemies spawn once when entering room
|
||||||
|
spawner.max_enemies = 1 # One enemy per spawner
|
||||||
|
|
||||||
|
# Check if this spawner should be destroyed after spawning once
|
||||||
|
if spawner_data.has("spawn_once") and spawner_data.spawn_once:
|
||||||
|
spawner.set_meta("spawn_once", true) # Mark spawner for destruction after spawning
|
||||||
|
|
||||||
|
# Set enemy scenes (use default enemy types)
|
||||||
|
# enemy_scenes is Array[PackedScene], so we need to properly type it
|
||||||
|
var enemy_scenes: Array[PackedScene] = []
|
||||||
|
var scene_paths = [
|
||||||
|
"res://scenes/enemy_rat.tscn",
|
||||||
|
"res://scenes/enemy_humanoid.tscn",
|
||||||
|
"res://scenes/enemy_slime.tscn",
|
||||||
|
"res://scenes/enemy_bat.tscn"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Load scenes and add to typed array
|
||||||
|
for path in scene_paths:
|
||||||
|
var scene = load(path) as PackedScene
|
||||||
|
if scene:
|
||||||
|
enemy_scenes.append(scene)
|
||||||
|
|
||||||
|
spawner.enemy_scenes = enemy_scenes
|
||||||
|
|
||||||
|
# Set multiplayer authority
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
spawner.set_multiplayer_authority(1)
|
||||||
|
|
||||||
|
# Store room reference
|
||||||
|
if room and not room.is_empty():
|
||||||
|
spawner.set_meta("room", room)
|
||||||
|
|
||||||
|
# Add to scene
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if entities_node:
|
||||||
|
entities_node.add_child(spawner)
|
||||||
|
spawner.global_position = i_position
|
||||||
|
print("GameWorld: ✓✓✓ Successfully spawned enemy spawner '", spawner.name, "' at ", i_position, " for room at (", room.x if room and not room.is_empty() else "unknown", ", ", room.y if room and not room.is_empty() else "unknown", ")")
|
||||||
|
print("GameWorld: Spawner has room metadata: ", spawner.has_meta("room"))
|
||||||
|
if spawner.has_meta("room"):
|
||||||
|
var spawner_room = spawner.get_meta("room")
|
||||||
|
print("GameWorld: Spawner room metadata: (", spawner_room.x if spawner_room and not spawner_room.is_empty() else "none", ", ", spawner_room.y if spawner_room and not spawner_room.is_empty() else "none", ", ", spawner_room.w if spawner_room and not spawner_room.is_empty() else "none", "x", spawner_room.h if spawner_room and not spawner_room.is_empty() else "none", ")")
|
||||||
|
print("GameWorld: Spawner in group 'enemy_spawner': ", spawner.is_in_group("enemy_spawner"))
|
||||||
|
print("GameWorld: Spawner enemy_scenes.size(): ", spawner.enemy_scenes.size() if "enemy_scenes" in spawner else "N/A")
|
||||||
|
return spawner
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
func _spawn_room_triggers():
|
||||||
|
# Spawn room trigger areas for all rooms
|
||||||
|
if dungeon_data.is_empty() or not dungeon_data.has("rooms"):
|
||||||
|
return
|
||||||
|
|
||||||
|
var rooms = dungeon_data.rooms
|
||||||
|
if rooms == null or not rooms is Array:
|
||||||
|
return
|
||||||
|
|
||||||
|
var trigger_script = load("res://scripts/room_trigger.gd")
|
||||||
|
if not trigger_script:
|
||||||
|
push_error("ERROR: Could not load room_trigger script!")
|
||||||
|
return
|
||||||
|
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if not entities_node:
|
||||||
|
push_error("ERROR: Could not find Entities node!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("GameWorld: Spawning ", rooms.size(), " room triggers")
|
||||||
|
|
||||||
|
for i in range(rooms.size()):
|
||||||
|
var room = rooms[i]
|
||||||
|
if not room is Dictionary:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var trigger = Area2D.new()
|
||||||
|
trigger.set_script(trigger_script)
|
||||||
|
trigger.name = "RoomTrigger_%d" % i
|
||||||
|
trigger.add_to_group("room_trigger")
|
||||||
|
|
||||||
|
# Set room data
|
||||||
|
trigger.room = room
|
||||||
|
|
||||||
|
# Create collision shape covering ONLY the room interior (no overlap with adjacent rooms)
|
||||||
|
var collision_shape = CollisionShape2D.new()
|
||||||
|
var rect_shape = RectangleShape2D.new()
|
||||||
|
var tile_size = 16
|
||||||
|
# Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls)
|
||||||
|
# This ensures the trigger only covers THIS room, not adjacent rooms or doorways
|
||||||
|
var room_world_x = (room.x + 2) * tile_size
|
||||||
|
var room_world_y = (room.y + 2) * tile_size
|
||||||
|
var room_world_w = (room.w - 4) * tile_size # Width excluding 2-tile walls on each side
|
||||||
|
var room_world_h = (room.h - 4) * tile_size # Height excluding 2-tile walls on each side
|
||||||
|
rect_shape.size = Vector2(room_world_w, room_world_h)
|
||||||
|
collision_shape.shape = rect_shape
|
||||||
|
# Position collision shape at center of room (relative to Area2D)
|
||||||
|
collision_shape.position = Vector2(room_world_w / 2.0, room_world_h / 2.0)
|
||||||
|
trigger.add_child(collision_shape)
|
||||||
|
|
||||||
|
# Set Area2D global position to the top-left corner of the room interior
|
||||||
|
# This ensures the trigger ONLY covers this specific room
|
||||||
|
trigger.global_position = Vector2(room_world_x, room_world_y)
|
||||||
|
|
||||||
|
# Set multiplayer authority
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
trigger.set_multiplayer_authority(1)
|
||||||
|
|
||||||
|
# Add to scene
|
||||||
|
entities_node.add_child(trigger)
|
||||||
|
|
||||||
|
print("GameWorld: Spawned ", rooms.size(), " room triggers")
|
||||||
|
|
||||||
|
func _place_key_in_room(room: Dictionary):
|
||||||
|
# Place a key in the specified room (as loot)
|
||||||
|
if room.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var loot_scene = preload("res://scenes/loot.tscn")
|
||||||
|
if not loot_scene:
|
||||||
|
return
|
||||||
|
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if not entities_node:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find a valid floor position in the room
|
||||||
|
var tile_size = 16
|
||||||
|
var valid_positions = []
|
||||||
|
|
||||||
|
# Room interior is from room.x + 2 to room.x + room.w - 2
|
||||||
|
for x in range(room.x + 2, room.x + room.w - 2):
|
||||||
|
for y in range(room.y + 2, room.y + room.h - 2):
|
||||||
|
if x >= 0 and x < dungeon_data.map_size.x and y >= 0 and y < dungeon_data.map_size.y:
|
||||||
|
if dungeon_data.grid[x][y] == 1: # Floor
|
||||||
|
var world_x = x * tile_size + tile_size / 2.0
|
||||||
|
var world_y = y * tile_size + tile_size / 2.0
|
||||||
|
valid_positions.append(Vector2(world_x, world_y))
|
||||||
|
|
||||||
|
if valid_positions.size() > 0:
|
||||||
|
# Pick a random position
|
||||||
|
var rng = RandomNumberGenerator.new()
|
||||||
|
rng.randomize()
|
||||||
|
var key_pos = valid_positions[rng.randi() % valid_positions.size()]
|
||||||
|
|
||||||
|
# Spawn key loot
|
||||||
|
var key_loot = loot_scene.instantiate()
|
||||||
|
key_loot.name = "KeyLoot_%d_%d" % [int(key_pos.x), int(key_pos.y)]
|
||||||
|
key_loot.loot_type = key_loot.LootType.KEY
|
||||||
|
|
||||||
|
# Set multiplayer authority
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
key_loot.set_multiplayer_authority(1)
|
||||||
|
|
||||||
|
entities_node.add_child(key_loot)
|
||||||
|
key_loot.global_position = key_pos
|
||||||
|
|
||||||
|
print("GameWorld: Placed key in room at ", key_pos)
|
||||||
|
|
||||||
|
func _place_pillar_in_room(room: Dictionary, switch_position: Vector2):
|
||||||
|
# Place a pillar in the specified room (needed for pillar switches)
|
||||||
|
if room.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var interactable_object_scene = preload("res://scenes/interactable_object.tscn")
|
||||||
|
if not interactable_object_scene:
|
||||||
|
push_error("ERROR: Could not load interactable_object scene for pillar!")
|
||||||
|
return
|
||||||
|
|
||||||
|
var entities_node = get_node_or_null("Entities")
|
||||||
|
if not entities_node:
|
||||||
|
push_error("ERROR: Could not find Entities node for pillar placement!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find a valid floor position in the room (away from the switch)
|
||||||
|
var tile_size = 16
|
||||||
|
var valid_positions = []
|
||||||
|
|
||||||
|
# Room interior is from room.x + 2 to room.x + room.w - 2 (excluding 2-tile walls)
|
||||||
|
# CRITICAL: Also exclude last row/column of floor tiles to prevent objects from spawning in walls
|
||||||
|
# Objects are 16x16, so we need at least 1 tile buffer from walls
|
||||||
|
# Floor tiles are at: room.x+2, room.x+3, ..., room.x+room.w-3 (last floor tile before wall)
|
||||||
|
# To exclude last tile, we want: room.x+2, room.x+3, ..., room.x+room.w-4
|
||||||
|
var min_x = room.x + 2
|
||||||
|
var max_x = room.x + room.w - 4 # Exclude last 2 floor tiles (room.w-3 is last floor, room.w-4 is second-to-last)
|
||||||
|
var min_y = room.y + 2
|
||||||
|
var max_y = room.y + room.h - 4 # Exclude last 2 floor tiles (room.h-3 is last floor, room.h-4 is second-to-last)
|
||||||
|
|
||||||
|
for x in range(min_x, max_x + 1): # +1 because range is exclusive at end
|
||||||
|
for y in range(min_y, max_y + 1):
|
||||||
|
if x >= 0 and x < dungeon_data.map_size.x and y >= 0 and y < dungeon_data.map_size.y:
|
||||||
|
if dungeon_data.grid[x][y] == 1: # Floor
|
||||||
|
# CRITICAL: Interactable objects are 16x16 pixels with origin at (0,0) (top-left)
|
||||||
|
# To center a 16x16 sprite in a 16x16 tile, we need to offset by 8 pixels into the tile
|
||||||
|
# Tile (x,y) spans from (x*16, y*16) to ((x+1)*16, (y+1)*16)
|
||||||
|
# Position sprite 8 pixels into the tile from top-left: (x*16 + 8, y*16 + 8)
|
||||||
|
var world_x = x * tile_size + 8
|
||||||
|
var world_y = y * tile_size + 8
|
||||||
|
var world_pos = Vector2(world_x, world_y)
|
||||||
|
|
||||||
|
# Ensure pillar is at least 2 tiles away from the switch
|
||||||
|
var distance_to_switch = world_pos.distance_to(switch_position)
|
||||||
|
if distance_to_switch >= tile_size * 2: # At least 2 tiles away
|
||||||
|
valid_positions.append(world_pos)
|
||||||
|
|
||||||
|
if valid_positions.size() > 0:
|
||||||
|
# Pick a random position
|
||||||
|
var rng = RandomNumberGenerator.new()
|
||||||
|
rng.randomize()
|
||||||
|
var pillar_pos = valid_positions[rng.randi() % valid_positions.size()]
|
||||||
|
|
||||||
|
# Spawn pillar interactable object
|
||||||
|
var pillar = interactable_object_scene.instantiate()
|
||||||
|
pillar.name = "Pillar_%d_%d" % [int(pillar_pos.x), int(pillar_pos.y)]
|
||||||
|
pillar.set_meta("dungeon_spawned", true)
|
||||||
|
pillar.set_meta("room", room)
|
||||||
|
|
||||||
|
# Set multiplayer authority
|
||||||
|
if multiplayer.has_multiplayer_peer():
|
||||||
|
pillar.set_multiplayer_authority(1)
|
||||||
|
|
||||||
|
# Add to scene tree
|
||||||
|
entities_node.add_child(pillar)
|
||||||
|
pillar.global_position = pillar_pos
|
||||||
|
|
||||||
|
# Call setup function to configure as pillar
|
||||||
|
if pillar.has_method("setup_pillar"):
|
||||||
|
pillar.call("setup_pillar")
|
||||||
|
else:
|
||||||
|
push_error("ERROR: Pillar does not have setup_pillar method!")
|
||||||
|
|
||||||
|
# Add to group for easy access
|
||||||
|
pillar.add_to_group("interactable_object")
|
||||||
|
|
||||||
|
print("GameWorld: Placed pillar in room at ", pillar_pos, " (switch at ", switch_position, ")")
|
||||||
|
else:
|
||||||
|
push_warning("GameWorld: Could not find valid position for pillar in room! Room might be too small.")
|
||||||
|
|
||||||
|
func _connect_door_to_room_trigger(door: Node):
|
||||||
|
# Connect a door to its room trigger area
|
||||||
|
# blocking_room is a variable in door.gd, so it should exist
|
||||||
|
var blocking_room = door.blocking_room
|
||||||
|
if not blocking_room or blocking_room.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find the room trigger for this room
|
||||||
|
for trigger in get_tree().get_nodes_in_group("room_trigger"):
|
||||||
|
if is_instance_valid(trigger):
|
||||||
|
# room is a variable in room_trigger.gd, compare by values
|
||||||
|
var trigger_room = trigger.room
|
||||||
|
if trigger_room and not trigger_room.is_empty() and \
|
||||||
|
trigger_room.x == blocking_room.x and trigger_room.y == blocking_room.y and \
|
||||||
|
trigger_room.w == blocking_room.w and trigger_room.h == blocking_room.h:
|
||||||
|
# Connect door to trigger
|
||||||
|
door.room_trigger_area = trigger
|
||||||
|
# Add door to trigger's doors list (doors_in_room is a variable in room_trigger.gd)
|
||||||
|
trigger.doors_in_room.append(door)
|
||||||
|
break
|
||||||
|
|||||||
@@ -291,7 +291,15 @@ func can_be_destroyed() -> bool:
|
|||||||
func on_grabbed(by_player):
|
func on_grabbed(by_player):
|
||||||
# Special handling for chests - open instead of grab
|
# Special handling for chests - open instead of grab
|
||||||
if object_type == "Chest" and not is_chest_opened:
|
if object_type == "Chest" and not is_chest_opened:
|
||||||
_open_chest()
|
# In multiplayer, send RPC to server if client is opening
|
||||||
|
if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
|
||||||
|
# Client - send request to server
|
||||||
|
if by_player and by_player.is_multiplayer_authority():
|
||||||
|
var player_peer_id = by_player.get_multiplayer_authority()
|
||||||
|
_request_chest_open.rpc_id(1, player_peer_id)
|
||||||
|
else:
|
||||||
|
# Server or single player - open directly
|
||||||
|
_open_chest(by_player)
|
||||||
return
|
return
|
||||||
|
|
||||||
is_being_held = true
|
is_being_held = true
|
||||||
@@ -463,46 +471,120 @@ func setup_pushable_high_box():
|
|||||||
if sprite_above:
|
if sprite_above:
|
||||||
sprite_above.frame = top_frames[index]
|
sprite_above.frame = top_frames[index]
|
||||||
|
|
||||||
func _open_chest():
|
func _open_chest(by_player: Node = null):
|
||||||
if is_chest_opened:
|
if is_chest_opened:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Only process on server (authority)
|
||||||
|
if multiplayer.has_multiplayer_peer() and not is_multiplayer_authority():
|
||||||
|
return
|
||||||
|
|
||||||
is_chest_opened = true
|
is_chest_opened = true
|
||||||
if sprite and chest_opened_frame >= 0:
|
if sprite and chest_opened_frame >= 0:
|
||||||
sprite.frame = chest_opened_frame
|
sprite.frame = chest_opened_frame
|
||||||
|
|
||||||
# Spawn loot item
|
# Random loot type
|
||||||
var loot_scene = preload("res://scenes/loot.tscn")
|
var loot_types = [
|
||||||
if loot_scene:
|
{"type": "coin", "name": "Coin", "color": Color(1.0, 0.84, 0.0)},
|
||||||
var loot = loot_scene.instantiate()
|
{"type": "apple", "name": "Apple", "color": Color.GREEN},
|
||||||
if loot:
|
{"type": "banana", "name": "Banana", "color": Color.YELLOW},
|
||||||
# Random loot type
|
{"type": "cherry", "name": "Cherry", "color": Color.RED},
|
||||||
var loot_types = loot.LootType.values()
|
{"type": "key", "name": "Key", "color": Color.YELLOW}
|
||||||
loot.loot_type = loot_types[randi() % loot_types.size()]
|
]
|
||||||
|
var selected_loot = loot_types[randi() % loot_types.size()]
|
||||||
|
|
||||||
# Position above chest with some randomness
|
# CRITICAL: Instantly give item to player instead of spawning loot object
|
||||||
var spawn_pos = global_position + Vector2(randf_range(-10, 10), randf_range(-20, -10))
|
if by_player and is_instance_valid(by_player) and by_player.is_in_group("player"):
|
||||||
loot.global_position = spawn_pos
|
# Give item directly to player based on type
|
||||||
|
match selected_loot.type:
|
||||||
|
"coin":
|
||||||
|
if by_player.has_method("add_coins"):
|
||||||
|
by_player.add_coins(1)
|
||||||
|
# Show pickup notification with coin graphic
|
||||||
|
var coin_texture = load("res://assets/gfx/pickups/gold_coin.png")
|
||||||
|
_show_item_pickup_notification(by_player, "+1 coin", selected_loot.color, coin_texture, 6, 1, 0)
|
||||||
|
"apple":
|
||||||
|
var heal_amount = 20.0
|
||||||
|
if by_player.has_method("heal"):
|
||||||
|
by_player.heal(heal_amount)
|
||||||
|
# Show pickup notification with apple graphic
|
||||||
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
|
_show_item_pickup_notification(by_player, "+" + str(int(heal_amount)) + " hp", selected_loot.color, items_texture, 20, 14, (8 * 20) + 11)
|
||||||
|
"banana":
|
||||||
|
var heal_amount = 20.0
|
||||||
|
if by_player.has_method("heal"):
|
||||||
|
by_player.heal(heal_amount)
|
||||||
|
# Show pickup notification with banana graphic
|
||||||
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
|
_show_item_pickup_notification(by_player, "+" + str(int(heal_amount)) + " hp", selected_loot.color, items_texture, 20, 14, (8 * 20) + 12)
|
||||||
|
"cherry":
|
||||||
|
var heal_amount = 20.0
|
||||||
|
if by_player.has_method("heal"):
|
||||||
|
by_player.heal(heal_amount)
|
||||||
|
# Show pickup notification with cherry graphic
|
||||||
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
|
_show_item_pickup_notification(by_player, "+" + str(int(heal_amount)) + " hp", selected_loot.color, items_texture, 20, 14, (8 * 20) + 13)
|
||||||
|
"key":
|
||||||
|
if by_player.has_method("add_key"):
|
||||||
|
by_player.add_key(1)
|
||||||
|
# Show pickup notification with key graphic
|
||||||
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
|
_show_item_pickup_notification(by_player, "+1 key", selected_loot.color, items_texture, 20, 14, (13 * 20) + 10)
|
||||||
|
|
||||||
# Set initial velocity to fly out
|
# Play chest open sound
|
||||||
var random_angle = randf() * PI * 2
|
if has_node("SfxChestOpen"):
|
||||||
var random_force = randf_range(80.0, 120.0)
|
$SfxChestOpen.play()
|
||||||
loot.velocity = Vector2(cos(random_angle), sin(random_angle)) * random_force
|
|
||||||
loot.velocity_z = randf_range(100.0, 150.0)
|
|
||||||
loot.is_airborne = true
|
|
||||||
loot.velocity_set_by_spawner = true
|
|
||||||
|
|
||||||
get_parent().call_deferred("add_child", loot)
|
print(name, " opened by ", by_player.name, "! Item given: ", selected_loot.name)
|
||||||
|
else:
|
||||||
|
push_error("Chest: ERROR - No valid player to give item to!")
|
||||||
|
|
||||||
# Sync to network if multiplayer
|
# Sync chest opening visual to all clients (item already given on server)
|
||||||
if multiplayer.has_multiplayer_peer():
|
if multiplayer.has_multiplayer_peer():
|
||||||
_sync_chest_open.rpc()
|
_sync_chest_open.rpc(selected_loot.type if by_player else "coin")
|
||||||
|
|
||||||
print(name, " opened! Loot spawned: ", loot_types[loot.loot_type])
|
|
||||||
|
|
||||||
@rpc("any_peer", "reliable")
|
@rpc("any_peer", "reliable")
|
||||||
func _sync_chest_open():
|
func _request_chest_open(player_peer_id: int):
|
||||||
# Sync chest opening to all clients
|
# Server receives chest open request from client
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_chest_opened:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find the player by peer ID
|
||||||
|
var player = null
|
||||||
|
var players = get_tree().get_nodes_in_group("player")
|
||||||
|
for p in players:
|
||||||
|
if p.get_multiplayer_authority() == player_peer_id:
|
||||||
|
player = p
|
||||||
|
break
|
||||||
|
|
||||||
|
if not player:
|
||||||
|
push_error("Chest: ERROR - Could not find player with peer_id ", player_peer_id, " for chest opening!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Open chest on server (this will give item to player)
|
||||||
|
_open_chest(player)
|
||||||
|
|
||||||
|
@rpc("any_peer", "reliable")
|
||||||
|
func _sync_chest_open(_loot_type_str: String = "coin"):
|
||||||
|
# Sync chest opening to all clients (only visual - item already given on server)
|
||||||
if not is_chest_opened and sprite and chest_opened_frame >= 0:
|
if not is_chest_opened and sprite and chest_opened_frame >= 0:
|
||||||
is_chest_opened = true
|
is_chest_opened = true
|
||||||
sprite.frame = chest_opened_frame
|
sprite.frame = chest_opened_frame
|
||||||
|
|
||||||
|
# Play chest open sound on clients
|
||||||
|
if has_node("SfxChestOpen"):
|
||||||
|
$SfxChestOpen.play()
|
||||||
|
|
||||||
|
func _show_item_pickup_notification(player: Node, text: String, text_color: Color, item_texture: Texture2D = null, sprite_hframes: int = 1, sprite_vframes: int = 1, sprite_frame: int = 0):
|
||||||
|
# Show item graphic and text above player's head for 0.5s, then fade out over 0.5s
|
||||||
|
var floating_text_scene = preload("res://scenes/floating_text.tscn")
|
||||||
|
if floating_text_scene and player and is_instance_valid(player):
|
||||||
|
var floating_text = floating_text_scene.instantiate()
|
||||||
|
var parent = player.get_parent()
|
||||||
|
if parent:
|
||||||
|
parent.add_child(floating_text)
|
||||||
|
floating_text.global_position = player.global_position + Vector2(0, -20)
|
||||||
|
floating_text.setup(text, text_color, 0.5, 0.5, item_texture, sprite_hframes, sprite_vframes, sprite_frame) # Show for 0.5s, fade over 0.5s
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ func show_level(level_number: int):
|
|||||||
if tween.is_valid():
|
if tween.is_valid():
|
||||||
tween.kill()
|
tween.kill()
|
||||||
|
|
||||||
|
# Get vbox once at the start (reuse throughout function)
|
||||||
|
var vbox = get_child(0) if get_child_count() > 0 else null
|
||||||
|
|
||||||
# Update label text FIRST before showing
|
# Update label text FIRST before showing
|
||||||
if level_label:
|
if level_label:
|
||||||
level_label.text = "LEVEL " + str(level_number)
|
level_label.text = "LEVEL " + str(level_number)
|
||||||
@@ -30,7 +33,6 @@ func show_level(level_number: int):
|
|||||||
else:
|
else:
|
||||||
print("LevelTextUI: ERROR - level_label is null!")
|
print("LevelTextUI: ERROR - level_label is null!")
|
||||||
# Try to find it again
|
# Try to find it again
|
||||||
var vbox = get_child(0) if get_child_count() > 0 else null
|
|
||||||
if vbox:
|
if vbox:
|
||||||
for child in vbox.get_children():
|
for child in vbox.get_children():
|
||||||
if child.name == "LevelLabel" or child is Label:
|
if child.name == "LevelLabel" or child is Label:
|
||||||
@@ -43,7 +45,6 @@ func show_level(level_number: int):
|
|||||||
visible = true
|
visible = true
|
||||||
|
|
||||||
# Fade in - fade the VBoxContainer and level label
|
# Fade in - fade the VBoxContainer and level label
|
||||||
var vbox = get_child(0) if get_child_count() > 0 else null
|
|
||||||
if vbox:
|
if vbox:
|
||||||
vbox.modulate.a = 0.0
|
vbox.modulate.a = 0.0
|
||||||
var fade_in = create_tween()
|
var fade_in = create_tween()
|
||||||
@@ -68,4 +69,3 @@ func show_level(level_number: int):
|
|||||||
await fade_out.finished
|
await fade_out.finished
|
||||||
|
|
||||||
visible = false
|
visible = false
|
||||||
|
|
||||||
|
|||||||
@@ -102,28 +102,28 @@ func _setup_sprite():
|
|||||||
sprite.texture = items_texture
|
sprite.texture = items_texture
|
||||||
sprite.hframes = 20
|
sprite.hframes = 20
|
||||||
sprite.vframes = 14
|
sprite.vframes = 14
|
||||||
sprite.frame = (8 * 20) + 11 # vframe 9, hframe 11
|
sprite.frame = (8 * 20) + 11
|
||||||
LootType.BANANA:
|
LootType.BANANA:
|
||||||
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
if items_texture:
|
if items_texture:
|
||||||
sprite.texture = items_texture
|
sprite.texture = items_texture
|
||||||
sprite.hframes = 20
|
sprite.hframes = 20
|
||||||
sprite.vframes = 14
|
sprite.vframes = 14
|
||||||
sprite.frame = (8 * 20) + 12 # vframe 9, hframe 12
|
sprite.frame = (8 * 20) + 12
|
||||||
LootType.CHERRY:
|
LootType.CHERRY:
|
||||||
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
if items_texture:
|
if items_texture:
|
||||||
sprite.texture = items_texture
|
sprite.texture = items_texture
|
||||||
sprite.hframes = 20
|
sprite.hframes = 20
|
||||||
sprite.vframes = 14
|
sprite.vframes = 14
|
||||||
sprite.frame = (8 * 20) + 13 # vframe 9, hframe 13
|
sprite.frame = (8 * 20) + 13
|
||||||
LootType.KEY:
|
LootType.KEY:
|
||||||
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
if items_texture:
|
if items_texture:
|
||||||
sprite.texture = items_texture
|
sprite.texture = items_texture
|
||||||
sprite.hframes = 20
|
sprite.hframes = 20
|
||||||
sprite.vframes = 14
|
sprite.vframes = 14
|
||||||
sprite.frame = (13 * 20) + 11 # vframe 9, hframe 13
|
sprite.frame = (13 * 20) + 10
|
||||||
|
|
||||||
func _setup_collision_shape():
|
func _setup_collision_shape():
|
||||||
if not collision_shape:
|
if not collision_shape:
|
||||||
@@ -338,8 +338,9 @@ func _process_pickup_on_server(player: Node):
|
|||||||
# Give player coin
|
# Give player coin
|
||||||
if player.has_method("add_coins"):
|
if player.has_method("add_coins"):
|
||||||
player.add_coins(coin_value)
|
player.add_coins(coin_value)
|
||||||
# Show floating text (gold color)
|
# Show floating text with item graphic and text
|
||||||
_show_floating_text(player, "+1 coin", Color(1.0, 0.84, 0.0)) # Gold color
|
var coin_texture = load("res://assets/gfx/pickups/gold_coin.png")
|
||||||
|
_show_floating_text(player, "+" + str(coin_value) + " coin", Color(1.0, 0.84, 0.0), 0.5, 0.5, coin_texture, 6, 1, 0)
|
||||||
|
|
||||||
self.visible = false
|
self.visible = false
|
||||||
|
|
||||||
@@ -347,14 +348,53 @@ func _process_pickup_on_server(player: Node):
|
|||||||
if sfx_coin_collect and sfx_coin_collect.playing:
|
if sfx_coin_collect and sfx_coin_collect.playing:
|
||||||
await sfx_coin_collect.finished
|
await sfx_coin_collect.finished
|
||||||
queue_free()
|
queue_free()
|
||||||
LootType.APPLE, LootType.BANANA, LootType.CHERRY:
|
LootType.APPLE:
|
||||||
if sfx_potion_collect:
|
if sfx_potion_collect:
|
||||||
sfx_potion_collect.play()
|
sfx_potion_collect.play()
|
||||||
# Heal player
|
# Heal player
|
||||||
|
var actual_heal = 0.0
|
||||||
if player.has_method("heal"):
|
if player.has_method("heal"):
|
||||||
|
actual_heal = heal_amount
|
||||||
player.heal(heal_amount)
|
player.heal(heal_amount)
|
||||||
# Show floating text
|
# Show floating text with item graphic and heal amount
|
||||||
_show_floating_text(player, "+" + str(int(heal_amount)), Color.GREEN)
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
|
_show_floating_text(player, "+" + str(int(actual_heal)) + " hp", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 11)
|
||||||
|
|
||||||
|
self.visible = false
|
||||||
|
|
||||||
|
# Wait for sound to finish before removing
|
||||||
|
if sfx_potion_collect and sfx_potion_collect.playing:
|
||||||
|
await sfx_potion_collect.finished
|
||||||
|
queue_free()
|
||||||
|
LootType.BANANA:
|
||||||
|
if sfx_potion_collect:
|
||||||
|
sfx_potion_collect.play()
|
||||||
|
# Heal player
|
||||||
|
var actual_heal = 0.0
|
||||||
|
if player.has_method("heal"):
|
||||||
|
actual_heal = heal_amount
|
||||||
|
player.heal(heal_amount)
|
||||||
|
# Show floating text with item graphic and heal amount
|
||||||
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
|
_show_floating_text(player, "+" + str(int(actual_heal)) + " hp", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 12)
|
||||||
|
|
||||||
|
self.visible = false
|
||||||
|
|
||||||
|
# Wait for sound to finish before removing
|
||||||
|
if sfx_potion_collect and sfx_potion_collect.playing:
|
||||||
|
await sfx_potion_collect.finished
|
||||||
|
queue_free()
|
||||||
|
LootType.CHERRY:
|
||||||
|
if sfx_potion_collect:
|
||||||
|
sfx_potion_collect.play()
|
||||||
|
# Heal player
|
||||||
|
var actual_heal = 0.0
|
||||||
|
if player.has_method("heal"):
|
||||||
|
actual_heal = heal_amount
|
||||||
|
player.heal(heal_amount)
|
||||||
|
# Show floating text with item graphic and heal amount
|
||||||
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
|
_show_floating_text(player, "+" + str(int(actual_heal)) + " hp", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 13)
|
||||||
|
|
||||||
self.visible = false
|
self.visible = false
|
||||||
|
|
||||||
@@ -365,9 +405,12 @@ func _process_pickup_on_server(player: Node):
|
|||||||
LootType.KEY:
|
LootType.KEY:
|
||||||
if sfx_key_collect:
|
if sfx_key_collect:
|
||||||
sfx_key_collect.play()
|
sfx_key_collect.play()
|
||||||
# TODO: GIVE PLAYER KEY IN INVENTORY!
|
# Give player key in inventory
|
||||||
# Show floating text
|
if player.has_method("add_key"):
|
||||||
_show_floating_text(player, "+1 key", Color.YELLOW)
|
player.add_key(1)
|
||||||
|
# Show floating text with item graphic and text
|
||||||
|
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||||
|
_show_floating_text(player, "+1 key", Color.YELLOW, 0.5, 0.5, items_texture, 20, 14, (13 * 20) + 10)
|
||||||
|
|
||||||
self.visible = false
|
self.visible = false
|
||||||
|
|
||||||
@@ -458,23 +501,26 @@ func _sync_remove():
|
|||||||
visible = false
|
visible = false
|
||||||
|
|
||||||
# Wait for sound to finish before removing (if sound is playing)
|
# Wait for sound to finish before removing (if sound is playing)
|
||||||
var sound_playing = false
|
var _sound_playing = false
|
||||||
if loot_type == LootType.COIN and sfx_coin_collect and sfx_coin_collect.playing:
|
if loot_type == LootType.COIN and sfx_coin_collect and sfx_coin_collect.playing:
|
||||||
sound_playing = true
|
_sound_playing = true
|
||||||
await sfx_coin_collect.finished
|
await sfx_coin_collect.finished
|
||||||
elif loot_type in [LootType.APPLE, LootType.BANANA, LootType.CHERRY] and sfx_loot_collect and sfx_loot_collect.playing:
|
elif loot_type in [LootType.APPLE, LootType.BANANA, LootType.CHERRY] and sfx_loot_collect and sfx_loot_collect.playing:
|
||||||
sound_playing = true
|
_sound_playing = true
|
||||||
await sfx_loot_collect.finished
|
await sfx_loot_collect.finished
|
||||||
|
|
||||||
# Remove from scene
|
# Remove from scene
|
||||||
if not is_queued_for_deletion():
|
if not is_queued_for_deletion():
|
||||||
queue_free()
|
queue_free()
|
||||||
|
|
||||||
func _show_floating_text(player: Node, text: String, color: Color):
|
func _show_floating_text(player: Node, text: String, color: Color, show_time: float = 0.5, fade_time: float = 0.5, item_texture: Texture2D = null, sprite_hframes: int = 1, sprite_vframes: int = 1, sprite_frame: int = 0):
|
||||||
# Create floating text above player
|
# Create floating text and item graphic above player's head
|
||||||
|
# Shows for show_time seconds, then fades out over fade_time seconds
|
||||||
var floating_text_scene = preload("res://scenes/floating_text.tscn")
|
var floating_text_scene = preload("res://scenes/floating_text.tscn")
|
||||||
if floating_text_scene:
|
if floating_text_scene and player and is_instance_valid(player):
|
||||||
var floating_text = floating_text_scene.instantiate()
|
var floating_text = floating_text_scene.instantiate()
|
||||||
player.get_parent().add_child(floating_text)
|
var parent = player.get_parent()
|
||||||
floating_text.global_position = player.global_position + Vector2(0, -20)
|
if parent:
|
||||||
floating_text.setup(text, color)
|
parent.add_child(floating_text)
|
||||||
|
floating_text.global_position = player.global_position + Vector2(0, -20)
|
||||||
|
floating_text.setup(text, color, show_time, fade_time, item_texture, sprite_hframes, sprite_vframes, sprite_frame)
|
||||||
|
|||||||
@@ -107,6 +107,9 @@ var coins: int:
|
|||||||
if character_stats:
|
if character_stats:
|
||||||
character_stats.coin = value
|
character_stats.coin = value
|
||||||
|
|
||||||
|
# Key inventory
|
||||||
|
var keys: int = 0 # Number of keys the player has
|
||||||
|
|
||||||
# Animation system
|
# Animation system
|
||||||
enum Direction {
|
enum Direction {
|
||||||
DOWN = 0,
|
DOWN = 0,
|
||||||
@@ -2065,7 +2068,10 @@ func take_damage(amount: float, attacker_position: Vector2):
|
|||||||
tween.tween_property(sprite_body, "modulate", Color.RED, 0.1)
|
tween.tween_property(sprite_body, "modulate", Color.RED, 0.1)
|
||||||
tween.tween_property(sprite_body, "modulate", Color.WHITE, 0.1)
|
tween.tween_property(sprite_body, "modulate", Color.WHITE, 0.1)
|
||||||
|
|
||||||
# Sync damage visual effects to other clients
|
# Show damage number (red, using dmg_numbers.png font)
|
||||||
|
_show_damage_number(amount, attacker_position)
|
||||||
|
|
||||||
|
# Sync damage visual effects to other clients (including damage numbers)
|
||||||
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
if multiplayer.has_multiplayer_peer() and can_send_rpcs and is_inside_tree():
|
||||||
_sync_damage.rpc(amount, attacker_position)
|
_sync_damage.rpc(amount, attacker_position)
|
||||||
|
|
||||||
@@ -2351,7 +2357,59 @@ func heal(amount: float):
|
|||||||
current_health = min(current_health + amount, max_health)
|
current_health = min(current_health + amount, max_health)
|
||||||
print(name, " healed for ", amount, " HP! Health: ", current_health, "/", max_health)
|
print(name, " healed for ", amount, " HP! Health: ", current_health, "/", max_health)
|
||||||
|
|
||||||
|
func add_key(amount: int = 1):
|
||||||
|
keys += amount
|
||||||
|
print(name, " picked up ", amount, " key(s)! Total keys: ", keys)
|
||||||
|
|
||||||
|
func has_key() -> bool:
|
||||||
|
return keys > 0
|
||||||
|
|
||||||
|
func use_key():
|
||||||
|
if keys > 0:
|
||||||
|
keys -= 1
|
||||||
|
print(name, " used a key! Remaining keys: ", keys)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
@rpc("authority", "reliable")
|
@rpc("authority", "reliable")
|
||||||
|
func _show_damage_number(amount: float, from_position: Vector2):
|
||||||
|
# Show damage number (red, using dmg_numbers.png font) above player
|
||||||
|
# Only show if damage > 0
|
||||||
|
if amount <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
var damage_number_scene = preload("res://scenes/damage_number.tscn")
|
||||||
|
if not damage_number_scene:
|
||||||
|
return
|
||||||
|
|
||||||
|
var damage_label = damage_number_scene.instantiate()
|
||||||
|
if not damage_label:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Set damage text and red color
|
||||||
|
damage_label.label = str(int(amount))
|
||||||
|
damage_label.color = Color.RED
|
||||||
|
|
||||||
|
# Calculate direction from attacker (slight upward variation)
|
||||||
|
var direction_from_attacker = (global_position - from_position).normalized()
|
||||||
|
# Add slight upward bias
|
||||||
|
direction_from_attacker = direction_from_attacker.lerp(Vector2(0, -1), 0.5).normalized()
|
||||||
|
damage_label.direction = direction_from_attacker
|
||||||
|
|
||||||
|
# Position above player's head
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if game_world:
|
||||||
|
var entities_node = game_world.get_node_or_null("Entities")
|
||||||
|
if entities_node:
|
||||||
|
entities_node.add_child(damage_label)
|
||||||
|
damage_label.global_position = global_position + Vector2(0, -16) # Above player head
|
||||||
|
else:
|
||||||
|
get_tree().current_scene.add_child(damage_label)
|
||||||
|
damage_label.global_position = global_position + Vector2(0, -16)
|
||||||
|
else:
|
||||||
|
get_tree().current_scene.add_child(damage_label)
|
||||||
|
damage_label.global_position = global_position + Vector2(0, -16)
|
||||||
|
|
||||||
func _sync_damage(_amount: float, attacker_position: Vector2):
|
func _sync_damage(_amount: float, attacker_position: Vector2):
|
||||||
# This RPC only syncs visual effects, not damage application
|
# This RPC only syncs visual effects, not damage application
|
||||||
# (damage is already applied via rpc_take_damage)
|
# (damage is already applied via rpc_take_damage)
|
||||||
|
|||||||
605
src/scripts/room_trigger.gd
Normal file
605
src/scripts/room_trigger.gd
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
extends Area2D
|
||||||
|
|
||||||
|
# Room Trigger - Detects when players enter/exit a room
|
||||||
|
|
||||||
|
var room: Dictionary = {} # Room data this trigger covers
|
||||||
|
var players_in_room: Array = [] # Players currently in the room
|
||||||
|
var doors_in_room: Array = [] # Doors that belong to this room
|
||||||
|
var enemies_in_room: Array = [] # Enemies in this room
|
||||||
|
var floor_switches_in_room: Array = [] # Floor switches in this room
|
||||||
|
var enemy_spawners: Array = [] # Enemy spawners for this room
|
||||||
|
var enemies_spawned: bool = false # True if enemies have been spawned for this room
|
||||||
|
var debug_label: Label = null # Debug label showing puzzle type and trigger status
|
||||||
|
var room_entered: bool = false # True if player has entered this room
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Set collision mask to detect players
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 1 # Detect players (layer 1)
|
||||||
|
|
||||||
|
# Connect signals
|
||||||
|
body_entered.connect(_on_body_entered)
|
||||||
|
body_exited.connect(_on_body_exited)
|
||||||
|
|
||||||
|
# Create debug label to show puzzle type and trigger status
|
||||||
|
_create_debug_label()
|
||||||
|
|
||||||
|
# Find doors, enemies, and switches in this room (deferred to ensure doors are fully initialized)
|
||||||
|
# This ensures doors have their blocking_room set before we try to find them
|
||||||
|
call_deferred("_find_room_entities")
|
||||||
|
|
||||||
|
func _on_body_entered(body):
|
||||||
|
# Player entered the room
|
||||||
|
if body.is_in_group("player"):
|
||||||
|
if not body in players_in_room:
|
||||||
|
players_in_room.append(body)
|
||||||
|
_on_player_entered_room(body)
|
||||||
|
|
||||||
|
func _on_body_exited(body):
|
||||||
|
# Player left the room
|
||||||
|
if body.is_in_group("player"):
|
||||||
|
if body in players_in_room:
|
||||||
|
players_in_room.erase(body)
|
||||||
|
_on_player_exited_room(body)
|
||||||
|
|
||||||
|
func _on_player_entered_room(player: Node):
|
||||||
|
# Handle player entering room
|
||||||
|
print("Player ", player.name, " entered room at ", room.x, ", ", room.y)
|
||||||
|
print("RoomTrigger: This trigger is for room (", room.x, ", ", room.y, ", ", room.w, "x", room.h, ")")
|
||||||
|
print("RoomTrigger: Found ", doors_in_room.size(), " doors in this room")
|
||||||
|
|
||||||
|
# Mark room as entered and update debug label
|
||||||
|
room_entered = true
|
||||||
|
_update_debug_label()
|
||||||
|
|
||||||
|
# Check if this room has a puzzle (has blocking doors IN this room)
|
||||||
|
# If this room has a puzzle, close the doors IN this room (blocking exits)
|
||||||
|
# doors_in_room contains doors where room1 == this room OR blocking_room == this room (doors IN this room)
|
||||||
|
for door in doors_in_room:
|
||||||
|
if not is_instance_valid(door):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# CRITICAL: Verify this door is actually IN this room
|
||||||
|
# A door is IN a room if door.room1 == this room OR door.blocking_room == this room
|
||||||
|
var door_room1 = door.room1 if door.room1 else {}
|
||||||
|
var door_blocking_room = door.blocking_room if door.blocking_room else {}
|
||||||
|
var door_in_this_room = false
|
||||||
|
|
||||||
|
# Check if door.room1 matches this room (door starts FROM this room)
|
||||||
|
if door_room1 and not door_room1.is_empty():
|
||||||
|
door_in_this_room = (door_room1.x == room.x and door_room1.y == room.y and \
|
||||||
|
door_room1.w == room.w and door_room1.h == room.h)
|
||||||
|
|
||||||
|
# Also check blocking_room
|
||||||
|
if not door_in_this_room and door_blocking_room and not door_blocking_room.is_empty():
|
||||||
|
door_in_this_room = (door_blocking_room.x == room.x and door_blocking_room.y == room.y and \
|
||||||
|
door_blocking_room.w == room.w and door_blocking_room.h == room.h)
|
||||||
|
|
||||||
|
if not door_in_this_room:
|
||||||
|
# Door is NOT in this room - DO NOT call it!
|
||||||
|
print("RoomTrigger: ERROR - Door ", door.name, " is NOT in room (", room.x, ", ", room.y, ")!")
|
||||||
|
print("RoomTrigger: Door room1: (", door_room1.x if door_room1 else "none", ", ", door_room1.y if door_room1 else "none", ")")
|
||||||
|
print("RoomTrigger: Door blocking_room: (", door_blocking_room.x if door_blocking_room else "none", ", ", door_blocking_room.y if door_blocking_room else "none", ")")
|
||||||
|
print("RoomTrigger: Removing from this trigger's doors list!")
|
||||||
|
doors_in_room.erase(door)
|
||||||
|
if door.room_trigger_area == self:
|
||||||
|
door.room_trigger_area = null
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Door is in this room - verify it's connected to this trigger
|
||||||
|
if door.room_trigger_area != self:
|
||||||
|
door.room_trigger_area = self
|
||||||
|
|
||||||
|
# Call the door's _on_room_entered to close it (if puzzle not solved)
|
||||||
|
if door.has_method("_on_room_entered"):
|
||||||
|
door._on_room_entered(player)
|
||||||
|
|
||||||
|
# Spawn enemies if this room has a spawner
|
||||||
|
print("RoomTrigger: About to call _spawn_room_enemies()...")
|
||||||
|
_spawn_room_enemies()
|
||||||
|
print("RoomTrigger: Finished _spawn_room_enemies()")
|
||||||
|
|
||||||
|
func _on_player_exited_room(player: Node):
|
||||||
|
# Handle player leaving room
|
||||||
|
print("Player ", player.name, " exited room at ", room.x, ", ", room.y)
|
||||||
|
|
||||||
|
func _find_room_entities():
|
||||||
|
# Find all doors, enemies, and switches that belong to this room
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if not game_world:
|
||||||
|
return
|
||||||
|
|
||||||
|
var entities_node = game_world.get_node_or_null("Entities")
|
||||||
|
if not entities_node:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find doors - search in Entities node
|
||||||
|
# CRITICAL: Find doors where room1 == THIS room OR blocking_room == THIS room
|
||||||
|
# Blocking doors are IN the puzzle room (they lead OUT OF this room)
|
||||||
|
# When you enter this room, doors IN this room should close (blocking exits)
|
||||||
|
print("RoomTrigger: Finding doors for room (", room.x, ", ", room.y, ", ", room.w, "x", room.h, ")")
|
||||||
|
var _total_blocking_doors = 0
|
||||||
|
for child in entities_node.get_children():
|
||||||
|
if child.is_in_group("blocking_door") or child.name.begins_with("BlockingDoor_"):
|
||||||
|
_total_blocking_doors += 1
|
||||||
|
if not is_instance_valid(child):
|
||||||
|
print("RoomTrigger: Door ", child.name, " is invalid, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if door is IN this room (room1 == this room OR blocking_room == this room)
|
||||||
|
var door_in_this_room = false
|
||||||
|
var door_room1 = child.room1 if child.room1 else {}
|
||||||
|
var door_blocking_room = child.blocking_room if child.blocking_room else {}
|
||||||
|
|
||||||
|
# CRITICAL: For blocking doors (StoneDoor/GateDoor), BOTH room1 AND blocking_room should match this room
|
||||||
|
# For KeyDoors, only room1 needs to match
|
||||||
|
var is_blocking_door = (child.type == "StoneDoor" or child.type == "GateDoor")
|
||||||
|
|
||||||
|
# Check room1 first (door starts FROM this room)
|
||||||
|
if door_room1 and not door_room1.is_empty():
|
||||||
|
var room1_matches = (door_room1.x == room.x and door_room1.y == room.y and \
|
||||||
|
door_room1.w == room.w and door_room1.h == room.h)
|
||||||
|
if room1_matches:
|
||||||
|
door_in_this_room = true
|
||||||
|
print("RoomTrigger: Door ", child.name, " room1 matches this room (door IN this room)")
|
||||||
|
|
||||||
|
# For blocking doors, also verify blocking_room matches (it should match room1)
|
||||||
|
if door_in_this_room and is_blocking_door and door_blocking_room and not door_blocking_room.is_empty():
|
||||||
|
var blocking_matches = (door_blocking_room.x == room.x and door_blocking_room.y == room.y and \
|
||||||
|
door_blocking_room.w == room.w and door_blocking_room.h == room.h)
|
||||||
|
if not blocking_matches:
|
||||||
|
# Blocking door's blocking_room doesn't match - this is an error!
|
||||||
|
print("RoomTrigger: ERROR - Blocking door ", child.name, " room1 matches but blocking_room (", door_blocking_room.x, ", ", door_blocking_room.y, ") doesn't match this room (", room.x, ", ", room.y, ")!")
|
||||||
|
door_in_this_room = false # Reject this door
|
||||||
|
else:
|
||||||
|
print("RoomTrigger: Blocking door ", child.name, " blocking_room also matches this room (verified)")
|
||||||
|
|
||||||
|
# For non-blocking doors or if room1 didn't match, check blocking_room as fallback
|
||||||
|
if not door_in_this_room and door_blocking_room and not door_blocking_room.is_empty():
|
||||||
|
door_in_this_room = (door_blocking_room.x == room.x and door_blocking_room.y == room.y and \
|
||||||
|
door_blocking_room.w == room.w and door_blocking_room.h == room.h)
|
||||||
|
if door_in_this_room:
|
||||||
|
print("RoomTrigger: Door ", child.name, " blocking_room matches this room (fallback check)")
|
||||||
|
else:
|
||||||
|
print("RoomTrigger: Door ", child.name, " blocking_room (", door_blocking_room.x if door_blocking_room else "none", ", ", door_blocking_room.y if door_blocking_room else "none", ") doesn't match this room (", room.x, ", ", room.y, ")")
|
||||||
|
|
||||||
|
if not door_room1 and not door_blocking_room:
|
||||||
|
print("RoomTrigger: Door ", child.name, " has no room1 or blocking_room set!")
|
||||||
|
|
||||||
|
if door_in_this_room:
|
||||||
|
# This door is IN THIS room (blocks exits from this room) - add it to this trigger
|
||||||
|
# CRITICAL: Check if door is already connected to a different trigger FIRST
|
||||||
|
# This prevents doors from being connected to multiple triggers
|
||||||
|
if child.room_trigger_area and child.room_trigger_area != self:
|
||||||
|
# Door already connected to a different trigger - this is an ERROR!
|
||||||
|
var other_trigger_room = child.room_trigger_area.room if child.room_trigger_area.room else {}
|
||||||
|
var other_room_str = "(" + str(other_trigger_room.x if other_trigger_room else "none") + ", " + str(other_trigger_room.y if other_trigger_room else "none") + ")"
|
||||||
|
var this_room_str = "(" + str(room.x) + ", " + str(room.y) + ")"
|
||||||
|
|
||||||
|
# Debug: Print door's room info to understand why it's matching multiple rooms
|
||||||
|
print("RoomTrigger: WARNING - Door ", child.name, " already connected to trigger for room ", other_room_str, "!")
|
||||||
|
print("RoomTrigger: Door room1: (", door_room1.x if door_room1 else "none", ", ", door_room1.y if door_room1 else "none", ")")
|
||||||
|
print("RoomTrigger: Door blocking_room: (", door_blocking_room.x if door_blocking_room else "none", ", ", door_blocking_room.y if door_blocking_room else "none", ")")
|
||||||
|
print("RoomTrigger: Current trigger room: ", this_room_str)
|
||||||
|
print("RoomTrigger: Skipping this door - it belongs to the other trigger!")
|
||||||
|
|
||||||
|
# Don't add to this trigger if already connected to another trigger
|
||||||
|
if child in doors_in_room:
|
||||||
|
doors_in_room.erase(child)
|
||||||
|
continue # Skip this door entirely
|
||||||
|
|
||||||
|
# Door is not connected to another trigger - safe to add
|
||||||
|
# CRITICAL: Only add if not already in the list (avoid duplicates)
|
||||||
|
if not child in doors_in_room:
|
||||||
|
doors_in_room.append(child)
|
||||||
|
print("RoomTrigger: Added door ", child.name, " that is IN room (", room.x, ", ", room.y, ") to this trigger")
|
||||||
|
|
||||||
|
# Set door's room trigger reference (should be null at this point, but set it anyway)
|
||||||
|
if not child.room_trigger_area:
|
||||||
|
child.room_trigger_area = self
|
||||||
|
else:
|
||||||
|
# This door is NOT in this room - ensure it's not in this trigger's list
|
||||||
|
# This prevents doors from being accidentally connected to wrong triggers
|
||||||
|
if child in doors_in_room:
|
||||||
|
print("RoomTrigger: Removing door ", child.name, " from this trigger - it's not in this room (", room.x, ", ", room.y, ")")
|
||||||
|
doors_in_room.erase(child)
|
||||||
|
if child.room_trigger_area == self:
|
||||||
|
print("RoomTrigger: Disconnecting door ", child.name, " from this trigger - wrong room!")
|
||||||
|
child.room_trigger_area = null
|
||||||
|
|
||||||
|
print("RoomTrigger: Found ", doors_in_room.size(), " doors for room (", room.x, ", ", room.y, ")")
|
||||||
|
|
||||||
|
# Find enemies
|
||||||
|
for child in entities_node.get_children():
|
||||||
|
if child.is_in_group("enemy"):
|
||||||
|
# Check if enemy is in this room
|
||||||
|
if child.has_meta("room"):
|
||||||
|
var enemy_room = child.get_meta("room")
|
||||||
|
if enemy_room == room:
|
||||||
|
enemies_in_room.append(child)
|
||||||
|
else:
|
||||||
|
# Check by position
|
||||||
|
var tile_size = 16
|
||||||
|
var enemy_tile_x = int(child.global_position.x / tile_size)
|
||||||
|
var enemy_tile_y = int(child.global_position.y / tile_size)
|
||||||
|
var room_min_x = room.x + 2
|
||||||
|
var room_max_x = room.x + room.w - 2
|
||||||
|
var room_min_y = room.y + 2
|
||||||
|
var room_max_y = room.y + room.h - 2
|
||||||
|
|
||||||
|
if enemy_tile_x >= room_min_x and enemy_tile_x < room_max_x and \
|
||||||
|
enemy_tile_y >= room_min_y and enemy_tile_y < room_max_y:
|
||||||
|
enemies_in_room.append(child)
|
||||||
|
|
||||||
|
# Find floor switches
|
||||||
|
for child in get_tree().get_nodes_in_group("floor_switch"):
|
||||||
|
if is_instance_valid(child):
|
||||||
|
# Check if switch is in this room
|
||||||
|
var tile_size = 16
|
||||||
|
var switch_tile_x = int(child.global_position.x / tile_size)
|
||||||
|
var switch_tile_y = int(child.global_position.y / tile_size)
|
||||||
|
var room_min_x = room.x + 2
|
||||||
|
var room_max_x = room.x + room.w - 2
|
||||||
|
var room_min_y = room.y + 2
|
||||||
|
var room_max_y = room.y + room.h - 2
|
||||||
|
|
||||||
|
if switch_tile_x >= room_min_x and switch_tile_x < room_max_x and \
|
||||||
|
switch_tile_y >= room_min_y and switch_tile_y < room_max_y:
|
||||||
|
floor_switches_in_room.append(child)
|
||||||
|
|
||||||
|
# Update debug label after finding all entities
|
||||||
|
call_deferred("_update_debug_label")
|
||||||
|
|
||||||
|
func _spawn_room_enemies():
|
||||||
|
print("RoomTrigger: ===== _spawn_room_enemies() CALLED for room (", room.x, ", ", room.y, ") =====")
|
||||||
|
|
||||||
|
# Spawn enemies when player enters room (if room has spawners and not already spawned)
|
||||||
|
if enemies_spawned:
|
||||||
|
print("RoomTrigger: Already spawned enemies, skipping...")
|
||||||
|
return # Already spawned enemies
|
||||||
|
|
||||||
|
# CRITICAL: Remove any existing smoke puffs before spawning new ones
|
||||||
|
_cleanup_smoke_puffs()
|
||||||
|
|
||||||
|
# Find enemy spawners for this room
|
||||||
|
_find_room_spawners()
|
||||||
|
|
||||||
|
print("RoomTrigger: ===== Found ", enemy_spawners.size(), " spawners for room (", room.x, ", ", room.y, ") =====")
|
||||||
|
|
||||||
|
# Spawn enemies from all spawners in this room (only once)
|
||||||
|
if enemy_spawners.size() > 0:
|
||||||
|
for spawner in enemy_spawners:
|
||||||
|
if not is_instance_valid(spawner):
|
||||||
|
print("RoomTrigger: WARNING - Invalid spawner found, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not spawner.has_method("spawn_enemy"):
|
||||||
|
print("RoomTrigger: WARNING - Spawner ", spawner.name, " doesn't have spawn_enemy method!")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# CRITICAL: Verify spawner has enemy scenes set
|
||||||
|
if "enemy_scenes" in spawner:
|
||||||
|
if spawner.enemy_scenes.size() == 0:
|
||||||
|
print("RoomTrigger: ERROR - Spawner ", spawner.name, " has empty enemy_scenes array! Cannot spawn!")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
print("RoomTrigger: Spawner ", spawner.name, " has ", spawner.enemy_scenes.size(), " enemy scenes available")
|
||||||
|
else:
|
||||||
|
print("RoomTrigger: ERROR - Spawner ", spawner.name, " has no enemy_scenes property!")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# CRITICAL: Verify spawner is on server (authority) - only server can spawn
|
||||||
|
if multiplayer.has_multiplayer_peer() and not spawner.is_multiplayer_authority():
|
||||||
|
print("RoomTrigger: WARNING - Spawner ", spawner.name, " is not multiplayer authority! Cannot spawn on client!")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print("RoomTrigger: Calling spawn_enemy() on spawner ", spawner.name, " at ", spawner.global_position)
|
||||||
|
# Spawn enemies from this spawner (spawner will handle max_enemies check)
|
||||||
|
# NOTE: spawn_enemy() is async (uses await), so we don't await it here - it will execute asynchronously
|
||||||
|
spawner.spawn_enemy()
|
||||||
|
|
||||||
|
enemies_spawned = true
|
||||||
|
print("RoomTrigger: Called spawn_enemy() on ", enemy_spawners.size(), " spawners in room at ", room.x, ", ", room.y)
|
||||||
|
|
||||||
|
# Update debug label
|
||||||
|
_update_debug_label()
|
||||||
|
|
||||||
|
# Wait a bit for enemies to actually spawn (since spawn_enemy() waits for smoke puffs first)
|
||||||
|
# Give it enough time for smoke puffs to finish (2.4s) plus a small buffer
|
||||||
|
await get_tree().create_timer(3.0).timeout
|
||||||
|
_find_room_entities() # Refresh enemy list after spawning completes
|
||||||
|
_update_debug_label() # Update again after enemies spawn
|
||||||
|
else:
|
||||||
|
print("RoomTrigger: No spawners found for room (", room.x, ", ", room.y, ")")
|
||||||
|
_update_debug_label()
|
||||||
|
|
||||||
|
func _cleanup_smoke_puffs():
|
||||||
|
# Remove all existing smoke puffs in the scene before spawning new ones
|
||||||
|
var entities_node = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if not entities_node:
|
||||||
|
entities_node = get_node_or_null("/root/GameWorld/Entities")
|
||||||
|
|
||||||
|
if not entities_node:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find and remove all smoke puffs
|
||||||
|
var smoke_puffs_removed = 0
|
||||||
|
for child in entities_node.get_children():
|
||||||
|
if child.is_in_group("smoke_puff") or child.name.begins_with("SmokePuff"):
|
||||||
|
print("RoomTrigger: Removing existing smoke puff: ", child.name)
|
||||||
|
child.queue_free()
|
||||||
|
smoke_puffs_removed += 1
|
||||||
|
|
||||||
|
if smoke_puffs_removed > 0:
|
||||||
|
print("RoomTrigger: Cleaned up ", smoke_puffs_removed, " existing smoke puffs before spawning")
|
||||||
|
|
||||||
|
func _find_room_spawners():
|
||||||
|
# CRITICAL: Clear the list first to avoid accumulating old spawners
|
||||||
|
enemy_spawners.clear()
|
||||||
|
|
||||||
|
# Find enemy spawners in this room
|
||||||
|
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||||
|
if not game_world:
|
||||||
|
print("RoomTrigger: ERROR - No game_world found when searching for spawners!")
|
||||||
|
return
|
||||||
|
|
||||||
|
var entities_node = game_world.get_node_or_null("Entities")
|
||||||
|
if not entities_node:
|
||||||
|
print("RoomTrigger: ERROR - No Entities node found when searching for spawners!")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("RoomTrigger: ===== Searching for spawners in room (", room.x, ", ", room.y, ", ", room.w, "x", room.h, ") =====")
|
||||||
|
print("RoomTrigger: Entities node has ", entities_node.get_child_count(), " children")
|
||||||
|
|
||||||
|
# Search for spawners (they might be direct children of Entities or in a Spawners node)
|
||||||
|
var found_spawners_count = 0
|
||||||
|
for child in entities_node.get_children():
|
||||||
|
if child.name.begins_with("EnemySpawner_") or child.is_in_group("enemy_spawner"):
|
||||||
|
found_spawners_count += 1
|
||||||
|
print("RoomTrigger: Checking spawner: ", child.name, " at ", child.global_position)
|
||||||
|
|
||||||
|
if not is_instance_valid(child):
|
||||||
|
print("RoomTrigger: Spawner is invalid, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
var spawner_in_room = false
|
||||||
|
|
||||||
|
# First check if spawner has room metadata matching this room
|
||||||
|
if child.has_meta("room"):
|
||||||
|
var spawner_room = child.get_meta("room")
|
||||||
|
print("RoomTrigger: Spawner has room metadata: (", spawner_room.x if spawner_room and not spawner_room.is_empty() else "none", ", ", spawner_room.y if spawner_room and not spawner_room.is_empty() else "none", ")")
|
||||||
|
if spawner_room and not spawner_room.is_empty():
|
||||||
|
# Compare rooms by position and size
|
||||||
|
if spawner_room.x == room.x and spawner_room.y == room.y and \
|
||||||
|
spawner_room.w == room.w and spawner_room.h == room.h:
|
||||||
|
spawner_in_room = true
|
||||||
|
print("RoomTrigger: ✓ Spawner room matches this trigger room!")
|
||||||
|
else:
|
||||||
|
print("RoomTrigger: ✗ Spawner room doesn't match - spawner: (", spawner_room.x, ",", spawner_room.y, ",", spawner_room.w, "x", spawner_room.h, "), trigger: (", room.x, ",", room.y, ",", room.w, "x", room.h, ")")
|
||||||
|
|
||||||
|
# Also check blocking_room metadata (fallback)
|
||||||
|
if not spawner_in_room and child.has_meta("blocking_room"):
|
||||||
|
var blocking_room = child.get_meta("blocking_room")
|
||||||
|
print("RoomTrigger: Spawner has blocking_room metadata: (", blocking_room.x if blocking_room and not blocking_room.is_empty() else "none", ", ", blocking_room.y if blocking_room and not blocking_room.is_empty() else "none", ")")
|
||||||
|
if blocking_room and not blocking_room.is_empty():
|
||||||
|
# Compare rooms by position and size
|
||||||
|
if blocking_room.x == room.x and blocking_room.y == room.y and \
|
||||||
|
blocking_room.w == room.w and blocking_room.h == room.h:
|
||||||
|
spawner_in_room = true
|
||||||
|
print("RoomTrigger: ✓ Spawner blocking_room matches this trigger room!")
|
||||||
|
else:
|
||||||
|
print("RoomTrigger: ✗ Spawner blocking_room doesn't match - spawner: (", blocking_room.x, ",", blocking_room.y, ",", blocking_room.w, "x", blocking_room.h, "), trigger: (", room.x, ",", room.y, ",", room.w, "x", room.h, ")")
|
||||||
|
|
||||||
|
# Also check by position (fallback if no room metadata)
|
||||||
|
if not spawner_in_room:
|
||||||
|
var tile_size = 16
|
||||||
|
var spawner_tile_x = int(child.global_position.x / tile_size)
|
||||||
|
var spawner_tile_y = int(child.global_position.y / tile_size)
|
||||||
|
var room_min_x = room.x + 2
|
||||||
|
var room_max_x = room.x + room.w - 2
|
||||||
|
var room_min_y = room.y + 2
|
||||||
|
var room_max_y = room.y + room.h - 2
|
||||||
|
|
||||||
|
print("RoomTrigger: Checking by position: spawner at tile (", spawner_tile_x, ", ", spawner_tile_y, "), room bounds: (", room_min_x, "-", room_max_x, ", ", room_min_y, "-", room_max_y, ")")
|
||||||
|
|
||||||
|
if spawner_tile_x >= room_min_x and spawner_tile_x < room_max_x and \
|
||||||
|
spawner_tile_y >= room_min_y and spawner_tile_y < room_max_y:
|
||||||
|
spawner_in_room = true
|
||||||
|
print("RoomTrigger: Spawner is within room bounds!")
|
||||||
|
|
||||||
|
if spawner_in_room and not child in enemy_spawners:
|
||||||
|
enemy_spawners.append(child)
|
||||||
|
print("RoomTrigger: ✓ Added spawner ", child.name, " to this room trigger")
|
||||||
|
elif spawner_in_room:
|
||||||
|
print("RoomTrigger: Spawner already in list, skipping")
|
||||||
|
else:
|
||||||
|
print("RoomTrigger: Spawner is NOT in this room, skipping")
|
||||||
|
|
||||||
|
print("RoomTrigger: Total spawners found: ", found_spawners_count, ", spawners in this room: ", enemy_spawners.size())
|
||||||
|
|
||||||
|
# Update debug label after finding entities
|
||||||
|
call_deferred("_update_debug_label")
|
||||||
|
|
||||||
|
func _create_debug_label():
|
||||||
|
# Create a debug label to show puzzle type and trigger status above the room
|
||||||
|
var label = Label.new()
|
||||||
|
label.name = "DebugLabel"
|
||||||
|
|
||||||
|
# Position label at center-top of the room (relative to Area2D)
|
||||||
|
var tile_size = 16
|
||||||
|
var room_world_w = (room.w - 4) * tile_size
|
||||||
|
|
||||||
|
# Position at center of room, slightly above top edge
|
||||||
|
label.position = Vector2(room_world_w / 2.0 - 100, -40) # Centered horizontally, 40px above
|
||||||
|
label.size = Vector2(200, 50) # Wide and tall enough for text
|
||||||
|
|
||||||
|
# Set high z-index to be above everything
|
||||||
|
label.z_index = 1000
|
||||||
|
|
||||||
|
# Enable autowrap so text doesn't overflow
|
||||||
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
|
||||||
|
# Load standard_font.png as bitmap font (it's imported as FontFile resource)
|
||||||
|
# The PNG is automatically imported as a FontFile when using font_data_image importer
|
||||||
|
# Use ResourceLoader.load() to ensure we get the FontFile resource
|
||||||
|
var standard_font_resource = ResourceLoader.load("res://assets/fonts/standard_font.png", "FontFile")
|
||||||
|
if standard_font_resource and standard_font_resource is FontFile:
|
||||||
|
label.add_theme_font_override("font", standard_font_resource)
|
||||||
|
label.add_theme_font_size_override("font_size", 8)
|
||||||
|
else:
|
||||||
|
# Fallback: just set smaller font size if font can't be loaded
|
||||||
|
label.add_theme_font_size_override("font_size", 8)
|
||||||
|
if not standard_font_resource:
|
||||||
|
print("RoomTrigger: WARNING - Could not load standard_font.png font resource!")
|
||||||
|
|
||||||
|
# Style the label
|
||||||
|
label.add_theme_color_override("font_color", Color.YELLOW)
|
||||||
|
label.add_theme_color_override("font_shadow_color", Color.BLACK)
|
||||||
|
label.add_theme_constant_override("shadow_offset_x", 1)
|
||||||
|
label.add_theme_constant_override("shadow_offset_y", 1)
|
||||||
|
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||||
|
|
||||||
|
# Set initial text
|
||||||
|
label.text = "Room (%d,%d)\nPuzzle: ...\nStatus: Not entered" % [room.x, room.y]
|
||||||
|
|
||||||
|
debug_label = label
|
||||||
|
add_child(label)
|
||||||
|
|
||||||
|
# Update label after entities are found (deferred)
|
||||||
|
call_deferred("_update_debug_label")
|
||||||
|
|
||||||
|
func _update_debug_label():
|
||||||
|
# Update the debug label with current puzzle type and trigger status
|
||||||
|
if not debug_label:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Determine puzzle type from doors in this room
|
||||||
|
var puzzle_type = "none"
|
||||||
|
var puzzle_info = ""
|
||||||
|
|
||||||
|
# Check doors to determine puzzle type
|
||||||
|
for door in doors_in_room:
|
||||||
|
if not is_instance_valid(door):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if door is a blocking door
|
||||||
|
if door.has_method("_on_room_entered"):
|
||||||
|
if "type" in door:
|
||||||
|
if door.type == "KeyDoor":
|
||||||
|
puzzle_type = "keydoor"
|
||||||
|
puzzle_info = "KeyDoor"
|
||||||
|
break # KeyDoor takes priority
|
||||||
|
elif door.type == "StoneDoor" or door.type == "GateDoor":
|
||||||
|
# Check what puzzle type this door requires
|
||||||
|
# These are regular properties defined in door.gd (lines 35-36), so they should always exist
|
||||||
|
# Access properties directly - they're defined as var in the class
|
||||||
|
var door_requires_enemies = false
|
||||||
|
var door_requires_switch = false
|
||||||
|
|
||||||
|
# Access properties directly (they're defined in door.gd as var requires_enemies and requires_switch)
|
||||||
|
# Since they're class properties, they should always exist, so just access them
|
||||||
|
door_requires_enemies = door.requires_enemies
|
||||||
|
door_requires_switch = door.requires_switch
|
||||||
|
|
||||||
|
if door_requires_enemies:
|
||||||
|
puzzle_type = "enemy"
|
||||||
|
puzzle_info = "Enemy Spawner"
|
||||||
|
elif door_requires_switch:
|
||||||
|
# Check switch type by looking at connected switches
|
||||||
|
var has_pillar_switch = false
|
||||||
|
var has_walk_switch = false
|
||||||
|
for switch in floor_switches_in_room:
|
||||||
|
if is_instance_valid(switch):
|
||||||
|
# switch_type is an exported property in floor_switch.gd, so it should always exist
|
||||||
|
var switch_type_val = switch.switch_type
|
||||||
|
if switch_type_val == "pillar":
|
||||||
|
has_pillar_switch = true
|
||||||
|
elif switch_type_val == "walk":
|
||||||
|
has_walk_switch = true
|
||||||
|
|
||||||
|
if has_pillar_switch:
|
||||||
|
puzzle_type = "switch_pillar"
|
||||||
|
puzzle_info = "Pillar Switch"
|
||||||
|
elif has_walk_switch:
|
||||||
|
puzzle_type = "switch_walk"
|
||||||
|
puzzle_info = "Walk Switch"
|
||||||
|
else:
|
||||||
|
puzzle_type = "switch"
|
||||||
|
puzzle_info = "Switch"
|
||||||
|
else:
|
||||||
|
# Door exists but no puzzle info - show door type
|
||||||
|
if puzzle_type == "none":
|
||||||
|
puzzle_type = door.type.to_lower()
|
||||||
|
puzzle_info = door.type
|
||||||
|
|
||||||
|
# Also check switches and spawners directly
|
||||||
|
if floor_switches_in_room.size() > 0:
|
||||||
|
for switch in floor_switches_in_room:
|
||||||
|
if is_instance_valid(switch):
|
||||||
|
# switch_type is an exported property in floor_switch.gd, so it should always exist
|
||||||
|
var switch_type_val = switch.switch_type
|
||||||
|
if switch_type_val == "pillar":
|
||||||
|
puzzle_type = "switch_pillar"
|
||||||
|
puzzle_info = "Pillar Switch"
|
||||||
|
break
|
||||||
|
elif switch_type_val == "walk":
|
||||||
|
if puzzle_type != "switch_pillar": # Pillar takes priority
|
||||||
|
puzzle_type = "switch_walk"
|
||||||
|
puzzle_info = "Walk Switch"
|
||||||
|
|
||||||
|
if enemy_spawners.size() > 0:
|
||||||
|
if puzzle_type != "keydoor": # KeyDoor takes priority
|
||||||
|
puzzle_type = "enemy"
|
||||||
|
puzzle_info = "Enemy Spawner (%d)" % enemy_spawners.size()
|
||||||
|
|
||||||
|
# Determine status
|
||||||
|
var status_text = "ENTERED" if room_entered else "Not entered"
|
||||||
|
if enemies_spawned:
|
||||||
|
status_text += " | Spawned"
|
||||||
|
|
||||||
|
# Update label text
|
||||||
|
debug_label.text = "Room (%d,%d)\nPuzzle: %s\nStatus: %s" % [room.x, room.y, puzzle_info, status_text]
|
||||||
|
|
||||||
|
# Color code based on puzzle type
|
||||||
|
var color = Color.WHITE
|
||||||
|
match puzzle_type:
|
||||||
|
"enemy":
|
||||||
|
color = Color.RED
|
||||||
|
"switch_walk":
|
||||||
|
color = Color.GREEN
|
||||||
|
"switch_pillar":
|
||||||
|
color = Color.CYAN
|
||||||
|
"keydoor":
|
||||||
|
color = Color.YELLOW
|
||||||
|
_:
|
||||||
|
color = Color.GRAY
|
||||||
|
|
||||||
|
if room_entered:
|
||||||
|
color = color.lerp(Color.WHITE, 0.3) # Make entered rooms brighter
|
||||||
|
|
||||||
|
debug_label.add_theme_color_override("font_color", color)
|
||||||
|
|
||||||
|
# Update label position to stay at center-top of room
|
||||||
|
var tile_size = 16
|
||||||
|
var room_world_w = (room.w - 4) * tile_size
|
||||||
|
debug_label.position = Vector2(room_world_w / 2.0 - 100, -40)
|
||||||
|
|
||||||
|
func check_puzzle_state():
|
||||||
|
# Check if room puzzle is solved (all enemies defeated or switches activated)
|
||||||
|
# This is called by doors to check their state
|
||||||
|
var all_enemies_defeated = true
|
||||||
|
for enemy in enemies_in_room:
|
||||||
|
if is_instance_valid(enemy) and not enemy.is_dead:
|
||||||
|
all_enemies_defeated = false
|
||||||
|
break
|
||||||
|
|
||||||
|
var all_switches_activated = true
|
||||||
|
for switch in floor_switches_in_room:
|
||||||
|
if is_instance_valid(switch):
|
||||||
|
# is_activated is a variable, not a method
|
||||||
|
if not switch.is_activated:
|
||||||
|
all_switches_activated = false
|
||||||
|
break
|
||||||
|
|
||||||
|
return all_enemies_defeated or all_switches_activated
|
||||||
1
src/scripts/room_trigger.gd.uid
Normal file
1
src/scripts/room_trigger.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://mn3ighwoy0hi
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
extends Node2D
|
extends Node2D
|
||||||
|
|
||||||
# Smoke Puff Effect - Plays animation and fades out
|
# Smoke Puff Effect - Plays animation and fades out while moving slowly
|
||||||
|
|
||||||
@export var animation_speed: float = 10.0
|
@export var animation_speed: float = 10.0
|
||||||
@export var fade_duration: float = 0.5
|
@export var fade_duration: float = 0.5
|
||||||
|
@export var move_speed: float = 15.0 # Pixels per second movement speed
|
||||||
|
@export var move_duration: float = 1.5 # How long to move before starting fade
|
||||||
|
|
||||||
@onready var sprite: Sprite2D = $Sprite2D
|
@onready var sprite: Sprite2D = $Sprite2D
|
||||||
|
|
||||||
@@ -11,31 +13,89 @@ var current_frame: int = 0
|
|||||||
var frame_timer: float = 0.0
|
var frame_timer: float = 0.0
|
||||||
var total_frames: int = 4 # 4 frames per row
|
var total_frames: int = 4 # 4 frames per row
|
||||||
var puff_type: int = 0 # 0 or 1 for first or second row
|
var puff_type: int = 0 # 0 or 1 for first or second row
|
||||||
|
var move_direction: Vector2 = Vector2.ZERO # Direction to move in
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
|
# Add to group for easy cleanup
|
||||||
|
add_to_group("smoke_puff")
|
||||||
|
|
||||||
|
# Wait for sprite to be ready (ensure @onready variable is set)
|
||||||
|
await get_tree().process_frame
|
||||||
|
|
||||||
|
# Verify sprite exists
|
||||||
|
if not sprite:
|
||||||
|
push_error("SmokePuff: ERROR - Sprite2D not found! Check that scene has Sprite2D child node.")
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
|
|
||||||
# Randomly choose puff type
|
# Randomly choose puff type
|
||||||
puff_type = randi() % 2
|
puff_type = randi() % 2
|
||||||
|
|
||||||
|
# Randomly choose movement direction (random angle, slow movement)
|
||||||
|
var random_angle = randf() * TAU # 0 to 2*PI
|
||||||
|
move_direction = Vector2(cos(random_angle), sin(random_angle))
|
||||||
|
|
||||||
# Set initial frame
|
# Set initial frame
|
||||||
sprite.frame = puff_type * total_frames
|
sprite.frame = puff_type * total_frames
|
||||||
current_frame = 0
|
current_frame = 0
|
||||||
|
|
||||||
|
print("SmokePuff: Starting animation, sprite: ", sprite, ", frame: ", sprite.frame, ", move_direction: ", move_direction)
|
||||||
|
|
||||||
# Start animation
|
# Start animation
|
||||||
animate_puff()
|
animate_puff()
|
||||||
|
|
||||||
func animate_puff():
|
func animate_puff():
|
||||||
# Animate through the 4 frames
|
# Verify sprite still exists
|
||||||
var tween = create_tween()
|
if not sprite:
|
||||||
|
push_error("SmokePuff: ERROR - Sprite is null during animation!")
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
|
|
||||||
for i in range(total_frames):
|
# Calculate frame animation timing
|
||||||
tween.tween_callback(func():
|
var frame_interval = 1.0 / animation_speed # Time per frame
|
||||||
sprite.frame = puff_type * total_frames + i
|
var frame_animation_duration = float(total_frames) * frame_interval
|
||||||
)
|
|
||||||
tween.tween_interval(1.0 / animation_speed)
|
# Set initial frame
|
||||||
|
sprite.frame = puff_type * total_frames
|
||||||
|
current_frame = 0
|
||||||
|
frame_timer = 0.0
|
||||||
|
|
||||||
|
# Start movement tween
|
||||||
|
var move_distance = move_speed * move_duration
|
||||||
|
var target_position = global_position + move_direction * move_distance
|
||||||
|
var move_tween = create_tween()
|
||||||
|
if move_tween:
|
||||||
|
move_tween.tween_property(self, "global_position", target_position, move_duration)
|
||||||
|
|
||||||
|
# After animation completes, fade out and remove
|
||||||
|
var total_animation_time = max(frame_animation_duration, move_duration)
|
||||||
|
await get_tree().create_timer(total_animation_time).timeout
|
||||||
|
|
||||||
# Fade out
|
# Fade out
|
||||||
tween.tween_property(sprite, "modulate:a", 0.0, fade_duration)
|
if sprite:
|
||||||
|
print("SmokePuff: Starting fade out...")
|
||||||
|
var fade_tween = create_tween()
|
||||||
|
if fade_tween:
|
||||||
|
fade_tween.tween_property(sprite, "modulate:a", 0.0, fade_duration)
|
||||||
|
await fade_tween.finished
|
||||||
|
|
||||||
# Remove after animation
|
print("SmokePuff: Animation complete, removing...")
|
||||||
tween.tween_callback(queue_free)
|
queue_free()
|
||||||
|
|
||||||
|
func _process(delta):
|
||||||
|
# Handle frame animation in _process() for more reliable timing
|
||||||
|
if not sprite:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update frame animation
|
||||||
|
if current_frame < total_frames:
|
||||||
|
frame_timer += delta
|
||||||
|
var frame_interval = 1.0 / animation_speed
|
||||||
|
|
||||||
|
if frame_timer >= frame_interval:
|
||||||
|
frame_timer = 0.0
|
||||||
|
current_frame += 1
|
||||||
|
|
||||||
|
if current_frame < total_frames:
|
||||||
|
sprite.frame = puff_type * total_frames + current_frame
|
||||||
|
print("SmokePuff: Frame ", current_frame, " -> ", sprite.frame)
|
||||||
|
|||||||
@@ -114,7 +114,13 @@ func _on_body_entered(body):
|
|||||||
else:
|
else:
|
||||||
# Fallback: broadcast if we can't get peer_id
|
# Fallback: broadcast if we can't get peer_id
|
||||||
body.rpc_take_damage.rpc(damage, attacker_pos)
|
body.rpc_take_damage.rpc(damage, attacker_pos)
|
||||||
print("Sword projectile hit enemy: ", body.name, " for ", damage, " damage! (owner: ", player_owner.name if player_owner else "none", " is_authority: ", player_owner.is_multiplayer_authority() if player_owner else false, ")")
|
# Debug print - handle null player_owner safely
|
||||||
|
var owner_name: String = "none"
|
||||||
|
var is_authority: bool = false
|
||||||
|
if player_owner:
|
||||||
|
owner_name = str(player_owner.name)
|
||||||
|
is_authority = player_owner.is_multiplayer_authority()
|
||||||
|
print("Sword projectile hit enemy: ", body.name, " for ", damage, " damage! (owner: ", owner_name, " is_authority: ", is_authority, ")")
|
||||||
return # Don't apply generic knockback, take_damage handles it
|
return # Don't apply generic knockback, take_damage handles it
|
||||||
|
|
||||||
# Deal damage to boxes or other damageable objects
|
# Deal damage to boxes or other damageable objects
|
||||||
|
|||||||
34
src/scripts/teleporter_into_closed_room.gd
Normal file
34
src/scripts/teleporter_into_closed_room.gd
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
@export var is_enabled = true # set to disabled for keydoors!
|
||||||
|
|
||||||
|
# Called when the node enters the scene tree for the first time.
|
||||||
|
func _ready() -> void:
|
||||||
|
pass # Replace with function body.
|
||||||
|
|
||||||
|
|
||||||
|
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func _on_area_which_teleports_player_into_room_body_entered(body: Node2D) -> void:
|
||||||
|
if !is_enabled:
|
||||||
|
return
|
||||||
|
# TODO: teleport player passed the door...
|
||||||
|
get_parent().teleportPlayer(body)
|
||||||
|
pass # Replace with function body.
|
||||||
|
|
||||||
|
|
||||||
|
func _on_area_to_start_emit_body_entered(body: Node2D) -> void:
|
||||||
|
if !is_enabled:
|
||||||
|
return
|
||||||
|
$GPUParticles2D.emitting = true
|
||||||
|
pass # Replace with function body.
|
||||||
|
|
||||||
|
|
||||||
|
func _on_area_to_start_emit_body_exited(body: Node2D) -> void:
|
||||||
|
if !is_enabled:
|
||||||
|
return
|
||||||
|
$GPUParticles2D.emitting = false
|
||||||
|
pass # Replace with function body.
|
||||||
1
src/scripts/teleporter_into_closed_room.gd.uid
Normal file
1
src/scripts/teleporter_into_closed_room.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b4wejvn0dfrji
|
||||||
Reference in New Issue
Block a user