fixed finally webrtc
This commit is contained in:
BIN
src/assets/audio/sfx/weapons/magic.mp3
Normal file
BIN
src/assets/audio/sfx/weapons/magic.mp3
Normal file
Binary file not shown.
19
src/assets/audio/sfx/weapons/magic.mp3.import
Normal file
19
src/assets/audio/sfx/weapons/magic.mp3.import
Normal file
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="mp3"
|
||||
type="AudioStreamMP3"
|
||||
uid="uid://cnq1rcdh6h34x"
|
||||
path="res://.godot/imported/magic.mp3-f46332096bbc9033630ad74555859383.mp3str"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/sfx/weapons/magic.mp3"
|
||||
dest_files=["res://.godot/imported/magic.mp3-f46332096bbc9033630ad74555859383.mp3str"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
BIN
src/assets/audio/sfx/weapons/magic_impact.mp3
Normal file
BIN
src/assets/audio/sfx/weapons/magic_impact.mp3
Normal file
Binary file not shown.
19
src/assets/audio/sfx/weapons/magic_impact.mp3.import
Normal file
19
src/assets/audio/sfx/weapons/magic_impact.mp3.import
Normal file
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="mp3"
|
||||
type="AudioStreamMP3"
|
||||
uid="uid://d3vo82fiyo076"
|
||||
path="res://.godot/imported/magic_impact.mp3-a913f5b2e9946b900e1eb0911f0432da.mp3str"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/sfx/weapons/magic_impact.mp3"
|
||||
dest_files=["res://.godot/imported/magic_impact.mp3-a913f5b2e9946b900e1eb0911f0432da.mp3str"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
BIN
src/assets/audio/sfx/weapons/schioow.mp3
Normal file
BIN
src/assets/audio/sfx/weapons/schioow.mp3
Normal file
Binary file not shown.
19
src/assets/audio/sfx/weapons/schioow.mp3.import
Normal file
19
src/assets/audio/sfx/weapons/schioow.mp3.import
Normal file
@@ -0,0 +1,19 @@
|
||||
[remap]
|
||||
|
||||
importer="mp3"
|
||||
type="AudioStreamMP3"
|
||||
uid="uid://bvefpp6la7ehx"
|
||||
path="res://.godot/imported/schioow.mp3-fc2506aa54189f71eeb0e728e8ae4aa4.mp3str"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/audio/sfx/weapons/schioow.mp3"
|
||||
dest_files=["res://.godot/imported/schioow.mp3-fc2506aa54189f71eeb0e728e8ae4aa4.mp3str"]
|
||||
|
||||
[params]
|
||||
|
||||
loop=false
|
||||
loop_offset=0
|
||||
bpm=0
|
||||
beat_count=0
|
||||
bar_beats=4
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 10 KiB |
BIN
src/assets/gfx/morph_ball.png
Normal file
BIN
src/assets/gfx/morph_ball.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
40
src/assets/gfx/morph_ball.png.import
Normal file
40
src/assets/gfx/morph_ball.png.import
Normal file
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://2dya7wnmph5m"
|
||||
path="res://.godot/imported/morph_ball.png-ca00582698de0ab8df351e786cd9023f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/gfx/morph_ball.png"
|
||||
dest_files=["res://.godot/imported/morph_ball.png-ca00582698de0ab8df351e786cd9023f.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://ndtcy3xo1uob"
|
||||
uid="uid://bv2tcr2o1cov8"
|
||||
path="res://.godot/imported/torch_steel_01.png-1412ad992fcc159a1ee81cbd09810b38.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
|
||||
@@ -80,10 +80,11 @@ attack={
|
||||
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":75,"key_label":0,"unicode":107,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[input_devices]
|
||||
|
||||
pointing/emulate_touch_from_mouse=true
|
||||
inventory={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194306,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[physics]
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[gd_scene format=3 uid="uid://tcobiw1iirdw"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bqrtsr3mjvv3j" path="res://scripts/attack_axe_swing.gd" id="1_xo3v0"]
|
||||
[ext_resource type="Texture2D" uid="uid://bwxpic53sluul" path="res://assets/gfx/sword_slash.png" id="2_lwt2c"]
|
||||
[ext_resource type="Texture2D" uid="uid://dkpritx47nd4m" path="res://assets/gfx/pickups/items_n_shit.png" id="2_hb10f"]
|
||||
[ext_resource type="AudioStream" uid="uid://4vulahdsj4i2" path="res://assets/audio/sfx/swoosh/throw_01.wav.mp3" id="3_v2p0x"]
|
||||
[ext_resource type="AudioStream" uid="uid://uerx5rib87a6" path="res://assets/audio/sfx/weapons/bone_hit_wall_01.wav.mp3" id="4_ul7bj"]
|
||||
[ext_resource type="AudioStream" uid="uid://dc7nt8gnjt5u5" path="res://assets/audio/sfx/weapons/melee_attack_12.wav.mp3" id="5_whqew"]
|
||||
@@ -52,7 +52,10 @@ y_sort_enabled = true
|
||||
script = ExtResource("1_xo3v0")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=461038063]
|
||||
texture = ExtResource("2_lwt2c")
|
||||
texture = ExtResource("2_hb10f")
|
||||
hframes = 20
|
||||
vframes = 14
|
||||
frame = 111
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=691292922]
|
||||
libraries/ = SubResource("AnimationLibrary_hj6i2")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[gd_scene format=3 uid="uid://b3my31y2ljai1"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://ddprn0wrasavr" path="res://scripts/attack_spear_thrust.gd" id="1_psi1x"]
|
||||
[ext_resource type="Texture2D" uid="uid://bwxpic53sluul" path="res://assets/gfx/sword_slash.png" id="2_rh1o6"]
|
||||
[ext_resource type="Texture2D" uid="uid://dkpritx47nd4m" path="res://assets/gfx/pickups/items_n_shit.png" id="2_d2i4u"]
|
||||
[ext_resource type="AudioStream" uid="uid://4vulahdsj4i2" path="res://assets/audio/sfx/swoosh/throw_01.wav.mp3" id="3_j7ui3"]
|
||||
[ext_resource type="AudioStream" uid="uid://uerx5rib87a6" path="res://assets/audio/sfx/weapons/bone_hit_wall_01.wav.mp3" id="4_cijfq"]
|
||||
[ext_resource type="AudioStream" uid="uid://dc7nt8gnjt5u5" path="res://assets/audio/sfx/weapons/melee_attack_12.wav.mp3" id="5_h4gub"]
|
||||
@@ -52,7 +52,11 @@ y_sort_enabled = true
|
||||
script = ExtResource("1_psi1x")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1051719548]
|
||||
texture = ExtResource("2_rh1o6")
|
||||
rotation = -2.3736477
|
||||
texture = ExtResource("2_d2i4u")
|
||||
hframes = 20
|
||||
vframes = 14
|
||||
frame = 131
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=907474922]
|
||||
libraries/ = SubResource("AnimationLibrary_hj6i2")
|
||||
@@ -63,6 +67,7 @@ pitch_scale = 1.4
|
||||
autoplay = true
|
||||
|
||||
[node name="DamageArea" type="Area2D" parent="." unique_id=1687133888]
|
||||
visible = false
|
||||
collision_layer = 0
|
||||
collision_mask = 75
|
||||
|
||||
|
||||
124
src/scenes/attack_staff.tscn
Normal file
124
src/scenes/attack_staff.tscn
Normal file
@@ -0,0 +1,124 @@
|
||||
[gd_scene format=3 uid="uid://c8galam3n3p2r"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bn5vp502u6pf5" path="res://scripts/staff_projectile.gd" id="1_projectile"]
|
||||
[ext_resource type="Shader" uid="uid://ldl7vaq5n13f" path="res://shaders/cloth.gdshader" id="2_84jgf"]
|
||||
[ext_resource type="Texture2D" uid="uid://2dya7wnmph5m" path="res://assets/gfx/morph_ball.png" id="2_rkc3p"]
|
||||
[ext_resource type="AudioStream" uid="uid://bvefpp6la7ehx" path="res://assets/audio/sfx/weapons/schioow.mp3" id="3_84jgf"]
|
||||
[ext_resource type="AudioStream" uid="uid://d3vo82fiyo076" path="res://assets/audio/sfx/weapons/magic_impact.mp3" id="4_rkc3p"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_qorvo"]
|
||||
shader = ExtResource("2_84jgf")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_projectile"]
|
||||
size = Vector2(10, 12)
|
||||
|
||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_qorvo"]
|
||||
random_pitch = 1.0960649
|
||||
streams_count = 1
|
||||
stream_0/stream = ExtResource("3_84jgf")
|
||||
|
||||
[sub_resource type="Animation" id="Animation_84jgf"]
|
||||
length = 0.001
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0),
|
||||
"transitions": PackedFloat32Array(1),
|
||||
"update": 1,
|
||||
"values": [6]
|
||||
}
|
||||
|
||||
[sub_resource type="Animation" id="Animation_qorvo"]
|
||||
resource_name = "flying"
|
||||
length = 0.4
|
||||
loop_mode = 1
|
||||
tracks/0/type = "value"
|
||||
tracks/0/imported = false
|
||||
tracks/0/enabled = true
|
||||
tracks/0/path = NodePath("Sprite2D:frame")
|
||||
tracks/0/interp = 1
|
||||
tracks/0/loop_wrap = true
|
||||
tracks/0/keys = {
|
||||
"times": PackedFloat32Array(0, 0.1, 0.2, 0.3),
|
||||
"transitions": PackedFloat32Array(1, 1, 1, 1),
|
||||
"update": 1,
|
||||
"values": [6, 8, 7, 8]
|
||||
}
|
||||
|
||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_2q84p"]
|
||||
_data = {
|
||||
&"RESET": SubResource("Animation_84jgf"),
|
||||
&"flying": SubResource("Animation_qorvo")
|
||||
}
|
||||
|
||||
[node name="StaffProjectile" type="Node2D" unique_id=357652786]
|
||||
y_sort_enabled = true
|
||||
script = ExtResource("1_projectile")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=993464286]
|
||||
material = SubResource("ShaderMaterial_qorvo")
|
||||
scale = Vector2(0.75, 0.75)
|
||||
texture = ExtResource("2_rkc3p")
|
||||
hframes = 6
|
||||
vframes = 6
|
||||
frame = 6
|
||||
|
||||
[node name="Area2D" type="Area2D" parent="." unique_id=556563629]
|
||||
collision_layer = 4
|
||||
collision_mask = 3
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D" unique_id=520125160]
|
||||
position = Vector2(-0.25, 0)
|
||||
shape = SubResource("RectangleShape2D_projectile")
|
||||
debug_color = Color(0.70196074, 0, 0.09064378, 0.41960785)
|
||||
|
||||
[node name="SfxSwosh" type="AudioStreamPlayer2D" parent="." unique_id=1006342490]
|
||||
stream = SubResource("AudioStreamRandomizer_qorvo")
|
||||
pitch_scale = 0.95
|
||||
max_distance = 983.0
|
||||
attenuation = 7.999991
|
||||
panning_strength = 1.1
|
||||
bus = &"Sfx"
|
||||
|
||||
[node name="SfxImpact" type="AudioStreamPlayer2D" parent="." unique_id=1627987810]
|
||||
stream = ExtResource("4_rkc3p")
|
||||
volume_db = -0.282
|
||||
pitch_scale = 0.78
|
||||
max_distance = 983.0
|
||||
attenuation = 7.999991
|
||||
max_polyphony = 4
|
||||
panning_strength = 1.16
|
||||
bus = &"Sfx"
|
||||
|
||||
[node name="SfxImpactWall" type="AudioStreamPlayer2D" parent="." unique_id=77775230]
|
||||
stream = ExtResource("4_rkc3p")
|
||||
volume_db = -4.0
|
||||
pitch_scale = 1.3
|
||||
max_distance = 951.0
|
||||
attenuation = 8.282115
|
||||
max_polyphony = 4
|
||||
panning_strength = 1.15
|
||||
bus = &"Sfx"
|
||||
|
||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=241170994]
|
||||
libraries/ = SubResource("AnimationLibrary_2q84p")
|
||||
autoplay = &"flying"
|
||||
@@ -4,6 +4,7 @@
|
||||
[ext_resource type="Script" uid="uid://cpxabh3uq1kl4" path="res://scripts/create_shadow_sprite.gd" id="2"]
|
||||
[ext_resource type="Texture2D" uid="uid://bkninujaqqvb1" path="res://assets/gfx/Puny-Characters/Layer 0 - Skins/Human1_1.png" id="3"]
|
||||
[ext_resource type="Texture2D" uid="uid://bloqx3mibftjn" path="res://assets/gfx/Puny-Characters/WeaponOverlayer.png" id="4"]
|
||||
[ext_resource type="Shader" uid="uid://ldl7vaq5n13f" path="res://shaders/cloth.gdshader" id="4_r7ul0"]
|
||||
[ext_resource type="Texture2D" uid="uid://cwklipebg6eyp" path="res://assets/gfx/enemies/_utropstecken.png" id="5"]
|
||||
[ext_resource type="Texture2D" uid="uid://c4jkxpv3objot" path="res://assets/gfx/enemies/_questionmark.png" id="6"]
|
||||
[ext_resource type="AudioStream" uid="uid://dtydo3gymnrcv" path="res://assets/audio/sfx/enemies/goblin/die1.mp3" id="7_fikv0"]
|
||||
@@ -25,6 +26,168 @@ fill = 1
|
||||
fill_from = Vector2(0.51304346, 0.46086955)
|
||||
fill_to = Vector2(0, 0)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_uedn7"]
|
||||
shader = ExtResource("4_r7ul0")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_5x2ph"]
|
||||
shader = ExtResource("4_r7ul0")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_r7ul0"]
|
||||
shader = ExtResource("4_r7ul0")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_oynfq"]
|
||||
shader = ExtResource("4_r7ul0")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_b0veo"]
|
||||
shader = ExtResource("4_r7ul0")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_of8l8"]
|
||||
shader = ExtResource("4_r7ul0")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_ofeay"]
|
||||
shader = ExtResource("4_r7ul0")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_5a33a"]
|
||||
shader = ExtResource("4_r7ul0")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_i1636"]
|
||||
shader = ExtResource("4_r7ul0")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_1"]
|
||||
radius = 5.0
|
||||
|
||||
@@ -62,47 +225,56 @@ script = ExtResource("2")
|
||||
|
||||
[node name="Sprite2DBody" type="Sprite2D" parent="." unique_id=855871821]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_uedn7")
|
||||
texture = ExtResource("3")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DBoots" type="Sprite2D" parent="." unique_id=460958943]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_5x2ph")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DArmour" type="Sprite2D" parent="." unique_id=6790482]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_r7ul0")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DFacialHair" type="Sprite2D" parent="." unique_id=31110906]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_oynfq")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DHair" type="Sprite2D" parent="." unique_id=425592986]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_b0veo")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DEyes" type="Sprite2D" parent="." unique_id=496437887]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_of8l8")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DEyeLashes" type="Sprite2D" parent="." unique_id=1799398723]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_ofeay")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DAddons" type="Sprite2D" parent="." unique_id=1702763725]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_5a33a")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DHeadgear" type="Sprite2D" parent="." unique_id=164186416]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_i1636")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ z_index = -2
|
||||
tile_set = ExtResource("9")
|
||||
|
||||
[node name="TileMapLayerAbove" type="TileMapLayer" parent="Environment" unique_id=1234567892]
|
||||
modulate = Color(1, 1, 1, 0.77254903)
|
||||
modulate = Color(1, 1, 1, 0.46666667)
|
||||
z_index = 1
|
||||
tile_set = ExtResource("9")
|
||||
|
||||
|
||||
@@ -143,6 +143,34 @@ layout_mode = 2
|
||||
theme = SubResource("Theme_standard_font")
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="VBoxContainerConnectionStatus" type="VBoxContainer" parent="UpperRight/HBoxContainer" unique_id=1933444960]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="LabelConnectionStatus" type="Label" parent="UpperRight/HBoxContainer/VBoxContainerConnectionStatus" unique_id=1933444961]
|
||||
layout_mode = 2
|
||||
theme = SubResource("Theme_standard_font")
|
||||
text = "CONNECTION"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="LabelMatchboxStatus" type="Label" parent="UpperRight/HBoxContainer/VBoxContainerConnectionStatus" unique_id=1933444962]
|
||||
layout_mode = 2
|
||||
theme = SubResource("Theme_standard_font")
|
||||
text = "Matchbox: --"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="LabelICEStatus" type="Label" parent="UpperRight/HBoxContainer/VBoxContainerConnectionStatus" unique_id=1933444963]
|
||||
layout_mode = 2
|
||||
theme = SubResource("Theme_standard_font")
|
||||
text = "ICE: --"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="LabelDataChannelsStatus" type="Label" parent="UpperRight/HBoxContainer/VBoxContainerConnectionStatus" unique_id=1933444964]
|
||||
layout_mode = 2
|
||||
theme = SubResource("Theme_standard_font")
|
||||
text = "Data: --"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="VBoxContainerBoss" type="VBoxContainer" parent="UpperRight/HBoxContainer" unique_id=1933444957]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[ext_resource type="Script" uid="uid://jk7o0itmiwp6" path="res://scripts/loot.gd" id="1"]
|
||||
[ext_resource type="Script" uid="uid://cpxabh3uq1kl4" path="res://scripts/create_shadow_sprite.gd" id="2"]
|
||||
[ext_resource type="AudioStream" uid="uid://b60bke4f5uw4v" path="res://assets/audio/sfx/pickups/coin_pickup.mp3" id="3_30m34"]
|
||||
[ext_resource type="Texture2D" uid="uid://cimek2qjgoqa1" path="res://assets/gfx/pickups/gold_coin.png" id="3_531sv"]
|
||||
[ext_resource type="AudioStream" uid="uid://brl8ivwb1l5i7" path="res://assets/audio/sfx/pickups/coin_drop_01.wav.mp3" id="4_rtp8m"]
|
||||
[ext_resource type="AudioStream" uid="uid://umoxmryvbm01" path="res://assets/audio/sfx/cloth/leather_cloth_01.wav.mp3" id="5_vl55g"]
|
||||
[ext_resource type="AudioStream" uid="uid://d1qqsganlqnwh" path="res://assets/audio/sfx/pickups/key.mp3" id="6_gyjv8"]
|
||||
@@ -48,6 +49,8 @@ script = ExtResource("2")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1501367665]
|
||||
y_sort_enabled = true
|
||||
texture = ExtResource("3_531sv")
|
||||
hframes = 6
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=265450649]
|
||||
shape = SubResource("CircleShape2D_2")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
[ext_resource type="Script" uid="uid://ck72vhkja7nbo" path="res://scripts/player.gd" id="1"]
|
||||
[ext_resource type="Script" uid="uid://cpxabh3uq1kl4" path="res://scripts/create_shadow_sprite.gd" id="3"]
|
||||
[ext_resource type="Shader" uid="uid://ldl7vaq5n13f" path="res://shaders/cloth.gdshader" id="3_wnwbv"]
|
||||
[ext_resource type="Texture2D" uid="uid://bkninujaqqvb1" path="res://assets/gfx/Puny-Characters/Layer 0 - Skins/Human1_1.png" id="4"]
|
||||
[ext_resource type="Texture2D" uid="uid://dx1fovugabbwc" path="res://assets/gfx/Puny-Characters/Layer 1 - Shoes/IronBoots.png" id="5"]
|
||||
[ext_resource type="Texture2D" uid="uid://bbqk2lcs772q3" path="res://assets/gfx/Puny-Characters/Layer 2 - Clothes/Armour Body/BronzeArmour.png" id="6"]
|
||||
@@ -28,6 +29,8 @@
|
||||
[ext_resource type="AudioStream" uid="uid://bdhmel5vyixng" path="res://assets/audio/sfx/player/take_damage/player_damaged_07.wav.mp3" id="26_gl8cc"]
|
||||
[ext_resource type="AudioStream" uid="uid://4vulahdsj4i2" path="res://assets/audio/sfx/swoosh/throw_01.wav.mp3" id="27_31cv2"]
|
||||
[ext_resource type="AudioStream" uid="uid://w6yon88kjfml" path="res://assets/audio/sfx/nickes/lift_sfx.ogg" id="28_pf23h"]
|
||||
[ext_resource type="AudioStream" uid="uid://d4kjyb1olr74s" path="res://assets/audio/sfx/weapons/bow/bow_draw-02.mp3" id="30_gl8cc"]
|
||||
[ext_resource type="AudioStream" uid="uid://mbtpqlb5n3gd" path="res://assets/audio/sfx/weapons/bow/bow_no_arrow.mp3" id="31_487ah"]
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_wqfne"]
|
||||
colors = PackedColorArray(0, 0, 0, 1, 1, 0.13732082, 0.092538536, 1)
|
||||
@@ -52,19 +55,199 @@ fill_to = Vector2(0.8974359, 0.08547009)
|
||||
radius = 32.0
|
||||
|
||||
[sub_resource type="Gradient" id="Gradient_3v2ag"]
|
||||
colors = PackedColorArray(0, 0, 0, 1, 0, 0, 0, 0)
|
||||
offsets = PackedFloat32Array(0.3883721, 0.8372093)
|
||||
colors = PackedColorArray(0, 0, 0, 0.74509805, 0, 0, 0, 0)
|
||||
|
||||
[sub_resource type="GradientTexture2D" id="GradientTexture2D_jej6c"]
|
||||
gradient = SubResource("Gradient_3v2ag")
|
||||
width = 14
|
||||
height = 8
|
||||
width = 12
|
||||
height = 6
|
||||
fill = 1
|
||||
fill_from = Vector2(0.51304346, 0.46086955)
|
||||
fill_to = Vector2(0, 0)
|
||||
|
||||
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_pf23h"]
|
||||
radius = 3.0
|
||||
height = 12.0
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_md1ol"]
|
||||
shader = ExtResource("3_wnwbv")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_bj30b"]
|
||||
shader = ExtResource("3_wnwbv")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_jc3p3"]
|
||||
shader = ExtResource("3_wnwbv")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_hax0n"]
|
||||
shader = ExtResource("3_wnwbv")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_t4otl"]
|
||||
shader = ExtResource("3_wnwbv")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_j2b1d"]
|
||||
shader = ExtResource("3_wnwbv")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_cs1tg"]
|
||||
shader = ExtResource("3_wnwbv")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_2dvfe"]
|
||||
shader = ExtResource("3_wnwbv")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_giy8y"]
|
||||
shader = ExtResource("3_wnwbv")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_fdfoy"]
|
||||
shader = ExtResource("3_wnwbv")
|
||||
shader_parameter/original_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/original_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_0 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_1 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_2 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_3 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_4 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_5 = Color(0, 0, 0, 1)
|
||||
shader_parameter/replace_6 = Color(0, 0, 0, 1)
|
||||
shader_parameter/tint = Color(1, 1, 1, 1)
|
||||
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_wnwbv"]
|
||||
radius = 4.0
|
||||
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_2"]
|
||||
radius = 8.0
|
||||
@@ -92,6 +275,11 @@ stream_4/stream = ExtResource("24_wqfne")
|
||||
stream_5/stream = ExtResource("25_wnwbv")
|
||||
stream_6/stream = ExtResource("26_gl8cc")
|
||||
|
||||
[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_hhpqf"]
|
||||
random_pitch = 1.0630184
|
||||
streams_count = 1
|
||||
stream_0/stream = ExtResource("31_487ah")
|
||||
|
||||
[node name="Player" type="CharacterBody2D" unique_id=937429705]
|
||||
collision_mask = 67
|
||||
motion_mode = 1
|
||||
@@ -127,60 +315,71 @@ script = ExtResource("3")
|
||||
|
||||
[node name="Sprite2DBody" type="Sprite2D" parent="." unique_id=2113577699]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_md1ol")
|
||||
texture = ExtResource("4")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DBoots" type="Sprite2D" parent="." unique_id=598174931]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_bj30b")
|
||||
texture = ExtResource("5")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DArmour" type="Sprite2D" parent="." unique_id=2130297502]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_jc3p3")
|
||||
texture = ExtResource("6")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DFacialHair" type="Sprite2D" parent="." unique_id=1050766722]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_hax0n")
|
||||
texture = ExtResource("7")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DHair" type="Sprite2D" parent="." unique_id=927492041]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_t4otl")
|
||||
texture = ExtResource("8")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DEyes" type="Sprite2D" parent="." unique_id=2054421939]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_j2b1d")
|
||||
texture = ExtResource("9")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DEyeLashes" type="Sprite2D" parent="." unique_id=1437938522]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_cs1tg")
|
||||
texture = ExtResource("10")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DAddons" type="Sprite2D" parent="." unique_id=962307958]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_2dvfe")
|
||||
texture = ExtResource("11")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DHeadgear" type="Sprite2D" parent="." unique_id=526463008]
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_giy8y")
|
||||
texture = ExtResource("12")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
|
||||
[node name="Sprite2DWeapon" type="Sprite2D" parent="." unique_id=1889932388]
|
||||
z_index = 1
|
||||
y_sort_enabled = true
|
||||
material = SubResource("ShaderMaterial_fdfoy")
|
||||
texture = ExtResource("13")
|
||||
hframes = 35
|
||||
vframes = 8
|
||||
@@ -188,7 +387,7 @@ vframes = 8
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=989315141]
|
||||
position = Vector2(0, 4)
|
||||
rotation = -1.5707964
|
||||
shape = SubResource("CapsuleShape2D_pf23h")
|
||||
shape = SubResource("CircleShape2D_wnwbv")
|
||||
|
||||
[node name="GrabArea" type="Area2D" parent="." unique_id=518653365]
|
||||
position = Vector2(0, 4)
|
||||
@@ -256,3 +455,13 @@ energy = 0.13
|
||||
blend_mode = 2
|
||||
shadow_enabled = true
|
||||
max_distance = 100.0
|
||||
|
||||
[node name="SfxBowShoot" type="AudioStreamPlayer2D" parent="." unique_id=340970961]
|
||||
stream = ExtResource("30_gl8cc")
|
||||
pitch_scale = 1.33
|
||||
attenuation = 6.7271657
|
||||
|
||||
[node name="SfxBowWithoutArrow" type="AudioStreamPlayer2D" parent="." unique_id=189976587]
|
||||
stream = SubResource("AudioStreamRandomizer_hhpqf")
|
||||
max_distance = 1455.0
|
||||
attenuation = 7.4642572
|
||||
|
||||
@@ -13,6 +13,7 @@ signal character_changed(char: CharacterStats)
|
||||
signal signal_drop_item(item: Item)
|
||||
|
||||
var character_type: String = "enemy"
|
||||
var race: String = "Human" # "Dwarf", "Elf", or "Human"
|
||||
@export var level: int = 1
|
||||
@export var character_name: String = ""
|
||||
@export var xp: float = 0
|
||||
@@ -27,7 +28,9 @@ var facial_hair_color:Color = Color.WHITE
|
||||
var hairstyle:String = ""
|
||||
var hair_color:Color = Color.WHITE
|
||||
var eyes:String = ""
|
||||
var eye_color:Color = Color.WHITE
|
||||
var eye_lashes:String = ""
|
||||
var eyelash_color:Color = Color.WHITE
|
||||
var add_on:String = ""
|
||||
|
||||
var bonusmaxhp: float = 0.0
|
||||
@@ -363,6 +366,7 @@ func save() -> Dictionary:
|
||||
"character_type": character_type,
|
||||
"character_name": character_name,
|
||||
|
||||
"race": race, # Save race
|
||||
"baseStats": baseStats,
|
||||
|
||||
"hp": hp,
|
||||
@@ -385,6 +389,8 @@ func save() -> Dictionary:
|
||||
|
||||
"facial_hair_color": facial_hair_color.to_html(true),
|
||||
"hair_color": hair_color.to_html(true),
|
||||
"eye_color": eye_color.to_html(true), # Save eye color
|
||||
"eyelash_color": eyelash_color.to_html(true), # Save eyelash color
|
||||
|
||||
"inventory": saveInventory(),
|
||||
"equipment": saveEquipment()
|
||||
@@ -449,6 +455,12 @@ func load(iDic: Dictionary) -> void:
|
||||
facial_hair_color = Color(iDic.get("facial_hair_color"))
|
||||
if iDic.has("hair_color"):
|
||||
hair_color = Color(iDic.get("hair_color"))
|
||||
if iDic.has("eye_color"):
|
||||
eye_color = Color(iDic.get("eye_color"))
|
||||
if iDic.has("eyelash_color"):
|
||||
eyelash_color = Color(iDic.get("eyelash_color"))
|
||||
if iDic.has("race"):
|
||||
race = iDic.get("race")
|
||||
|
||||
pass
|
||||
'
|
||||
@@ -767,3 +779,15 @@ func setHairColor(iColor:Color):
|
||||
hair_color = iColor
|
||||
emit_signal("character_changed", self)
|
||||
pass
|
||||
func setEyeColor(iColor: Color):
|
||||
eye_color = iColor
|
||||
emit_signal("character_changed", self)
|
||||
pass
|
||||
func setEyelashColor(iColor: Color):
|
||||
eyelash_color = iColor
|
||||
emit_signal("character_changed", self)
|
||||
pass
|
||||
func setRace(iRace: String):
|
||||
race = iRace
|
||||
emit_signal("character_changed", self)
|
||||
pass
|
||||
|
||||
@@ -764,6 +764,19 @@ func _place_torches_in_room(room: Dictionary, grid: Array, all_doors: Array, _ma
|
||||
# We need to check all valid Y positions on the left/right walls and place at torch_y_from_floor
|
||||
# Left/right walls are valid from room.y + 2 to room.y + room.h - 2 (skipping 2-tile corners)
|
||||
for y in range(room.y + 2, room.y + room.h - 2):
|
||||
# First check if there's a door at this Y position on the left wall
|
||||
# Left door (dir="W") is 3 tiles tall, so check if y is in any door's Y range
|
||||
var has_door_at_y = false
|
||||
for door in all_doors:
|
||||
var door_dir = door.dir if "dir" in door else ""
|
||||
if door_dir == "W" and door.x == room.x:
|
||||
# Left door at room.x, check if y is within door's Y range (door.y to door.y + 3)
|
||||
if y >= door.y and y <= door.y + 3:
|
||||
has_door_at_y = true
|
||||
break
|
||||
if has_door_at_y:
|
||||
continue # Skip this Y position if there's a door here
|
||||
|
||||
# Check if this is a valid left wall position
|
||||
# Left wall has 2 tiles: room.x and room.x + 1
|
||||
# Check both tiles to ensure we're not placing on a door
|
||||
@@ -804,14 +817,18 @@ func _place_torches_in_room(room: Dictionary, grid: Array, all_doors: Array, _ma
|
||||
# Door position is at door.x, door.y (upper-left tile)
|
||||
# Door occupies tiles: x from door.x to door.x + 2, y from door.y to door.y + 3
|
||||
# Door world bounding box: from (door.x * 16, door.y * 16) to ((door.x + 2) * 16, (door.y + 3) * 16)
|
||||
var door_min_x = door.x * tile_size_check
|
||||
var door_max_x = (door.x + 2) * tile_size_check
|
||||
var door_min_y = door.y * tile_size_check
|
||||
var door_max_y = (door.y + 3) * tile_size_check
|
||||
# CRITICAL: A torch is 16x16 pixels (8px in each direction), so expand door bounds by 8px in ALL directions
|
||||
# Door occupies columns room.x (0) and room.x + 1 (1), so torch at room.x + 1 can overlap if it extends left
|
||||
var door_min_x = door.x * tile_size_check - 8
|
||||
var door_max_x = (door.x + 2) * tile_size_check + 8
|
||||
var door_min_y = door.y * tile_size_check - 8
|
||||
var door_max_y = (door.y + 3) * tile_size_check + 8
|
||||
|
||||
# Check if torch bounding box overlaps with door bounding box
|
||||
if not (torch_bbox_max_x < door_min_x or torch_bbox_min_x > door_max_x or \
|
||||
torch_bbox_max_y < door_min_y or torch_bbox_min_y > door_max_y):
|
||||
# Check if torch bounding box overlaps with door bounding box (non-overlapping means torch is safe)
|
||||
# Overlap exists if NOT (torch_max < door_min OR torch_min > door_max)
|
||||
var x_overlap = not (torch_bbox_max_x < door_min_x or torch_bbox_min_x > door_max_x)
|
||||
var y_overlap = not (torch_bbox_max_y < door_min_y or torch_bbox_min_y > door_max_y)
|
||||
if x_overlap and y_overlap:
|
||||
overlaps_door = true
|
||||
break
|
||||
|
||||
@@ -1823,17 +1840,26 @@ func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size
|
||||
|
||||
# Check if room has a "switch_pillar" puzzle - if so, we MUST spawn at least 1 pillar
|
||||
var has_pillar_switch_puzzle = false
|
||||
var matching_puzzle_room = null
|
||||
if room_puzzle_data.size() > 0:
|
||||
for puzzle_room in room_puzzle_data.keys():
|
||||
# Compare rooms by values (x, y, w, h)
|
||||
if puzzle_room.x == room.x and puzzle_room.y == room.y and \
|
||||
puzzle_room.w == room.w and puzzle_room.h == room.h:
|
||||
var puzzle_info = room_puzzle_data[puzzle_room]
|
||||
LogManager.log("DungeonGenerator: Checking room (" + str(room.x) + "," + str(room.y) + ") - puzzle_room (" + str(puzzle_room.x) + "," + str(puzzle_room.y) + ") puzzle_type: " + str(puzzle_info.type), LogManager.CATEGORY_DUNGEON)
|
||||
if puzzle_info.type == "switch_pillar":
|
||||
has_pillar_switch_puzzle = true
|
||||
LogManager.log("DungeonGenerator: Room (" + str(room.x) + "," + str(room.y) + ") has pillar switch puzzle - will spawn at least 1 pillar", LogManager.CATEGORY_DUNGEON)
|
||||
# Try direct lookup first (room dictionary as key)
|
||||
if room_puzzle_data.has(room):
|
||||
matching_puzzle_room = room
|
||||
else:
|
||||
# Fallback: find matching room by comparing values (x, y, w, h)
|
||||
for puzzle_room in room_puzzle_data.keys():
|
||||
# Compare rooms by values (x, y, w, h)
|
||||
if puzzle_room.x == room.x and puzzle_room.y == room.y and \
|
||||
puzzle_room.w == room.w and puzzle_room.h == room.h:
|
||||
matching_puzzle_room = puzzle_room
|
||||
break
|
||||
|
||||
if matching_puzzle_room != null:
|
||||
var puzzle_info = room_puzzle_data[matching_puzzle_room]
|
||||
LogManager.log("DungeonGenerator: Checking room (" + str(room.x) + "," + str(room.y) + ") - puzzle_room (" + str(matching_puzzle_room.x) + "," + str(matching_puzzle_room.y) + ") puzzle_type: " + str(puzzle_info.type), LogManager.CATEGORY_DUNGEON)
|
||||
if puzzle_info.type == "switch_pillar":
|
||||
has_pillar_switch_puzzle = true
|
||||
LogManager.log("DungeonGenerator: Room (" + str(room.x) + "," + str(room.y) + ") has pillar switch puzzle - will spawn at least 1 pillar", LogManager.CATEGORY_DUNGEON)
|
||||
else:
|
||||
LogManager.log("DungeonGenerator: room_puzzle_data is empty for room (" + str(room.x) + "," + str(room.y) + ")", LogManager.CATEGORY_DUNGEON)
|
||||
|
||||
@@ -1905,8 +1931,33 @@ func _place_interactable_objects_in_room(room: Dictionary, grid: Array, map_size
|
||||
# Early return if no valid positions (unless pillar is required, but that's handled below)
|
||||
if valid_positions.size() == 0:
|
||||
if has_pillar_switch_puzzle:
|
||||
push_warning("DungeonGenerator: Room (", room.x, ",", room.y, ") has pillar switch puzzle but NO valid positions! Cannot place pillar.")
|
||||
return objects
|
||||
# CRITICAL: Pillar is REQUIRED, so we must find at least one position
|
||||
# Try a more permissive search - use ALL floor tiles in the room (even if near doors/walls)
|
||||
LogManager.log("DungeonGenerator: Room (" + str(room.x) + "," + str(room.y) + ") has pillar switch puzzle but no valid positions. Trying fallback search...", LogManager.CATEGORY_DUNGEON)
|
||||
# Use same bounds but skip the position validation check
|
||||
var found_fallback = false
|
||||
for x in range(min_x, max_x + 1):
|
||||
for y in range(min_y, max_y + 1):
|
||||
if x >= 0 and x < map_size.x and y >= 0 and y < map_size.y:
|
||||
if grid[x][y] == 1: # Floor
|
||||
var world_x = x * tile_size + 8
|
||||
var world_y = y * tile_size + 8
|
||||
var world_pos = Vector2(world_x, world_y)
|
||||
valid_positions.append(world_pos)
|
||||
# Only need one position for the required pillar
|
||||
found_fallback = true
|
||||
break
|
||||
if found_fallback:
|
||||
break
|
||||
|
||||
if valid_positions.size() == 0:
|
||||
push_warning("DungeonGenerator: Room (", room.x, ",", room.y, ") has pillar switch puzzle but NO floor tiles! Cannot place pillar.")
|
||||
return objects
|
||||
else:
|
||||
LogManager.log("DungeonGenerator: Found fallback position for required pillar in room (" + str(room.x) + "," + str(room.y) + ")", LogManager.CATEGORY_DUNGEON)
|
||||
else:
|
||||
# No pillar required, safe to return
|
||||
return objects
|
||||
|
||||
# Shuffle positions to randomize placement
|
||||
valid_positions.shuffle()
|
||||
|
||||
@@ -341,7 +341,7 @@ func take_damage(amount: float, from_position: Vector2, is_critical: bool = fals
|
||||
is_knocked_back = true
|
||||
knockback_time = 0.0
|
||||
|
||||
_on_take_damage()
|
||||
_on_take_damage(from_position)
|
||||
|
||||
# Flash red (even if dying, show the hit)
|
||||
_flash_damage()
|
||||
@@ -444,8 +444,9 @@ func _update_client_visuals():
|
||||
shadow.scale = Vector2.ONE * max(0.3, shadow_scale)
|
||||
shadow.modulate.a = 0.5 - (position_z / 50.0) * 0.2
|
||||
|
||||
func _on_take_damage():
|
||||
func _on_take_damage(_attacker_position: Vector2 = Vector2.ZERO):
|
||||
# Override in subclasses for custom damage reactions
|
||||
# attacker_position is the position of the attacker (for facing logic)
|
||||
pass
|
||||
|
||||
func _notify_doors_enemy_died():
|
||||
@@ -548,81 +549,214 @@ func _spawn_loot():
|
||||
LogManager.log_error(str(name) + " ERROR: loot_scene is null!", LogManager.CATEGORY_ENEMY)
|
||||
return
|
||||
|
||||
# Random chance to drop loot (70% chance)
|
||||
# Get killer's LCK stat to influence loot drops
|
||||
var killer_lck = 10.0 # Default LCK if no killer
|
||||
if killer_player and is_instance_valid(killer_player) and killer_player.character_stats:
|
||||
killer_lck = killer_player.character_stats.baseStats.lck + killer_player.character_stats.get_pass("lck")
|
||||
LogManager.log(str(name) + " killed by " + str(killer_player.name) + " with LCK: " + str(killer_lck), LogManager.CATEGORY_ENEMY)
|
||||
|
||||
# Random chance to drop loot (85% chance - increased from 70%)
|
||||
# LCK can increase this: +0.01% per LCK point (capped at 95%)
|
||||
var base_loot_chance = 0.85
|
||||
var lck_bonus = min(killer_lck * 0.001, 0.1) # +0.1% per LCK, max +10% (95% cap)
|
||||
var loot_chance = randf()
|
||||
LogManager.log(str(name) + " loot chance roll: " + str(loot_chance) + " (need > 0.3)", LogManager.CATEGORY_ENEMY)
|
||||
if loot_chance > 0.3:
|
||||
# Decide what to drop: 30% coin, 30% food, 40% item
|
||||
var drop_roll = randf()
|
||||
var loot_type = 0
|
||||
var drop_item = false
|
||||
|
||||
if drop_roll < 0.3:
|
||||
# 30% chance for coin
|
||||
loot_type = 0 # COIN
|
||||
elif drop_roll < 0.6:
|
||||
# 30% chance for food item
|
||||
var food_types = [1, 2, 3] # APPLE, BANANA, CHERRY
|
||||
loot_type = food_types[randi() % food_types.size()]
|
||||
var loot_threshold = 1.0 - (base_loot_chance + lck_bonus)
|
||||
LogManager.log(str(name) + " loot chance roll: " + str(loot_chance) + " (need > " + str(loot_threshold) + ", base=" + str(base_loot_chance) + ", LCK bonus=" + str(lck_bonus) + ")", LogManager.CATEGORY_ENEMY)
|
||||
if loot_chance > loot_threshold:
|
||||
# Determine how many loot items to drop (1-4 items, influenced by LCK)
|
||||
# Base: 1-3 items, LCK can push towards 2-4 items
|
||||
# LCK effect: Each 5 points of LCK above 10 increases chance for extra drops
|
||||
var lck_modifier = (killer_lck - 10.0) / 5.0 # +1 per 5 LCK above 10
|
||||
var num_drops_roll = randf()
|
||||
var base_num_drops_roll = num_drops_roll - (lck_modifier * 0.1) # LCK reduces roll needed (up to -0.6 at LCK 40)
|
||||
var num_drops = 1
|
||||
if base_num_drops_roll < 0.5:
|
||||
num_drops = 1 # 50% base chance for 1 item (reduced from 60%)
|
||||
elif base_num_drops_roll < 0.8:
|
||||
num_drops = 2 # 30% base chance for 2 items
|
||||
elif base_num_drops_roll < 0.95:
|
||||
num_drops = 3 # 15% base chance for 3 items
|
||||
else:
|
||||
# 40% chance for Item instance
|
||||
drop_item = true
|
||||
num_drops = 4 # 5% base chance for 4 items (LCK makes this more likely)
|
||||
|
||||
# Generate random velocity values (same on all clients)
|
||||
var random_angle = randf() * PI * 2
|
||||
var random_force = randf_range(50.0, 100.0)
|
||||
var random_velocity_z = randf_range(80.0, 120.0)
|
||||
|
||||
# Generate initial velocity (same on all clients via RPC)
|
||||
var initial_velocity = Vector2(cos(random_angle), sin(random_angle)) * random_force
|
||||
# Ensure at least 1 drop
|
||||
num_drops = max(1, num_drops)
|
||||
LogManager.log(str(name) + " spawning " + str(num_drops) + " loot item(s) (LCK modifier: " + str(lck_modifier) + ")", LogManager.CATEGORY_ENEMY)
|
||||
|
||||
# Find safe spawn position (on floor tile, not in walls)
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
var safe_spawn_pos = global_position
|
||||
var base_spawn_pos = global_position
|
||||
if game_world and game_world.has_method("_find_nearby_safe_spawn_position"):
|
||||
safe_spawn_pos = game_world._find_nearby_safe_spawn_position(global_position, 64.0)
|
||||
base_spawn_pos = game_world._find_nearby_safe_spawn_position(global_position, 64.0)
|
||||
|
||||
var entities_node = get_parent()
|
||||
if not entities_node:
|
||||
LogManager.log_error(str(name) + " ERROR: entities_node is null! Cannot spawn loot!", LogManager.CATEGORY_ENEMY)
|
||||
return
|
||||
|
||||
if drop_item:
|
||||
# Spawn Item instance as loot
|
||||
var item = ItemDatabase.get_random_enemy_drop()
|
||||
if item:
|
||||
ItemLootHelper.spawn_item_loot(item, global_position, entities_node, game_world)
|
||||
LogManager.log(str(name) + " ✓ dropped item: " + str(item.item_name) + " at " + str(safe_spawn_pos), LogManager.CATEGORY_ENEMY)
|
||||
else:
|
||||
# Spawn regular loot (coin or food)
|
||||
var loot = loot_scene.instantiate()
|
||||
entities_node.add_child(loot)
|
||||
loot.global_position = safe_spawn_pos
|
||||
loot.loot_type = loot_type
|
||||
# Set initial velocity before _ready() processes
|
||||
loot.velocity = initial_velocity
|
||||
loot.velocity_z = random_velocity_z
|
||||
loot.velocity_set_by_spawner = true
|
||||
loot.is_airborne = true
|
||||
LogManager.log(str(name) + " ✓ dropped loot: " + str(loot_type) + " at " + str(safe_spawn_pos) + " (original enemy pos: " + str(global_position) + ")", LogManager.CATEGORY_ENEMY)
|
||||
# Spawn multiple loot items
|
||||
for i in range(num_drops):
|
||||
# Decide what to drop for this item, influenced by LCK
|
||||
# LCK makes better items more likely: reduces coin chance, increases item chance
|
||||
var lck_bonus_item = min(killer_lck * 0.01, 0.2) # Up to +20% item chance at LCK 20+
|
||||
var lck_penalty_coin = min(killer_lck * 0.005, 0.15) # Up to -15% coin chance at LCK 30+
|
||||
|
||||
# Sync loot spawn to all clients (use safe position)
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
# Reuse game_world variable from above
|
||||
if game_world:
|
||||
# Generate unique loot ID
|
||||
# loot_id_counter is declared as a variable in game_world.gd, so it always exists
|
||||
var loot_id = game_world.loot_id_counter
|
||||
game_world.loot_id_counter += 1
|
||||
# Store loot ID on server loot instance
|
||||
loot.set_meta("loot_id", loot_id)
|
||||
# Sync to clients with ID
|
||||
game_world._rpc_to_ready_peers("_sync_loot_spawn", [safe_spawn_pos, loot_type, initial_velocity, random_velocity_z, loot_id])
|
||||
LogManager.log(str(name) + " ✓ synced loot spawn to clients", LogManager.CATEGORY_ENEMY)
|
||||
# Base probabilities: 50% coin, 20% food, 30% item
|
||||
var coin_chance = 0.5 - lck_penalty_coin
|
||||
var food_chance = 0.2
|
||||
var item_chance = 0.3 + lck_bonus_item
|
||||
|
||||
# Normalize probabilities
|
||||
var total = coin_chance + food_chance + item_chance
|
||||
coin_chance /= total
|
||||
food_chance /= total
|
||||
item_chance /= total
|
||||
|
||||
var drop_roll = randf()
|
||||
var loot_type = 0
|
||||
var drop_item = false
|
||||
var item_rarity_boost = false # LCK can boost item rarity
|
||||
|
||||
if drop_roll < coin_chance:
|
||||
# Coin
|
||||
loot_type = 0 # COIN
|
||||
elif drop_roll < coin_chance + food_chance:
|
||||
# Food item
|
||||
var food_types = [1, 2, 3] # APPLE, BANANA, CHERRY
|
||||
loot_type = food_types[randi() % food_types.size()]
|
||||
else:
|
||||
# Item instance - LCK can boost rarity
|
||||
drop_item = true
|
||||
# Higher LCK = better chance for rarer items
|
||||
item_rarity_boost = killer_lck > 15.0
|
||||
|
||||
# Generate deterministic random velocity values using dungeon seed
|
||||
# This ensures loot bounces the same on all clients
|
||||
var loot_rng = RandomNumberGenerator.new()
|
||||
# game_world is already declared above (line 587)
|
||||
var base_seed = 0
|
||||
if game_world and "dungeon_seed" in game_world:
|
||||
base_seed = game_world.dungeon_seed
|
||||
|
||||
# Get loot_id first (needed for seed calculation to ensure determinism)
|
||||
var loot_id = 0
|
||||
if game_world:
|
||||
# Try to get loot_id_counter (it's always declared in game_world.gd)
|
||||
# Access it directly - if it doesn't exist, we'll use fallback
|
||||
var loot_counter = game_world.get("loot_id_counter")
|
||||
if loot_counter != null:
|
||||
loot_id = loot_counter
|
||||
else:
|
||||
LogManager.log_error(str(name) + " ERROR: game_world not found for loot sync!", LogManager.CATEGORY_ENEMY)
|
||||
# Fallback: use enemy_index + loot_index for deterministic ID
|
||||
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else 0
|
||||
loot_id = enemy_index * 1000 + i
|
||||
else:
|
||||
# Fallback: use enemy_index + loot_index for deterministic ID
|
||||
var enemy_index = get_meta("enemy_index") if has_meta("enemy_index") else 0
|
||||
loot_id = enemy_index * 1000 + i
|
||||
|
||||
# Create unique seed for this loot item: dungeon_seed + loot_id
|
||||
# This ensures each loot item gets a unique but deterministic seed
|
||||
var loot_seed = base_seed + loot_id + 10000 # Offset to avoid collisions
|
||||
loot_rng.seed = loot_seed
|
||||
|
||||
var random_angle = loot_rng.randf() * PI * 2
|
||||
var random_force = loot_rng.randf_range(50.0, 100.0)
|
||||
var random_velocity_z = loot_rng.randf_range(80.0, 120.0)
|
||||
|
||||
# Generate initial velocity (same on all clients via RPC)
|
||||
var initial_velocity = Vector2(cos(random_angle), sin(random_angle)) * random_force
|
||||
|
||||
# Slightly offset position for multiple items (spread them out)
|
||||
var spawn_offset = Vector2(cos(random_angle), sin(random_angle)) * loot_rng.randf_range(10.0, 30.0)
|
||||
var safe_spawn_pos = base_spawn_pos + spawn_offset
|
||||
if game_world and game_world.has_method("_find_nearby_safe_spawn_position"):
|
||||
safe_spawn_pos = game_world._find_nearby_safe_spawn_position(safe_spawn_pos, 32.0)
|
||||
|
||||
if drop_item:
|
||||
# Spawn Item instance as loot - LCK influences rarity
|
||||
var item = null
|
||||
if item_rarity_boost:
|
||||
# High LCK: use chest rarity weights (better loot) instead of enemy drop weights
|
||||
# Roll for rarity with LCK bonus: each 5 LCK above 15 increases rare/epic chance
|
||||
var rarity_roll = randf()
|
||||
var lck_rarity_bonus = min((killer_lck - 15.0) * 0.02, 0.15) # Up to +15% rare/epic chance
|
||||
|
||||
# Clamp values to prevent going below 0 or above 1
|
||||
var common_threshold = max(0.0, 0.3 - lck_rarity_bonus)
|
||||
var uncommon_threshold = max(common_threshold, 0.65 - (lck_rarity_bonus * 0.5))
|
||||
var rare_threshold = min(1.0, 0.90 + (lck_rarity_bonus * 2.0))
|
||||
|
||||
if rarity_roll < common_threshold:
|
||||
# Common (reduced by LCK)
|
||||
item = ItemDatabase.get_random_item_by_rarity(ItemDatabase.ItemRarity.COMMON)
|
||||
elif rarity_roll < uncommon_threshold:
|
||||
# Uncommon (slightly reduced)
|
||||
item = ItemDatabase.get_random_item_by_rarity(ItemDatabase.ItemRarity.UNCOMMON)
|
||||
elif rarity_roll < rare_threshold:
|
||||
# Rare (increased by LCK)
|
||||
item = ItemDatabase.get_random_item_by_rarity(ItemDatabase.ItemRarity.RARE)
|
||||
else:
|
||||
# Epic/Consumable (greatly increased by LCK)
|
||||
var epic_roll = randf()
|
||||
if epic_roll < 0.5:
|
||||
item = ItemDatabase.get_random_item_by_rarity(ItemDatabase.ItemRarity.EPIC)
|
||||
else:
|
||||
item = ItemDatabase.get_random_item_by_rarity(ItemDatabase.ItemRarity.CONSUMABLE)
|
||||
else:
|
||||
# Normal LCK: use standard enemy drop weights
|
||||
item = ItemDatabase.get_random_enemy_drop()
|
||||
|
||||
if item:
|
||||
ItemLootHelper.spawn_item_loot(item, safe_spawn_pos, entities_node, game_world)
|
||||
LogManager.log(str(name) + " ✓ dropped item #" + str(i+1) + ": " + str(item.item_name) + " at " + str(safe_spawn_pos) + " (LCK boost: " + str(item_rarity_boost) + ")", LogManager.CATEGORY_ENEMY)
|
||||
else:
|
||||
# Spawn regular loot (coin or food)
|
||||
var loot = loot_scene.instantiate()
|
||||
entities_node.add_child(loot)
|
||||
loot.global_position = safe_spawn_pos
|
||||
loot.loot_type = loot_type
|
||||
# Set initial velocity before _ready() processes
|
||||
loot.velocity = initial_velocity
|
||||
loot.velocity_z = random_velocity_z
|
||||
loot.velocity_set_by_spawner = true
|
||||
loot.is_airborne = true
|
||||
LogManager.log(str(name) + " ✓ dropped loot #" + str(i+1) + ": " + str(loot_type) + " at " + str(safe_spawn_pos), LogManager.CATEGORY_ENEMY)
|
||||
|
||||
# Sync loot spawn to all clients (use safe position)
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
# Reuse game_world variable from above
|
||||
if game_world:
|
||||
# Use the loot_id we already calculated (or get real one if we used fallback)
|
||||
# loot_id_counter is declared as a variable in game_world.gd, so it always exists
|
||||
if loot_id == 0:
|
||||
# We used fallback, get real ID now
|
||||
loot_id = game_world.loot_id_counter
|
||||
game_world.loot_id_counter += 1
|
||||
# Recalculate seed with real loot_id
|
||||
var real_loot_seed = base_seed + loot_id + 10000
|
||||
loot_rng.seed = real_loot_seed
|
||||
# Regenerate velocity with correct seed
|
||||
var real_random_angle = loot_rng.randf() * PI * 2
|
||||
var real_random_force = loot_rng.randf_range(50.0, 100.0)
|
||||
var real_random_velocity_z = loot_rng.randf_range(80.0, 120.0)
|
||||
initial_velocity = Vector2(cos(real_random_angle), sin(real_random_angle)) * real_random_force
|
||||
random_velocity_z = real_random_velocity_z
|
||||
# Update loot with correct velocity
|
||||
loot.velocity = initial_velocity
|
||||
loot.velocity_z = random_velocity_z
|
||||
else:
|
||||
# We already have the correct loot_id, just increment counter
|
||||
game_world.loot_id_counter += 1
|
||||
# Store loot ID on server loot instance
|
||||
loot.set_meta("loot_id", loot_id)
|
||||
# Sync to clients with ID
|
||||
game_world._rpc_to_ready_peers("_sync_loot_spawn", [safe_spawn_pos, loot_type, initial_velocity, random_velocity_z, loot_id])
|
||||
LogManager.log(str(name) + " ✓ synced loot #" + str(i+1) + " spawn to clients", LogManager.CATEGORY_ENEMY)
|
||||
else:
|
||||
LogManager.log_error(str(name) + " ERROR: game_world not found for loot sync!", LogManager.CATEGORY_ENEMY)
|
||||
else:
|
||||
LogManager.log(str(name) + " loot chance failed (" + str(loot_chance) + " <= 0.3), no loot dropped", LogManager.CATEGORY_ENEMY)
|
||||
LogManager.log(str(name) + " loot chance failed (" + str(loot_chance) + " <= 0.15), no loot dropped", LogManager.CATEGORY_ENEMY)
|
||||
|
||||
# This function can be called directly (not just via RPC) when game_world routes the update
|
||||
func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, dir: int = 0, frame: int = 0, anim: String = "", frame_num: int = 0, state_value: int = -1):
|
||||
@@ -648,7 +782,8 @@ func _sync_position(pos: Vector2, vel: Vector2, z_pos: float = 0.0, dir: int = 0
|
||||
current_direction = dir as Direction
|
||||
|
||||
# Update state if provided (for enemies with state machines like bats/slimes)
|
||||
if state_value != -1 and "state" in self:
|
||||
# CRITICAL: Don't update state if enemy is dead - this prevents overriding DYING state
|
||||
if state_value != -1 and "state" in self and not is_dead:
|
||||
set("state", state_value)
|
||||
|
||||
# Update animation if provided (for humanoid enemies with player-like animation system)
|
||||
@@ -670,9 +805,15 @@ func _sync_damage_visual(damage_amount: float = 0.0, attacker_position: Vector2
|
||||
if is_multiplayer_authority():
|
||||
return # Server ignores its own updates
|
||||
|
||||
# CRITICAL: Don't play damage animation if enemy is already dead
|
||||
# This prevents damage sync from overriding death animation (e.g., if packets arrive out of order)
|
||||
if is_dead:
|
||||
LogManager.log(str(name) + " (client) ignoring damage visual sync - already dead", LogManager.CATEGORY_ENEMY)
|
||||
return
|
||||
|
||||
# Trigger damage animation and state change on client
|
||||
# This ensures clients play the damage animation (e.g., slime DAMAGE animation)
|
||||
_on_take_damage()
|
||||
_on_take_damage(attacker_position)
|
||||
|
||||
_flash_damage()
|
||||
|
||||
@@ -695,6 +836,47 @@ func _sync_death():
|
||||
# This matches what happens on the server when rats/slimes die
|
||||
set_collision_layer_value(2, false) # Remove from enemy collision layer (layer 2)
|
||||
|
||||
# CRITICAL: For state-based enemies (like slimes), set state to DYING before setting animation
|
||||
# This ensures _update_client_visuals doesn't override the DIE animation with DAMAGE
|
||||
# Check if enemy has a state variable - if so, try to set it to DYING
|
||||
# For slimes: SlimeState.DYING = 4
|
||||
# This prevents _update_client_visuals from seeing DAMAGED state and setting DAMAGE animation
|
||||
if "state" in self:
|
||||
var current_state = get("state")
|
||||
# SlimeState enum: IDLE=0, MOVING=1, JUMPING=2, DAMAGED=3, DYING=4
|
||||
# Set state to DYING (4) if it's currently DAMAGED (3) or less
|
||||
if current_state <= 3: # DAMAGED or less
|
||||
set("state", 4) # Set to DYING
|
||||
LogManager.log(str(name) + " (client) set state to DYING (4) in _sync_death to override DAMAGED state", LogManager.CATEGORY_ENEMY)
|
||||
|
||||
# For humanoid enemies, ensure death animation is set immediately and animation state is reset
|
||||
# This is critical for joiner clients who receive death sync
|
||||
if has_method("_set_animation"):
|
||||
LogManager.log(str(name) + " (client) setting DIE animation in _sync_death", LogManager.CATEGORY_ENEMY)
|
||||
_set_animation("DIE")
|
||||
# Also ensure animation frame is reset and animation system is ready
|
||||
if "current_frame" in self:
|
||||
set("current_frame", 0)
|
||||
LogManager.log(str(name) + " (client) reset current_frame to 0", LogManager.CATEGORY_ENEMY)
|
||||
if "time_since_last_frame" in self:
|
||||
set("time_since_last_frame", 0.0)
|
||||
LogManager.log(str(name) + " (client) reset time_since_last_frame to 0.0", LogManager.CATEGORY_ENEMY)
|
||||
# Verify animation was set
|
||||
if "current_animation" in self:
|
||||
var anim_name = get("current_animation")
|
||||
LogManager.log(str(name) + " (client) current_animation after _set_animation: " + str(anim_name), LogManager.CATEGORY_ENEMY)
|
||||
|
||||
# CRITICAL: Force immediate animation update for humanoid enemies
|
||||
# This ensures DIE animation is visible immediately on clients
|
||||
if has_method("_update_animation") and "current_animation" in self:
|
||||
call("_update_animation", 0.0)
|
||||
LogManager.log(str(name) + " (client) forced immediate _update_animation(0.0) after setting DIE in _sync_death", LogManager.CATEGORY_ENEMY)
|
||||
|
||||
# CRITICAL: Call _update_client_visuals immediately to ensure DIE animation is applied
|
||||
# This prevents _update_client_visuals from running later and overriding with DAMAGE
|
||||
if has_method("_update_client_visuals"):
|
||||
_update_client_visuals()
|
||||
|
||||
# Immediately mark as dead and stop AI/physics
|
||||
# This prevents "inactive" enemies that are already dead
|
||||
_play_death_animation()
|
||||
|
||||
@@ -269,20 +269,41 @@ func _randomize_appearance():
|
||||
var ear_type = appearance_rng.randi_range(0, 7)
|
||||
_set_ears(ear_type)
|
||||
|
||||
# Randomize hair color (bright colors for enemies)
|
||||
# Randomize hair color - vibrant and weird colors! (same as players)
|
||||
var hair_colors = [
|
||||
Color.WHITE, Color(0.9, 0.9, 0.9), Color(0.7, 0.7, 0.7), # White/Gray
|
||||
Color(0.5, 0.3, 0.2), Color(0.3, 0.2, 0.1), # Brown/Black
|
||||
Color(0.9, 0.7, 0.4), Color(0.8, 0.6, 0.3) # Blonde
|
||||
Color.WHITE, Color.BLACK, Color(0.4, 0.2, 0.1), # Brown
|
||||
Color(0.8, 0.6, 0.4), # Blonde
|
||||
Color(0.6, 0.3, 0.1), # Dark brown
|
||||
Color(0.9, 0.7, 0.5), # Light blonde
|
||||
Color(0.2, 0.2, 0.2), # Dark gray
|
||||
Color(0.5, 0.5, 0.5), # Gray
|
||||
Color(0.5, 0.8, 0.2), # Snot green
|
||||
Color(0.9, 0.5, 0.1), # Orange
|
||||
Color(0.8, 0.3, 0.9), # Purple
|
||||
Color(1.0, 0.9, 0.2), # Yellow
|
||||
Color(1.0, 0.5, 0.8), # Pink
|
||||
Color(0.9, 0.2, 0.2), # Red
|
||||
Color(0.2, 0.9, 0.9), # Bright cyan
|
||||
Color(0.6, 0.2, 0.9), # Magenta
|
||||
Color(0.9, 0.7, 0.2), # Gold
|
||||
Color(0.3, 0.9, 0.3), # Bright green
|
||||
Color(0.2, 0.2, 0.9), # Bright blue
|
||||
Color(0.9, 0.4, 0.6), # Hot pink
|
||||
Color(0.5, 0.2, 0.8), # Deep purple
|
||||
Color(0.9, 0.6, 0.1) # Amber
|
||||
]
|
||||
var hair_color = hair_colors[appearance_rng.randi() % hair_colors.size()]
|
||||
_set_hair_color(hair_color)
|
||||
|
||||
# Set facial hair color to match hair color or slightly different
|
||||
# Set facial hair color - usually matches hair, but can be different (30% chance)
|
||||
var facial_hair_color = hair_color
|
||||
if appearance_rng.randf() < 0.3: # 30% chance for slightly different color
|
||||
facial_hair_color = hair_colors[appearance_rng.randi() % hair_colors.size()]
|
||||
_set_facial_hair_color(facial_hair_color)
|
||||
if facial_hair_type > 0: # Only set color if they have facial hair
|
||||
if appearance_rng.randf() < 0.3: # 30% chance for different color
|
||||
facial_hair_color = hair_colors[appearance_rng.randi() % hair_colors.size()]
|
||||
_set_facial_hair_color(facial_hair_color)
|
||||
else:
|
||||
# No facial hair, but still set a default color (won't be visible)
|
||||
_set_facial_hair_color(hair_color)
|
||||
|
||||
func _set_skin(i_value: int):
|
||||
if i_value < 0 or i_value > 6:
|
||||
@@ -348,6 +369,10 @@ func _set_hair(i_type: int):
|
||||
sprite_hair.texture = hair_texture
|
||||
sprite_hair.hframes = 35
|
||||
sprite_hair.vframes = 8
|
||||
# Apply hair color after loading texture (in case it was set before)
|
||||
# Note: This will be set in _randomize_appearance, but ensure it's applied here too
|
||||
if sprite_hair.modulate != Color.WHITE:
|
||||
_set_hair_color(sprite_hair.modulate)
|
||||
|
||||
func _set_eye_lashes(i_eyelashes: int):
|
||||
if i_eyelashes < 0 or i_eyelashes > 8:
|
||||
@@ -460,11 +485,23 @@ func _set_ears(i_ears: int):
|
||||
sprite_addons.vframes = 8
|
||||
|
||||
func _set_facial_hair_color(i_color: Color):
|
||||
if sprite_facial_hair:
|
||||
if not sprite_facial_hair:
|
||||
return
|
||||
# Use shader tint parameter instead of modulate (same as players)
|
||||
if sprite_facial_hair.material and sprite_facial_hair.material is ShaderMaterial:
|
||||
sprite_facial_hair.material.set_shader_parameter("tint", Vector4(i_color.r, i_color.g, i_color.b, i_color.a))
|
||||
else:
|
||||
# Fallback to modulate if no shader material
|
||||
sprite_facial_hair.modulate = i_color
|
||||
|
||||
func _set_hair_color(i_color: Color):
|
||||
if sprite_hair:
|
||||
if not sprite_hair:
|
||||
return
|
||||
# Use shader tint parameter instead of modulate (same as players)
|
||||
if sprite_hair.material and sprite_hair.material is ShaderMaterial:
|
||||
sprite_hair.material.set_shader_parameter("tint", Vector4(i_color.r, i_color.g, i_color.b, i_color.a))
|
||||
else:
|
||||
# Fallback to modulate if no shader material
|
||||
sprite_hair.modulate = i_color
|
||||
|
||||
func _get_body_texture_for_type(type: HumanoidType) -> String:
|
||||
@@ -1264,6 +1301,25 @@ func _set_animation(anim_name: String):
|
||||
|
||||
func _update_animation(delta):
|
||||
# Update animation frame timing (even when dead, to play death animation)
|
||||
# CRITICAL: If dead, ensure DIE animation is set (don't let other animations override it)
|
||||
if is_dead:
|
||||
if current_animation != "DIE" and "DIE" in ANIMATIONS:
|
||||
LogManager.log(str(name) + " (client) forcing DIE animation in _update_animation - was: " + str(current_animation), LogManager.CATEGORY_ENEMY)
|
||||
current_animation = "DIE"
|
||||
current_frame = 0
|
||||
time_since_last_frame = 0.0
|
||||
|
||||
# CRITICAL: Ensure we have a valid animation set
|
||||
if not current_animation in ANIMATIONS:
|
||||
LogManager.log(str(name) + " WARNING: current_animation '" + str(current_animation) + "' not in ANIMATIONS!", LogManager.CATEGORY_ENEMY)
|
||||
# Fallback to IDLE if animation is invalid (or DIE if dead)
|
||||
if is_dead and "DIE" in ANIMATIONS:
|
||||
current_animation = "DIE"
|
||||
elif "IDLE" in ANIMATIONS:
|
||||
current_animation = "IDLE"
|
||||
else:
|
||||
return # Can't update animation without valid animation
|
||||
|
||||
time_since_last_frame += delta
|
||||
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)
|
||||
@@ -1404,13 +1460,37 @@ func _flash_damage():
|
||||
tween.tween_property(sprite_layer, "modulate", Color.RED, 0.1)
|
||||
tween.tween_property(sprite_layer, "modulate", Color.WHITE, 0.1)
|
||||
|
||||
func _on_take_damage():
|
||||
# Override to play damage animation (same as player)
|
||||
func _on_take_damage(attacker_position: Vector2 = Vector2.ZERO):
|
||||
# CRITICAL: Don't play damage animation if already dead
|
||||
# This prevents damage sync from overriding death animation on clients
|
||||
if is_dead:
|
||||
return
|
||||
|
||||
# Override to play damage animation and face attacker (same as player)
|
||||
_set_animation("DAMAGE")
|
||||
|
||||
# Face the attacker (if attacker position is provided)
|
||||
if attacker_position != Vector2.ZERO:
|
||||
# Calculate direction FROM attacker TO victim
|
||||
var direction_from_attacker = (global_position - attacker_position).normalized()
|
||||
# Face the attacker (opposite of direction from attacker)
|
||||
current_direction = _get_direction_from_vector(-direction_from_attacker) as Direction
|
||||
|
||||
func _play_death_animation():
|
||||
# Override to play death animation (same as player)
|
||||
# Ensure animation is set and animation state is properly initialized
|
||||
LogManager.log(str(name) + " _play_death_animation() called, is_authority: " + str(is_multiplayer_authority()) + ", current_animation: " + str(current_animation), LogManager.CATEGORY_ENEMY)
|
||||
_set_animation("DIE")
|
||||
# Force animation frame reset to ensure animation starts from beginning
|
||||
current_frame = 0
|
||||
time_since_last_frame = 0.0
|
||||
LogManager.log(str(name) + " _play_death_animation() set DIE animation, current_frame: " + str(current_frame) + ", current_animation: " + str(current_animation), LogManager.CATEGORY_ENEMY)
|
||||
|
||||
# CRITICAL: Force immediate visual update on clients to ensure animation is visible
|
||||
# Call _update_animation with 0 delta to apply current frame immediately
|
||||
if not is_multiplayer_authority():
|
||||
_update_animation(0.0)
|
||||
LogManager.log(str(name) + " (client) forced immediate animation update after setting DIE", LogManager.CATEGORY_ENEMY)
|
||||
|
||||
# Play death sound effect
|
||||
if sfx_die:
|
||||
|
||||
@@ -243,7 +243,7 @@ func _update_animation(delta):
|
||||
sprite.frame = frame_index
|
||||
anim_frame = frame_index # Keep anim_frame updated for compatibility
|
||||
|
||||
func _on_take_damage():
|
||||
func _on_take_damage(_attacker_position: Vector2 = Vector2.ZERO):
|
||||
# Play damage animation
|
||||
state = SlimeState.DAMAGED
|
||||
state_timer = 0.3
|
||||
@@ -268,6 +268,15 @@ func _update_client_visuals():
|
||||
# Update visuals on clients based on synced state
|
||||
super._update_client_visuals()
|
||||
|
||||
# CRITICAL: If dead, always show DIE animation (don't let state override it)
|
||||
# This prevents DAMAGED state from showing DAMAGE animation after death sync
|
||||
if is_dead:
|
||||
_set_animation("DIE")
|
||||
_update_animation(0.0)
|
||||
if sprite:
|
||||
sprite.frame = anim_frame
|
||||
return
|
||||
|
||||
# Map synced state to animation (similar to how bat/rat use state directly)
|
||||
match state:
|
||||
SlimeState.IDLE:
|
||||
|
||||
@@ -94,30 +94,30 @@ func _on_body_exited(body):
|
||||
return
|
||||
|
||||
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
|
||||
# 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":
|
||||
# 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:
|
||||
if is_inside_tree() and has_node("ReleaseSwitch") and get_node("ReleaseSwitch").is_inside_tree():
|
||||
get_node("ReleaseSwitch").play()
|
||||
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:
|
||||
if is_inside_tree() and $ReleaseSwitch:
|
||||
$ReleaseSwitch.play()
|
||||
if is_inside_tree() and has_node("ReleaseSwitch") and get_node("ReleaseSwitch").is_inside_tree():
|
||||
get_node("ReleaseSwitch").play()
|
||||
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:
|
||||
if is_inside_tree() and $ReleaseSwitch:
|
||||
$ReleaseSwitch.play()
|
||||
objects_on_switch.erase(body)
|
||||
current_weight -= weight
|
||||
_check_activation()
|
||||
|
||||
func _get_object_weight(body: Node) -> float:
|
||||
# Get weight of an object
|
||||
|
||||
46
src/scripts/fog_of_war.gd
Normal file
46
src/scripts/fog_of_war.gd
Normal file
@@ -0,0 +1,46 @@
|
||||
extends Node2D
|
||||
|
||||
var map_size: Vector2i = Vector2i.ZERO
|
||||
var tile_size: int = 16
|
||||
var explored_map: PackedInt32Array = PackedInt32Array()
|
||||
var visible_map: PackedInt32Array = PackedInt32Array()
|
||||
var fog_color_unseen: Color = Color(0, 0, 0, 1.0)
|
||||
var fog_color_seen: Color = Color(0, 0, 0, 0.85)
|
||||
var debug_lines: Array = []
|
||||
var debug_enabled: bool = false
|
||||
|
||||
func setup(new_map_size: Vector2i, new_tile_size: int = 16) -> void:
|
||||
map_size = new_map_size
|
||||
tile_size = new_tile_size
|
||||
|
||||
func set_maps(new_explored_map: PackedInt32Array, new_visible_map: PackedInt32Array) -> void:
|
||||
explored_map = new_explored_map
|
||||
visible_map = new_visible_map
|
||||
queue_redraw()
|
||||
|
||||
func set_debug_lines(lines: Array, enabled: bool) -> void:
|
||||
debug_lines = lines
|
||||
debug_enabled = enabled
|
||||
queue_redraw()
|
||||
|
||||
func _draw() -> void:
|
||||
if map_size == Vector2i.ZERO or explored_map.is_empty() or visible_map.is_empty():
|
||||
return
|
||||
|
||||
for x in range(map_size.x):
|
||||
for y in range(map_size.y):
|
||||
var idx = x + y * map_size.x
|
||||
if idx >= explored_map.size() or idx >= visible_map.size():
|
||||
continue
|
||||
var pos = Vector2(x * tile_size, y * tile_size)
|
||||
if visible_map[idx] == 1:
|
||||
continue
|
||||
if explored_map[idx] == 0:
|
||||
draw_rect(Rect2(pos, Vector2(tile_size, tile_size)), fog_color_unseen, true)
|
||||
else:
|
||||
draw_rect(Rect2(pos, Vector2(tile_size, tile_size)), fog_color_seen, true)
|
||||
|
||||
if debug_enabled:
|
||||
for line in debug_lines:
|
||||
if line is Array and line.size() == 2:
|
||||
draw_line(line[0], line[1], Color(0, 1, 0, 0.4), 1.0)
|
||||
1
src/scripts/fog_of_war.gd.uid
Normal file
1
src/scripts/fog_of_war.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dvexhx0su0ung
|
||||
@@ -154,6 +154,7 @@ func _check_command_line_args():
|
||||
LogManager.log("Auto-joining: No address provided, fetching available rooms (mode: " + str(current_mode) + ")...", LogManager.CATEGORY_UI)
|
||||
network_manager.set_local_player_count(local_count)
|
||||
is_auto_joining = true
|
||||
is_joining_attempt = true # Mark as joining attempt so connection failure handler works
|
||||
# Create timer for retrying room fetches
|
||||
room_fetch_timer = Timer.new()
|
||||
room_fetch_timer.name = "RoomFetchTimer"
|
||||
@@ -188,8 +189,11 @@ func _on_rooms_fetched_display(rooms: Array):
|
||||
"""Display available rooms when fetched (non-auto-join mode)"""
|
||||
# Only handle if not in auto-join mode (auto-join has its own handler)
|
||||
if is_auto_joining:
|
||||
LogManager.log("GameUI: Ignoring rooms_fetched_display - still in auto-join mode", LogManager.CATEGORY_UI)
|
||||
return # Let auto-join handler take care of it
|
||||
|
||||
LogManager.log("GameUI: Received rooms for display: " + str(rooms.size()) + " rooms", LogManager.CATEGORY_UI)
|
||||
|
||||
# Hide loading indicator - request completed
|
||||
_hide_loading_indicator()
|
||||
|
||||
@@ -237,12 +241,11 @@ func _on_rooms_fetched_auto_join(rooms: Array):
|
||||
# Stop retrying - we found rooms!
|
||||
if room_fetch_timer:
|
||||
room_fetch_timer.stop()
|
||||
is_auto_joining = false
|
||||
|
||||
# Hide room fetch status UI
|
||||
_hide_room_fetch_status()
|
||||
# DON'T set is_auto_joining = false yet - wait until connection succeeds or fails
|
||||
# DON'T hide room fetch status UI yet - keep it visible in case join fails
|
||||
|
||||
# Disconnect from signal since we're done
|
||||
# Disconnect from auto-join handler (we'll connect to display handler if join fails)
|
||||
if network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join):
|
||||
network_manager.rooms_fetched.disconnect(_on_rooms_fetched_auto_join)
|
||||
|
||||
@@ -258,6 +261,9 @@ func _on_rooms_fetched_auto_join(rooms: Array):
|
||||
if room_fetch_timer:
|
||||
room_fetch_timer.start()
|
||||
is_auto_joining = true
|
||||
# Reconnect to auto-join handler
|
||||
if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join):
|
||||
network_manager.rooms_fetched.connect(_on_rooms_fetched_auto_join)
|
||||
# Keep showing status UI
|
||||
return
|
||||
|
||||
@@ -269,7 +275,20 @@ func _on_rooms_fetched_auto_join(rooms: Array):
|
||||
network_manager.set_local_player_count(local_count)
|
||||
if network_manager.join_game(room_code):
|
||||
# Connection callback will handle starting the game
|
||||
# Note: We'll hide the UI and set is_auto_joining = false in _on_connection_succeeded
|
||||
pass
|
||||
else:
|
||||
# Join failed immediately - switch to display mode
|
||||
LogManager.log("Auto-join failed immediately, switching to room browser mode", LogManager.CATEGORY_UI)
|
||||
is_auto_joining = false
|
||||
# Connect to display handler
|
||||
if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_display):
|
||||
network_manager.rooms_fetched.connect(_on_rooms_fetched_display)
|
||||
# Keep room fetch status UI visible (with refresh button)
|
||||
_show_room_fetch_status()
|
||||
# Fetch and display available rooms
|
||||
_show_loading_indicator()
|
||||
_start_room_fetch()
|
||||
|
||||
func _retry_room_fetch():
|
||||
"""Retry fetching available rooms"""
|
||||
@@ -375,6 +394,25 @@ func _create_refresh_button():
|
||||
|
||||
func _on_refresh_button_pressed():
|
||||
"""Handle refresh button click"""
|
||||
LogManager.log("GameUI: Refresh button pressed", LogManager.CATEGORY_UI)
|
||||
|
||||
# CRITICAL: Ensure we're not in auto-join mode when refreshing manually
|
||||
# This prevents _on_rooms_fetched_display from ignoring the signal
|
||||
if is_auto_joining:
|
||||
LogManager.log("GameUI: Switching from auto-join to display mode for refresh", LogManager.CATEGORY_UI)
|
||||
is_auto_joining = false
|
||||
# Stop auto-join timer if it's running
|
||||
if room_fetch_timer:
|
||||
room_fetch_timer.stop()
|
||||
# Disconnect auto-join handler
|
||||
if network_manager and network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join):
|
||||
network_manager.rooms_fetched.disconnect(_on_rooms_fetched_auto_join)
|
||||
|
||||
# Ensure display handler is connected (in case it was disconnected)
|
||||
if network_manager and not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_display):
|
||||
LogManager.log("GameUI: Reconnecting rooms_fetched signal to display handler", LogManager.CATEGORY_UI)
|
||||
network_manager.rooms_fetched.connect(_on_rooms_fetched_display)
|
||||
|
||||
# Disable button and start cooldown
|
||||
if refresh_button:
|
||||
refresh_button.disabled = true
|
||||
@@ -394,10 +432,17 @@ func _on_refresh_cooldown_finished():
|
||||
|
||||
func _update_last_fetch_time():
|
||||
"""Update the last fetch time label with current datetime"""
|
||||
# Try to find the label if it's null (might not be ready yet)
|
||||
if not last_fetch_label:
|
||||
last_fetch_label = get_node_or_null("Control/MainMenu/VBoxContainer/RoomFetchStatusContainer/LastFetchLabel")
|
||||
|
||||
if last_fetch_label:
|
||||
var now = Time.get_datetime_dict_from_system()
|
||||
var time_str = "%02d:%02d:%02d" % [now.hour, now.minute, now.second]
|
||||
last_fetch_label.text = "Last fetched: " + time_str
|
||||
LogManager.log("GameUI: Updated last fetch time to: " + time_str, LogManager.CATEGORY_UI)
|
||||
else:
|
||||
LogManager.log_error("GameUI: Cannot update last fetch time - last_fetch_label is null!")
|
||||
|
||||
func _create_room_list_container():
|
||||
"""Create the container for displaying available rooms"""
|
||||
@@ -519,11 +564,23 @@ func _on_network_mode_changed(index: int):
|
||||
var mode_names = ["ENet", "WebRTC", "WebSocket"]
|
||||
LogManager.log("GameUI: Network mode changed to: " + mode_names[actual_mode], LogManager.CATEGORY_UI)
|
||||
|
||||
# If WebRTC is selected, fetch available rooms (unless we're auto-joining or hosting)
|
||||
if actual_mode == 1 and not is_auto_joining and not is_hosting: # WebRTC mode
|
||||
_start_room_fetch()
|
||||
elif actual_mode != 1: # Not WebRTC mode
|
||||
# Handle room fetching based on mode
|
||||
if actual_mode == 1: # WebRTC mode
|
||||
# Only fetch if not auto-joining and not hosting
|
||||
if not is_auto_joining and not is_hosting and not network_manager.is_hosting:
|
||||
LogManager.log("GameUI: Switched to WebRTC mode, fetching rooms", LogManager.CATEGORY_UI)
|
||||
# Ensure display handler is connected
|
||||
if network_manager and not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_display):
|
||||
network_manager.rooms_fetched.connect(_on_rooms_fetched_display)
|
||||
# Show room fetch status UI (with refresh button)
|
||||
_show_room_fetch_status()
|
||||
# Fetch rooms
|
||||
_start_room_fetch()
|
||||
else:
|
||||
LogManager.log("GameUI: Switched to WebRTC mode but skipping room fetch (auto_joining: " + str(is_auto_joining) + ", hosting: " + str(is_hosting) + ")", LogManager.CATEGORY_UI)
|
||||
else: # Not WebRTC mode (ENet or WebSocket)
|
||||
# Hide room fetch status if switching away from WebRTC
|
||||
LogManager.log("GameUI: Switched away from WebRTC mode, hiding room fetch UI", LogManager.CATEGORY_UI)
|
||||
_hide_room_fetch_status()
|
||||
|
||||
func _on_host_pressed():
|
||||
@@ -572,6 +629,16 @@ func _on_join_pressed():
|
||||
func _on_connection_succeeded():
|
||||
LogManager.log("GameUI: Connection succeeded signal received, starting game", LogManager.CATEGORY_UI)
|
||||
is_joining_attempt = false
|
||||
|
||||
# If we were in auto-join mode, now we can safely exit it
|
||||
if is_auto_joining:
|
||||
is_auto_joining = false
|
||||
# Hide room fetch status UI since we're connecting
|
||||
_hide_room_fetch_status()
|
||||
# Disconnect from auto-join handler if still connected
|
||||
if network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join):
|
||||
network_manager.rooms_fetched.disconnect(_on_rooms_fetched_auto_join)
|
||||
|
||||
# Check if node is still valid before starting game
|
||||
if not is_inside_tree():
|
||||
LogManager.log_error("GameUI: Cannot start game - node not in tree", LogManager.CATEGORY_UI)
|
||||
@@ -593,6 +660,28 @@ func _on_connection_failed():
|
||||
elif mode == 2:
|
||||
mode_name = "WebSocket"
|
||||
|
||||
# If we were in auto-join mode, switch to display mode and show rooms
|
||||
if is_auto_joining:
|
||||
LogManager.log("Connection failed during auto-join, switching to room browser mode", LogManager.CATEGORY_UI)
|
||||
is_auto_joining = false
|
||||
# Stop auto-join timer
|
||||
if room_fetch_timer:
|
||||
room_fetch_timer.stop()
|
||||
# Disconnect from auto-join handler
|
||||
if network_manager.rooms_fetched.is_connected(_on_rooms_fetched_auto_join):
|
||||
network_manager.rooms_fetched.disconnect(_on_rooms_fetched_auto_join)
|
||||
# Connect to display handler instead
|
||||
if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_display):
|
||||
network_manager.rooms_fetched.connect(_on_rooms_fetched_display)
|
||||
# Show room fetch status UI (refresh button and room list)
|
||||
_show_room_fetch_status()
|
||||
# Fetch and display available rooms
|
||||
_show_loading_indicator()
|
||||
_start_room_fetch()
|
||||
# Show error message
|
||||
_show_connection_error("Failed to auto-join room. Showing available rooms below.")
|
||||
return
|
||||
|
||||
if is_joining_attempt:
|
||||
var code_hint = (" (" + last_join_address + ")") if not last_join_address.is_empty() else ""
|
||||
_show_connection_error("Failed to join room" + code_hint + ". Did you enter the correct code?")
|
||||
@@ -603,6 +692,9 @@ func _on_connection_failed():
|
||||
_show_room_fetch_status()
|
||||
_show_loading_indicator()
|
||||
_start_room_fetch()
|
||||
# Also connect to display handler if not already connected
|
||||
if not network_manager.rooms_fetched.is_connected(_on_rooms_fetched_display):
|
||||
network_manager.rooms_fetched.connect(_on_rooms_fetched_display)
|
||||
else:
|
||||
_show_connection_error("Connection failed (" + mode_name + "). Please try again.")
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,9 @@ var label_host: Label = null
|
||||
var label_player_count: Label = null
|
||||
var label_room_code: Label = null
|
||||
var label_disconnected: Label = null
|
||||
var label_matchbox_status: Label = null
|
||||
var label_ice_status: Label = null
|
||||
var label_data_channels_status: Label = null
|
||||
|
||||
var game_world: Node = null
|
||||
var network_manager: Node = null
|
||||
@@ -44,6 +47,17 @@ func _ready():
|
||||
label_player_count = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelPlayerCount")
|
||||
label_room_code = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerHost/LabelRoomCode")
|
||||
label_disconnected = get_node_or_null("CenterTop/LabelDisconnected")
|
||||
label_matchbox_status = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerConnectionStatus/LabelMatchboxStatus")
|
||||
label_ice_status = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerConnectionStatus/LabelICEStatus")
|
||||
label_data_channels_status = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerConnectionStatus/LabelDataChannelsStatus")
|
||||
|
||||
# Debug: Log if connection status labels weren't found
|
||||
if not label_matchbox_status:
|
||||
print("IngameHUD: WARNING - label_matchbox_status not found!")
|
||||
if not label_ice_status:
|
||||
print("IngameHUD: WARNING - label_ice_status not found!")
|
||||
if not label_data_channels_status:
|
||||
print("IngameHUD: WARNING - label_data_channels_status not found!")
|
||||
|
||||
# Find network manager
|
||||
network_manager = get_node_or_null("/root/NetworkManager")
|
||||
@@ -78,6 +92,11 @@ func _ready():
|
||||
# Update host info display
|
||||
_update_host_info()
|
||||
|
||||
# Initially hide connection status (will be shown if WebRTC mode)
|
||||
var connection_status_container = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerConnectionStatus")
|
||||
if connection_status_container:
|
||||
connection_status_container.visible = false
|
||||
|
||||
# Start level timer
|
||||
level_start_time = Time.get_ticks_msec() / 1000.0
|
||||
|
||||
@@ -189,6 +208,9 @@ func _process(_delta):
|
||||
# Update boss health (if boss exists)
|
||||
_update_boss_health()
|
||||
|
||||
# Update connection status (only for WebRTC)
|
||||
_update_connection_status()
|
||||
|
||||
func _update_hud_scale():
|
||||
# Scale HUD to an integer factor to keep pixel text crisp
|
||||
var viewport_size = get_viewport().get_visible_rect().size
|
||||
@@ -299,3 +321,120 @@ func _update_boss_health():
|
||||
func reset_level_timer():
|
||||
# Reset timer when starting a new level
|
||||
start_timer()
|
||||
|
||||
func _update_connection_status():
|
||||
# Only show connection status for WebRTC mode
|
||||
var connection_status_container = get_node_or_null("UpperRight/HBoxContainer/VBoxContainerConnectionStatus")
|
||||
if not network_manager:
|
||||
if connection_status_container:
|
||||
connection_status_container.visible = false
|
||||
return
|
||||
|
||||
if network_manager.network_mode != 1: # Not WebRTC
|
||||
if connection_status_container:
|
||||
connection_status_container.visible = false
|
||||
return
|
||||
|
||||
# Show connection status container and labels (for both host and joiner)
|
||||
if connection_status_container:
|
||||
connection_status_container.visible = true
|
||||
else:
|
||||
# Debug: Log if container not found
|
||||
if not has_meta("connection_status_container_warned"):
|
||||
set_meta("connection_status_container_warned", true)
|
||||
print("IngameHUD: WARNING - connection_status_container not found!")
|
||||
|
||||
if label_matchbox_status:
|
||||
label_matchbox_status.visible = true
|
||||
else:
|
||||
if not has_meta("label_matchbox_status_warned"):
|
||||
set_meta("label_matchbox_status_warned", true)
|
||||
print("IngameHUD: WARNING - label_matchbox_status not found!")
|
||||
|
||||
if label_ice_status:
|
||||
label_ice_status.visible = true
|
||||
else:
|
||||
if not has_meta("label_ice_status_warned"):
|
||||
set_meta("label_ice_status_warned", true)
|
||||
print("IngameHUD: WARNING - label_ice_status not found!")
|
||||
|
||||
if label_data_channels_status:
|
||||
label_data_channels_status.visible = true
|
||||
else:
|
||||
if not has_meta("label_data_channels_status_warned"):
|
||||
set_meta("label_data_channels_status_warned", true)
|
||||
print("IngameHUD: WARNING - label_data_channels_status not found!")
|
||||
|
||||
# Update Matchbox connection status
|
||||
if network_manager.matchbox_client and label_matchbox_status:
|
||||
var is_net_connected = network_manager.matchbox_client.is_matchbox_connected()
|
||||
if is_net_connected:
|
||||
label_matchbox_status.text = "Matchbox: Connected"
|
||||
label_matchbox_status.modulate = Color.GREEN
|
||||
else:
|
||||
label_matchbox_status.text = "Matchbox: Disconnected"
|
||||
label_matchbox_status.modulate = Color.RED
|
||||
|
||||
# Update ICE connection status
|
||||
if network_manager.matchbox_client and label_ice_status:
|
||||
var ice_status = network_manager.matchbox_client.get_ice_connection_status()
|
||||
if ice_status.is_empty():
|
||||
label_ice_status.text = "ICE: No peers"
|
||||
label_ice_status.modulate = Color.YELLOW
|
||||
else:
|
||||
# Check if any peer has ICE connected
|
||||
# For WebRTC, we consider it "connected" if:
|
||||
# 1. Connection state is CONNECTED (2), OR
|
||||
# 2. Signaling is STABLE (0) - offer/answer exchange complete (even if ICE still finalizing)
|
||||
# This is more lenient because WebRTC in browsers can have different timing than native builds
|
||||
var any_connected = false
|
||||
var any_establishing = false
|
||||
for peer_id in ice_status.keys():
|
||||
var peer_status = ice_status[peer_id]
|
||||
var conn_state = peer_status.get("connection_state", -1)
|
||||
var sig_state = peer_status.get("signaling_state", -1)
|
||||
|
||||
# Connection is fully connected
|
||||
if conn_state == 2: # CONNECTED
|
||||
any_connected = true
|
||||
break
|
||||
# Signaling is STABLE (0) - offer/answer exchange is complete
|
||||
# Even if ICE is still CONNECTING, the connection is essentially established
|
||||
# This matches native behavior where everything works fine
|
||||
elif sig_state == 0: # STABLE signaling means connection is ready
|
||||
any_establishing = true
|
||||
|
||||
if any_connected or any_establishing:
|
||||
label_ice_status.text = "ICE: Connected"
|
||||
label_ice_status.modulate = Color.GREEN
|
||||
else:
|
||||
# Check connection state and ICE candidate exchange
|
||||
var connecting = false
|
||||
var candidate_info = ""
|
||||
for peer_id in ice_status.keys():
|
||||
var conn_state = ice_status[peer_id].get("connection_state", -1)
|
||||
if conn_state == 1: # CONNECTING
|
||||
connecting = true
|
||||
# Show ICE candidate counts to help debug
|
||||
var sent = ice_status[peer_id].get("candidates_sent", 0)
|
||||
var received = ice_status[peer_id].get("candidates_received", 0)
|
||||
if sent > 0 or received > 0:
|
||||
candidate_info = " (sent: " + str(sent) + ", recv: " + str(received) + ")"
|
||||
break
|
||||
|
||||
if connecting:
|
||||
label_ice_status.text = "ICE: Connecting..." + candidate_info
|
||||
label_ice_status.modulate = Color.YELLOW
|
||||
else:
|
||||
label_ice_status.text = "ICE: Disconnected"
|
||||
label_ice_status.modulate = Color.RED
|
||||
|
||||
# Update data channels status
|
||||
if network_manager.matchbox_client and label_data_channels_status:
|
||||
var channels_connected = network_manager.matchbox_client.are_data_channels_connected()
|
||||
if channels_connected:
|
||||
label_data_channels_status.text = "Data: Connected"
|
||||
label_data_channels_status.modulate = Color.GREEN
|
||||
else:
|
||||
label_data_channels_status.text = "Data: Disconnected"
|
||||
label_data_channels_status.modulate = Color.RED
|
||||
|
||||
@@ -499,7 +499,19 @@ func setup_pot():
|
||||
|
||||
var pot_frames = [1, 2, 3, 20, 21, 22, 58]
|
||||
if sprite:
|
||||
sprite.frame = pot_frames[randi() % pot_frames.size()]
|
||||
var box_seed = 0
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if game_world and "dungeon_seed" in game_world:
|
||||
box_seed = game_world.dungeon_seed
|
||||
# Add position and object_index to seed to make each box unique but deterministic
|
||||
box_seed += int(global_position.x) * 1000 + int(global_position.y)
|
||||
if has_meta("object_index"):
|
||||
box_seed += get_meta("object_index") * 10000
|
||||
|
||||
var rng = RandomNumberGenerator.new()
|
||||
rng.seed = box_seed
|
||||
var index = rng.randi() % pot_frames.size()
|
||||
sprite.frame = pot_frames[index]
|
||||
|
||||
func setup_liftable_barrel():
|
||||
object_type = "LiftableBarrel"
|
||||
@@ -511,7 +523,19 @@ func setup_liftable_barrel():
|
||||
|
||||
var barrel_frames = [4, 23]
|
||||
if sprite:
|
||||
sprite.frame = barrel_frames[randi() % barrel_frames.size()]
|
||||
var box_seed = 0
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if game_world and "dungeon_seed" in game_world:
|
||||
box_seed = game_world.dungeon_seed
|
||||
# Add position and object_index to seed to make each box unique but deterministic
|
||||
box_seed += int(global_position.x) * 1000 + int(global_position.y)
|
||||
if has_meta("object_index"):
|
||||
box_seed += get_meta("object_index") * 10000
|
||||
|
||||
var rng = RandomNumberGenerator.new()
|
||||
rng.seed = box_seed
|
||||
var index = rng.randi() % barrel_frames.size()
|
||||
sprite.frame = barrel_frames[index]
|
||||
|
||||
func setup_pushable_barrel():
|
||||
object_type = "PushableBarrel"
|
||||
@@ -607,9 +631,9 @@ func setup_pushable_high_box():
|
||||
# Use deterministic randomness based on dungeon seed and position
|
||||
# This ensures host and clients get the same chest variant
|
||||
var highbox_seed = 0
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if game_world and "dungeon_seed" in game_world:
|
||||
highbox_seed = game_world.dungeon_seed
|
||||
var world = get_tree().get_first_node_in_group("game_world")
|
||||
if world and "dungeon_seed" in world:
|
||||
highbox_seed = world.dungeon_seed
|
||||
# Add position to seed to make each chest unique but deterministic
|
||||
highbox_seed += int(global_position.x) * 1000 + int(global_position.y)
|
||||
|
||||
@@ -634,77 +658,103 @@ func _open_chest(by_player: Node = null):
|
||||
|
||||
# Track opened chest for syncing to new clients
|
||||
if multiplayer.has_multiplayer_peer() and multiplayer.is_server():
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if game_world and has_meta("object_index"):
|
||||
var world = get_tree().get_first_node_in_group("game_world")
|
||||
if world and has_meta("object_index"):
|
||||
var obj_index = get_meta("object_index")
|
||||
game_world.opened_chests[obj_index] = true
|
||||
world.opened_chests[obj_index] = true
|
||||
LogManager.log("Chest: Tracked opened chest with index " + str(obj_index), LogManager.CATEGORY_NETWORK)
|
||||
if sprite and chest_opened_frame >= 0:
|
||||
sprite.frame = chest_opened_frame
|
||||
|
||||
# Random loot type
|
||||
var loot_types = [
|
||||
{"type": "coin", "name": "Coin", "color": Color(1.0, 0.84, 0.0)},
|
||||
{"type": "apple", "name": "Apple", "color": Color.GREEN},
|
||||
{"type": "banana", "name": "Banana", "color": Color.YELLOW},
|
||||
{"type": "cherry", "name": "Cherry", "color": Color.RED},
|
||||
{"type": "key", "name": "Key", "color": Color.YELLOW}
|
||||
]
|
||||
var selected_loot = loot_types[randi() % loot_types.size()]
|
||||
# Get random item from entire item database (using chest rarity weights)
|
||||
# Use deterministic randomness based on dungeon seed and chest position
|
||||
var chest_seed = 0
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if game_world and "dungeon_seed" in game_world:
|
||||
chest_seed = game_world.dungeon_seed
|
||||
# Add position to seed to make each chest unique but deterministic
|
||||
chest_seed += int(global_position.x) * 1000 + int(global_position.y)
|
||||
|
||||
# Create deterministic RNG for this chest
|
||||
var chest_rng = RandomNumberGenerator.new()
|
||||
chest_rng.seed = chest_seed
|
||||
|
||||
# Get random item using deterministic RNG
|
||||
# We need to manually select by rarity since get_random_chest_item() uses global randi()
|
||||
var rarity_roll = chest_rng.randf()
|
||||
var rarity: ItemDatabase.ItemRarity
|
||||
if rarity_roll < 0.4:
|
||||
rarity = ItemDatabase.ItemRarity.COMMON
|
||||
elif rarity_roll < 0.75:
|
||||
rarity = ItemDatabase.ItemRarity.UNCOMMON
|
||||
elif rarity_roll < 0.95:
|
||||
rarity = ItemDatabase.ItemRarity.RARE
|
||||
else:
|
||||
rarity = ItemDatabase.ItemRarity.EPIC if chest_rng.randf() < 0.5 else ItemDatabase.ItemRarity.CONSUMABLE
|
||||
|
||||
# Get candidates for this rarity using deterministic RNG
|
||||
ItemDatabase._initialize()
|
||||
var candidates = []
|
||||
# Access static item_definitions directly
|
||||
for item_id in ItemDatabase.item_definitions.keys():
|
||||
var item_data = ItemDatabase.item_definitions[item_id]
|
||||
if item_data.has("rarity") and item_data["rarity"] == rarity:
|
||||
candidates.append(item_id)
|
||||
|
||||
# Fallback to common if no candidates
|
||||
if candidates.is_empty():
|
||||
for item_id in ItemDatabase.item_definitions.keys():
|
||||
var item_data = ItemDatabase.item_definitions[item_id]
|
||||
if item_data.has("rarity") and item_data["rarity"] == ItemDatabase.ItemRarity.COMMON:
|
||||
candidates.append(item_id)
|
||||
|
||||
# Select random item from candidates using deterministic RNG
|
||||
var random_item_id = candidates[chest_rng.randi() % candidates.size()] if not candidates.is_empty() else null
|
||||
var chest_item = ItemDatabase.create_item(random_item_id) if random_item_id else null
|
||||
|
||||
# CRITICAL: Instantly give item to player instead of spawning loot object
|
||||
if by_player and is_instance_valid(by_player) and by_player.is_in_group("player"):
|
||||
# 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) + 10)
|
||||
"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) + 11)
|
||||
"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) + 12)
|
||||
"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)
|
||||
if by_player and is_instance_valid(by_player) and by_player.is_in_group("player") and chest_item:
|
||||
# Add item to player inventory
|
||||
if by_player.character_stats:
|
||||
by_player.character_stats.add_item(chest_item)
|
||||
|
||||
# Show pickup notification
|
||||
var items_texture = load(chest_item.spritePath) if chest_item.spritePath != "" else null
|
||||
var display_text = chest_item.item_name.to_upper()
|
||||
var item_color = Color.WHITE
|
||||
|
||||
# Determine color based on item type/rarity
|
||||
if chest_item.item_type == Item.ItemType.Restoration:
|
||||
item_color = Color.GREEN
|
||||
elif chest_item.item_type == Item.ItemType.Equippable:
|
||||
item_color = Color.CYAN # Cyan for equipment (matches loot pickup color)
|
||||
else:
|
||||
item_color = Color.WHITE
|
||||
|
||||
# Show notification with item sprite
|
||||
if items_texture:
|
||||
_show_item_pickup_notification(by_player, display_text, item_color, items_texture, chest_item.spriteFrames.x, chest_item.spriteFrames.y, chest_item.spriteFrame)
|
||||
else:
|
||||
# Fallback: just show text
|
||||
_show_item_pickup_notification(by_player, display_text, item_color, null, 0, 0, 0)
|
||||
|
||||
# Play chest open sound
|
||||
if has_node("SfxChestOpen"):
|
||||
$SfxChestOpen.play()
|
||||
|
||||
print(name, " opened by ", by_player.name, "! Item given: ", selected_loot.name)
|
||||
print(name, " opened by ", by_player.name, "! Item given: ", chest_item.item_name)
|
||||
|
||||
# Sync chest opening visual to all clients (item already given on server)
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
var player_peer_id = by_player.get_multiplayer_authority() if by_player else 0
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
# Reuse game_world from earlier in the function
|
||||
if game_world and game_world.has_method("_rpc_to_ready_peers"):
|
||||
var chest_name = name
|
||||
if has_meta("object_index"):
|
||||
chest_name = "InteractableObject_%d" % get_meta("object_index")
|
||||
game_world._rpc_to_ready_peers("_sync_chest_open_by_name", [chest_name, selected_loot.type if by_player else "coin", player_peer_id])
|
||||
# Sync chest open visual with item_data so clients can show the floating text
|
||||
var item_data = chest_item.save() if chest_item else {}
|
||||
game_world._rpc_to_ready_peers("_sync_chest_open_by_name", [chest_name, "item", player_peer_id, item_data])
|
||||
else:
|
||||
push_error("Chest: ERROR - No valid player to give item to!")
|
||||
|
||||
@@ -737,7 +787,7 @@ func _request_chest_open(player_peer_id: int):
|
||||
_open_chest(player)
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _sync_chest_open(loot_type_str: String = "coin", player_peer_id: int = 0):
|
||||
func _sync_chest_open(loot_type_str: String = "coin", player_peer_id: int = 0, item_data: Dictionary = {}):
|
||||
# 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:
|
||||
is_chest_opened = true
|
||||
@@ -757,26 +807,43 @@ func _sync_chest_open(loot_type_str: String = "coin", player_peer_id: int = 0):
|
||||
break
|
||||
|
||||
if player and is_instance_valid(player):
|
||||
# Show notification based on loot type (same as server)
|
||||
match loot_type_str:
|
||||
"coin":
|
||||
var coin_texture = load("res://assets/gfx/pickups/gold_coin.png")
|
||||
_show_item_pickup_notification(player, "+1 COIN", Color(1.0, 0.84, 0.0), coin_texture, 6, 1, 0)
|
||||
"apple":
|
||||
var heal_amount = 20.0
|
||||
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||
_show_item_pickup_notification(player, "+" + str(int(heal_amount)) + " HP", Color.GREEN, items_texture, 20, 14, (8 * 20) + 10)
|
||||
"banana":
|
||||
var heal_amount = 20.0
|
||||
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||
_show_item_pickup_notification(player, "+" + str(int(heal_amount)) + " HP", Color.YELLOW, items_texture, 20, 14, (8 * 20) + 11)
|
||||
"cherry":
|
||||
var heal_amount = 20.0
|
||||
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||
_show_item_pickup_notification(player, "+" + str(int(heal_amount)) + " HP", Color.RED, items_texture, 20, 14, (8 * 20) + 12)
|
||||
"key":
|
||||
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||
_show_item_pickup_notification(player, "+1 KEY", Color.YELLOW, items_texture, 20, 14, (13 * 20) + 10)
|
||||
# If item_data is provided, use it to show item notification
|
||||
if not item_data.is_empty():
|
||||
var chest_item = Item.new(item_data)
|
||||
var items_texture = load(chest_item.spritePath) if chest_item.spritePath != "" else null
|
||||
var display_text = chest_item.item_name.to_upper()
|
||||
var item_color = Color.WHITE
|
||||
|
||||
if chest_item.item_type == Item.ItemType.Restoration:
|
||||
item_color = Color.GREEN
|
||||
elif chest_item.item_type == Item.ItemType.Equippable:
|
||||
item_color = Color.CYAN # Cyan for equipment (matches loot pickup color)
|
||||
|
||||
if items_texture:
|
||||
_show_item_pickup_notification(player, display_text, item_color, items_texture, chest_item.spriteFrames.x, chest_item.spriteFrames.y, chest_item.spriteFrame)
|
||||
else:
|
||||
_show_item_pickup_notification(player, display_text, item_color, null, 0, 0, 0)
|
||||
else:
|
||||
# Fallback to old loot type system (for backwards compatibility)
|
||||
match loot_type_str:
|
||||
"coin":
|
||||
var coin_texture = load("res://assets/gfx/pickups/gold_coin.png")
|
||||
_show_item_pickup_notification(player, "+1 COIN", Color(1.0, 0.84, 0.0), coin_texture, 6, 1, 0)
|
||||
"apple":
|
||||
var heal_amount = 20.0
|
||||
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||
_show_item_pickup_notification(player, "+" + str(int(heal_amount)) + " HP", Color.GREEN, items_texture, 20, 14, (8 * 20) + 10)
|
||||
"banana":
|
||||
var heal_amount = 20.0
|
||||
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||
_show_item_pickup_notification(player, "+" + str(int(heal_amount)) + " HP", Color.YELLOW, items_texture, 20, 14, (8 * 20) + 11)
|
||||
"cherry":
|
||||
var heal_amount = 20.0
|
||||
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||
_show_item_pickup_notification(player, "+" + str(int(heal_amount)) + " HP", Color.RED, items_texture, 20, 14, (8 * 20) + 12)
|
||||
"key":
|
||||
var items_texture = load("res://assets/gfx/pickups/items_n_shit.png")
|
||||
_show_item_pickup_notification(player, "+1 KEY", Color.YELLOW, items_texture, 20, 14, (13 * 20) + 10)
|
||||
|
||||
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
|
||||
|
||||
@@ -29,6 +29,7 @@ var equipment_selection_index: int = 0 # Current equipment slot index (0-5: main
|
||||
@onready var selection_rectangle: Panel = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/SelectionRectangle
|
||||
@onready var info_panel: Control = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/InfoPanel
|
||||
@onready var info_label: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/InfoPanel/InfoLabel
|
||||
@onready var stats_label: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsLabel
|
||||
@onready var label_base_stats: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelBaseStats
|
||||
@onready var label_base_stats_value: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelBaseStatsValue
|
||||
@onready var label_derived_stats: Label = $InventoryContainer/MarginContainer/MarginContainer/VBoxContainer/HBox/StatsPanel/StatsHBox/LabelDerivedStats
|
||||
@@ -161,6 +162,11 @@ func _update_stats():
|
||||
|
||||
var char_stats = local_player.character_stats
|
||||
|
||||
# Update race/class in stats label
|
||||
if stats_label:
|
||||
var race_text = char_stats.race
|
||||
stats_label.text = "Stats - " + race_text
|
||||
|
||||
# Update base stats
|
||||
label_base_stats_value.text = str(char_stats.level) + "\n\n" + \
|
||||
str(int(char_stats.hp)) + "/" + str(int(char_stats.maxhp)) + "\n" + \
|
||||
|
||||
@@ -63,6 +63,7 @@ func save():
|
||||
"description": description,
|
||||
"spritePath": spritePath,
|
||||
"equipmentPath": equipmentPath,
|
||||
"colorReplacements": colorReplacements,
|
||||
"spriteFrame": spriteFrame,
|
||||
"modifiers": modifiers,
|
||||
"duration": duration,
|
||||
@@ -121,4 +122,6 @@ func load(iDic: Dictionary):
|
||||
can_have_multiple_of = iDic.get("can_have_multiple_of")
|
||||
if iDic.has("weight"):
|
||||
weight = iDic.get("weight")
|
||||
if iDic.has("colorReplacements"):
|
||||
colorReplacements = iDic.get("colorReplacements", [])
|
||||
pass
|
||||
|
||||
@@ -14,10 +14,21 @@ static func spawn_item_loot(item: Item, position: Vector2, entities_node: Node,
|
||||
push_error("ItemLootHelper: Could not load loot.tscn scene!")
|
||||
return null
|
||||
|
||||
# Generate random velocity for physics
|
||||
var random_angle = randf() * PI * 2
|
||||
var random_force = randf_range(50.0, 100.0)
|
||||
var random_velocity_z = randf_range(80.0, 120.0)
|
||||
# Generate deterministic random velocity for physics using dungeon seed
|
||||
# This ensures loot bounces the same on all clients
|
||||
var loot_rng = RandomNumberGenerator.new()
|
||||
var base_seed = 0
|
||||
if game_world and "dungeon_seed" in game_world:
|
||||
base_seed = game_world.dungeon_seed
|
||||
# Create unique seed for this loot item: dungeon_seed + position hash + counter
|
||||
# Use position hash to make seed unique per spawn location
|
||||
var pos_hash = hash(str(int(position.x)) + "_" + str(int(position.y)))
|
||||
var loot_seed = base_seed + pos_hash + 20000 # Offset to avoid collisions with enemy loot
|
||||
loot_rng.seed = loot_seed
|
||||
|
||||
var random_angle = loot_rng.randf() * PI * 2
|
||||
var random_force = loot_rng.randf_range(50.0, 100.0)
|
||||
var random_velocity_z = loot_rng.randf_range(80.0, 120.0)
|
||||
var initial_velocity = Vector2(cos(random_angle), sin(random_angle)) * random_force
|
||||
|
||||
# Find safe spawn position if game_world is provided
|
||||
|
||||
@@ -21,11 +21,22 @@ var is_airborne: bool = true
|
||||
var velocity_set_by_spawner: bool = false # Track if velocity was set externally
|
||||
|
||||
# Bounce physics
|
||||
var bounce_restitution: float = 0.6 # How much bounce energy is retained (0-1)
|
||||
var bounce_restitution: float = 0.6 # How much bounce energy is retained (0-1) - matches old code
|
||||
var min_bounce_velocity: float = 40.0 # Minimum velocity needed to bounce
|
||||
var friction: float = 25.0 # Friction when on ground (increased to dampen faster)
|
||||
var friction: float = 8.0 # Friction when on ground (lower = more gradual slowdown, matches old code)
|
||||
var bounce_timer: float = 0.0 # Prevent rapid bounce sounds
|
||||
|
||||
# Multiplayer sync and prediction
|
||||
var sync_timer: float = 0.0 # Timer for periodic position/velocity sync
|
||||
var sync_interval: float = 0.05 # Sync every 0.05 seconds (20 times per second) for smoother sync
|
||||
var last_sync_time: float = 0.0 # Track last server sync time for reconciliation
|
||||
var server_position: Vector2 = Vector2.ZERO # Last server-authoritative position
|
||||
var server_velocity: Vector2 = Vector2.ZERO # Last server-authoritative velocity
|
||||
var server_position_z: float = 0.0 # Last server-authoritative Z position
|
||||
var server_velocity_z: float = 0.0 # Last server-authoritative Z velocity
|
||||
var prediction_error_threshold: float = 10.0 # Distance threshold before correcting (pixels)
|
||||
var correction_smoothing: float = 0.3 # Lerp factor for smooth correction (0-1, lower = smoother)
|
||||
|
||||
# Loot properties
|
||||
var coin_value: int = 1
|
||||
var heal_amount: float = 20.0
|
||||
@@ -77,11 +88,13 @@ func _ready():
|
||||
|
||||
# Adjust bounce properties based on loot type
|
||||
if loot_type == LootType.COIN:
|
||||
bounce_restitution = 0.4 # Reduced from 0.6 to dampen more
|
||||
bounce_restitution = 0.6 # Matches old code - more bouncy
|
||||
min_bounce_velocity = 40.0
|
||||
friction = 8.0 # Lower friction for coins - more gradual slowdown
|
||||
else:
|
||||
bounce_restitution = 0.2 # Reduced from 0.3 to dampen more
|
||||
bounce_restitution = 0.3 # Lower bounce for food items
|
||||
min_bounce_velocity = 60.0
|
||||
friction = 12.0 # Slightly higher friction for food items
|
||||
|
||||
func _setup_sprite():
|
||||
if not sprite:
|
||||
@@ -171,100 +184,174 @@ func _physics_process(delta):
|
||||
if collected:
|
||||
return
|
||||
|
||||
# Update bounce timer
|
||||
if bounce_timer > 0.0:
|
||||
bounce_timer -= delta
|
||||
if bounce_timer < 0:
|
||||
bounce_timer = 0.0
|
||||
var is_client = multiplayer.has_multiplayer_peer() and not is_multiplayer_authority()
|
||||
var is_server = not multiplayer.has_multiplayer_peer() or is_multiplayer_authority()
|
||||
|
||||
# Update Z-axis physics
|
||||
if is_airborne:
|
||||
# Apply gravity to Z-axis
|
||||
acceleration_z = -300.0 # Gravity
|
||||
velocity_z += acceleration_z * delta
|
||||
# Server (authority): Run physics normally
|
||||
if is_server:
|
||||
# Update bounce timer
|
||||
if bounce_timer > 0.0:
|
||||
bounce_timer -= delta
|
||||
if bounce_timer < 0:
|
||||
bounce_timer = 0.0
|
||||
|
||||
# CRITICAL: Apply damping to velocity_z to lerp it towards 0 (prevents infinite bouncing)
|
||||
# Dampen more when velocity is small (closer to ground) but allow normal bounces first
|
||||
var damping_factor = 8.0 # How quickly velocity_z approaches 0 (allow more visible bounces)
|
||||
if abs(velocity_z) < 25.0: # More aggressive damping for very small velocities only
|
||||
damping_factor = 20.0
|
||||
velocity_z = lerpf(velocity_z, 0.0, 1.0 - exp(-damping_factor * delta))
|
||||
# Update Z-axis physics
|
||||
if is_airborne:
|
||||
# Apply gravity to Z-axis (matches old code)
|
||||
acceleration_z = -300.0 # Gravity
|
||||
velocity_z += acceleration_z * delta
|
||||
position_z += velocity_z * delta
|
||||
|
||||
position_z += velocity_z * delta
|
||||
# Ground collision and bounce (matches old code - simpler, no aggressive damping)
|
||||
if position_z <= 0.0:
|
||||
position_z = 0.0
|
||||
|
||||
# Apply air resistance to slow down horizontal movement while airborne
|
||||
velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-8.0 * delta))
|
||||
# Apply friction ONLY when on ground (matches old code behavior)
|
||||
velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-friction * delta))
|
||||
|
||||
# Ground collision and bounce (skip if collected to prevent bounce sounds)
|
||||
if position_z <= 0.0:
|
||||
position_z = 0.0
|
||||
# Check if we should bounce (simpler logic matching old code)
|
||||
if not collected and abs(velocity_z) > min_bounce_velocity:
|
||||
# Play bounce sound for coins (matches old code volume formula)
|
||||
if loot_type == LootType.COIN and bounce_timer == 0.0 and velocity_z < 0.0:
|
||||
if sfx_coin_bounce:
|
||||
# Old code formula: -1 + (-10 - (velocityZ * 0.1))
|
||||
# Adjusted for negative velocity_z
|
||||
sfx_coin_bounce.volume_db = -1.0 + (-10.0 - (abs(velocity_z) * 0.1))
|
||||
sfx_coin_bounce.play()
|
||||
bounce_timer = 0.08 # Matches old code timing
|
||||
|
||||
# Apply friction when on ground (dampen X/Y momentum faster)
|
||||
velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-friction * delta))
|
||||
|
||||
# Check if we should bounce (only if not collected and velocity is significant)
|
||||
# Allow bouncing but ensure it eventually stops
|
||||
if not collected and abs(velocity_z) > min_bounce_velocity:
|
||||
# Bounce on floor
|
||||
# Only play bounce sound if bounce is significant enough and timer has elapsed
|
||||
# CRITICAL: Only play sound if velocity is large enough and coin is actually falling (downward)
|
||||
if loot_type == LootType.COIN and bounce_timer == 0.0 and abs(velocity_z) > 50.0 and velocity_z < 0.0:
|
||||
# Play bounce sound for coins (only for significant downward velocities)
|
||||
if sfx_coin_bounce:
|
||||
# Adjust volume based on bounce velocity (softer for smaller bounces)
|
||||
var volume_multiplier = clamp(abs(velocity_z) / 100.0, 0.3, 1.0)
|
||||
sfx_coin_bounce.volume_db = -3.0 + (-12.0 * (1.0 - volume_multiplier))
|
||||
sfx_coin_bounce.play()
|
||||
bounce_timer = 0.12 # Prevent rapid bounce sounds but allow reasonable bounce rate
|
||||
|
||||
velocity_z = - velocity_z * bounce_restitution
|
||||
|
||||
# CRITICAL: Force stop bouncing if velocity is too small after bounce (prevent micro-bounces)
|
||||
# Use a lower threshold to allow a few more bounces before stopping
|
||||
if abs(velocity_z) < min_bounce_velocity * 0.5:
|
||||
# Simple bounce (matches old code)
|
||||
velocity_z = -velocity_z * bounce_restitution
|
||||
is_airborne = true # Still bouncing
|
||||
else:
|
||||
# Velocity too small or collected - stop bouncing
|
||||
velocity_z = 0.0
|
||||
is_airborne = false
|
||||
else:
|
||||
is_airborne = true # Still bouncing
|
||||
else:
|
||||
# Velocity too small or collected - stop bouncing
|
||||
velocity_z = 0.0
|
||||
is_airborne = false
|
||||
else:
|
||||
is_airborne = false
|
||||
# Ensure velocity_z is zero when on ground
|
||||
velocity_z = 0.0
|
||||
# Apply friction even when not airborne (on ground)
|
||||
velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-friction * delta))
|
||||
else:
|
||||
is_airborne = false
|
||||
# Ensure velocity_z is zero when on ground
|
||||
velocity_z = 0.0
|
||||
# Apply friction when on ground (matches old code)
|
||||
velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-friction * delta))
|
||||
|
||||
# Move and check for collisions
|
||||
move_and_slide()
|
||||
# Move and check for collisions
|
||||
move_and_slide()
|
||||
|
||||
# Check for wall collisions (skip if collected to prevent bounce sounds)
|
||||
if not collected:
|
||||
for i in get_slide_collision_count():
|
||||
var collision = get_slide_collision(i)
|
||||
if collision:
|
||||
var collider = collision.get_collider()
|
||||
# Only bounce off walls, not players (players are detected via PickupArea)
|
||||
if collider and not collider.is_in_group("player"):
|
||||
# Bounce off walls
|
||||
var normal = collision.get_normal()
|
||||
velocity = velocity.bounce(normal) * 0.5 # Reduce velocity more after bounce (was 0.8)
|
||||
# Check for wall collisions (skip if collected to prevent bounce sounds)
|
||||
# Matches old code behavior - simpler wall bounce without aggressive velocity reduction
|
||||
if not collected:
|
||||
for i in get_slide_collision_count():
|
||||
var collision = get_slide_collision(i)
|
||||
if collision:
|
||||
var collider = collision.get_collider()
|
||||
# Only bounce off walls, not players (players are detected via PickupArea)
|
||||
if collider and not collider.is_in_group("player"):
|
||||
# Check if velocity is too small before bouncing (prevent infinite micro-bounces)
|
||||
var velocity_magnitude = velocity.length()
|
||||
if velocity_magnitude < 15.0: # If velocity is very small, stop bouncing
|
||||
velocity = Vector2.ZERO
|
||||
continue # Skip bounce and sound
|
||||
|
||||
# Play bounce sound for coins hitting walls
|
||||
if loot_type == LootType.COIN and bounce_timer == 0.0:
|
||||
# Bounce off walls (matches old code - no aggressive velocity reduction)
|
||||
var normal = collision.get_normal()
|
||||
velocity = velocity.bounce(normal) # Old code didn't reduce velocity here
|
||||
|
||||
# Play bounce sound for coins hitting walls (matches old code)
|
||||
if loot_type == LootType.COIN and bounce_timer == 0.0:
|
||||
if sfx_coin_bounce:
|
||||
sfx_coin_bounce.volume_db = -5.0
|
||||
sfx_coin_bounce.play()
|
||||
bounce_timer = 0.08 # Matches old code timing
|
||||
|
||||
# Update visual position based on Z
|
||||
_update_visuals()
|
||||
|
||||
# Animate coin rotation (always animate, even when not airborne)
|
||||
if loot_type == LootType.COIN:
|
||||
_animate_coin(delta)
|
||||
|
||||
# Server: Periodically sync position/velocity to clients (sync more frequently when airborne)
|
||||
sync_timer += delta
|
||||
# Sync more frequently when airborne (bouncing), less when settled
|
||||
var current_interval = sync_interval if is_airborne else sync_interval * 2.0
|
||||
if sync_timer >= current_interval:
|
||||
sync_timer = 0.0
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if game_world and game_world.has_method("_sync_loot_physics"):
|
||||
var loot_id = get_meta("loot_id") if has_meta("loot_id") else -1
|
||||
if loot_id >= 0:
|
||||
game_world._rpc_to_ready_peers("_sync_loot_physics", [loot_id, global_position, velocity, position_z, velocity_z])
|
||||
|
||||
# Client (prediction): Run physics locally for smooth movement, then reconcile with server
|
||||
elif is_client:
|
||||
# Run physics locally (client-side prediction) - same logic as server
|
||||
# Update bounce timer
|
||||
if bounce_timer > 0.0:
|
||||
bounce_timer -= delta
|
||||
if bounce_timer < 0:
|
||||
bounce_timer = 0.0
|
||||
|
||||
# Update Z-axis physics
|
||||
if is_airborne:
|
||||
# Apply gravity to Z-axis (matches server)
|
||||
acceleration_z = -300.0
|
||||
velocity_z += acceleration_z * delta
|
||||
position_z += velocity_z * delta
|
||||
|
||||
# Ground collision and bounce (matches server logic)
|
||||
if position_z <= 0.0:
|
||||
position_z = 0.0
|
||||
velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-friction * delta))
|
||||
|
||||
if not collected and abs(velocity_z) > min_bounce_velocity:
|
||||
# Play bounce sound for coins
|
||||
if loot_type == LootType.COIN and bounce_timer == 0.0 and velocity_z < 0.0:
|
||||
if sfx_coin_bounce:
|
||||
sfx_coin_bounce.volume_db = -5.0
|
||||
sfx_coin_bounce.volume_db = -1.0 + (-10.0 - (abs(velocity_z) * 0.1))
|
||||
sfx_coin_bounce.play()
|
||||
bounce_timer = 0.08
|
||||
velocity_z = -velocity_z * bounce_restitution
|
||||
is_airborne = true
|
||||
else:
|
||||
velocity_z = 0.0
|
||||
is_airborne = false
|
||||
else:
|
||||
is_airborne = false
|
||||
velocity_z = 0.0
|
||||
velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-friction * delta))
|
||||
|
||||
# Update visual position based on Z
|
||||
_update_visuals()
|
||||
# Move and check for collisions
|
||||
move_and_slide()
|
||||
|
||||
# Animate coin rotation (always animate, even when not airborne)
|
||||
if loot_type == LootType.COIN:
|
||||
_animate_coin(delta)
|
||||
# Check for wall collisions
|
||||
if not collected:
|
||||
for i in get_slide_collision_count():
|
||||
var collision = get_slide_collision(i)
|
||||
if collision:
|
||||
var collider = collision.get_collider()
|
||||
if collider and not collider.is_in_group("player"):
|
||||
var velocity_magnitude = velocity.length()
|
||||
if velocity_magnitude < 15.0:
|
||||
velocity = Vector2.ZERO
|
||||
continue
|
||||
var normal = collision.get_normal()
|
||||
velocity = velocity.bounce(normal)
|
||||
if loot_type == LootType.COIN and bounce_timer == 0.0:
|
||||
if sfx_coin_bounce:
|
||||
sfx_coin_bounce.volume_db = -5.0
|
||||
sfx_coin_bounce.play()
|
||||
bounce_timer = 0.08
|
||||
|
||||
# Update visuals
|
||||
_update_visuals()
|
||||
|
||||
# Animate coin rotation
|
||||
if loot_type == LootType.COIN:
|
||||
_animate_coin(delta)
|
||||
|
||||
# Reconcile with server state if available (called from game_world._sync_loot_physics)
|
||||
# Server state is stored in server_position, server_velocity, etc. variables
|
||||
# Reconciliation happens in game_world._reconcile_loot_state()
|
||||
|
||||
func _update_z_physics(delta):
|
||||
position_z += velocity_z * delta
|
||||
|
||||
@@ -11,7 +11,17 @@ signal connection_succeeded(was_reconnecting: bool)
|
||||
signal webrtc_ready() # Emitted when WebRTC mesh is set up after Welcome message
|
||||
|
||||
const MATCHBOX_SERVER = "wss://matchbox.thefirstboss.com"
|
||||
const STUN_SERVER = "stun:ruinborn.thefirstboss.com:3578"
|
||||
# STUN/TURN server configuration
|
||||
# COTURN server on ports 3478/3479 with relay ports 49150:49500
|
||||
const STUN_SERVER = "stun:ruinborn.thefirstboss.com:3478"
|
||||
# TURN server configuration (Coturn with long-term credentials)
|
||||
# Using standard TURN port 3478 (UDP/TCP) and 3479 (TLS)
|
||||
const TURN_SERVER = "turn:ruinborn.thefirstboss.com:3478" # Standard TURN port
|
||||
# Note: Relay ports 49160:49500 are configured on the COTURN server side
|
||||
# Realm: ruinborn.thefirstboss.com
|
||||
# Credentials: myuser:mypassword (long-term credentials with lt-cred-mech)
|
||||
const TURN_USERNAME = "myuser" # TURN username (long-term credentials)
|
||||
const TURN_PASSWORD = "mypassword" # TURN password (long-term credentials)
|
||||
|
||||
var websocket: WebSocketPeer = null
|
||||
var webrtc_peer: WebRTCMultiplayerPeer = null
|
||||
@@ -27,13 +37,15 @@ var pending_offers: Dictionary = {} # peer_id -> offer data
|
||||
var waiting_for_peer_id: bool = false # Client flag: waiting for host to assign peer ID
|
||||
var queued_signaling_messages: Array = [] # Queue for signaling messages received before peer ID assignment
|
||||
var connection_failed_emitted: bool = false # Prevent multiple connection_failed emissions
|
||||
var ice_candidates_sent: Dictionary = {} # peer_id -> count of ICE candidates sent
|
||||
var ice_candidates_received: Dictionary = {} # peer_id -> count of ICE candidates received
|
||||
var retry_count: int = 0 # Number of retry attempts
|
||||
var max_retries: int = 3 # Maximum retry attempts (for initial connection)
|
||||
var retry_timer: float = 0.0 # Timer for retry backoff
|
||||
var retry_delay: float = 5.0 # Initial retry delay in seconds
|
||||
var is_retrying: bool = false # Whether we're currently retrying
|
||||
var host_reconnect_timer: float = 0.0 # Timer for host reconnection
|
||||
var host_reconnect_delay: float = 60.0 # Host reconnection delay: 1 minute
|
||||
var host_reconnect_delay: float = 5.0 # Host reconnection delay: 5 seconds (reduced from 60.0)
|
||||
var host_reconnect_count: int = 0 # Number of host reconnection attempts
|
||||
var max_host_retries: int = 10 # Maximum host reconnection attempts
|
||||
var is_host_reconnecting: bool = false # Whether host is in reconnection mode
|
||||
@@ -82,8 +94,6 @@ func _check_and_emit_peer_connected(peer_id: int):
|
||||
var connection_state = pc.get_connection_state()
|
||||
var signaling_state = pc.get_signaling_state()
|
||||
|
||||
log_print("MatchboxClient: Checking connection for peer " + str(peer_id) + " (connection: " + str(connection_state) + ", signaling: " + str(signaling_state) + ")")
|
||||
|
||||
# If signaling state is STABLE (0) or connection is CONNECTING/CONNECTED, emit peer_connected
|
||||
# This allows the joiner to proceed even if the connection isn't fully established yet
|
||||
# The connection will complete during gameplay
|
||||
@@ -97,11 +107,9 @@ func _check_and_emit_peer_connected(peer_id: int):
|
||||
# For joiners, check if multiplayer system sees the peer
|
||||
# If the connection is CONNECTED (2), the peer should be available soon
|
||||
if connection_state == 2: # CONNECTED
|
||||
log_print("MatchboxClient: Connection is CONNECTED, checking if peer is available for RPCs")
|
||||
call_deferred("_check_multiplayer_peer_available", peer_id)
|
||||
else:
|
||||
# Connection not ready yet, try again after a short delay
|
||||
log_print("MatchboxClient: Connection not ready for peer " + str(peer_id) + " - will retry")
|
||||
# Connection not ready yet, try again after a short delay (silently retry)
|
||||
get_tree().create_timer(0.5).timeout.connect(func(): _check_and_emit_peer_connected(peer_id))
|
||||
|
||||
func _check_multiplayer_peer_available(peer_id: int):
|
||||
@@ -115,12 +123,11 @@ func _check_multiplayer_peer_available(peer_id: int):
|
||||
# Check if the peer is in the multiplayer peer list
|
||||
var peers = multiplayer.get_peers()
|
||||
if peer_id in peers:
|
||||
log_print("MatchboxClient: Multiplayer system recognizes peer " + str(peer_id))
|
||||
# The multiplayer.peer_connected signal should fire automatically
|
||||
# But if it doesn't, we might need to manually trigger it
|
||||
# For now, just log - NetworkManager will handle connection_succeeded
|
||||
# For now, just proceed - NetworkManager will handle connection_succeeded
|
||||
pass
|
||||
else:
|
||||
log_print("MatchboxClient: Multiplayer system doesn't recognize peer " + str(peer_id) + " yet - will retry")
|
||||
# Retry after a short delay (max 5 seconds)
|
||||
var peer_check_retry_count = get_meta("peer_check_retry_count", 0)
|
||||
if peer_check_retry_count < 10: # Max 10 retries (5 seconds)
|
||||
@@ -265,7 +272,7 @@ func _process(_delta):
|
||||
log_print("MatchboxClient: Connection closing...")
|
||||
|
||||
func _handle_message(message: String):
|
||||
log_print("MatchboxClient: Received message: " + message)
|
||||
# Only log message content if there's an error - reduce console spam
|
||||
var json = JSON.new()
|
||||
var error = json.parse(message)
|
||||
if error != OK:
|
||||
@@ -273,7 +280,6 @@ func _handle_message(message: String):
|
||||
return
|
||||
|
||||
var data = json.data
|
||||
log_print("MatchboxClient: Parsed data: " + str(data))
|
||||
|
||||
# Matchbox protocol uses direct keys: IdAssigned, NewPeer, PeerLeft, Signal
|
||||
if data.has("IdAssigned"):
|
||||
@@ -622,20 +628,63 @@ func _create_peer_connection(peer_id: int) -> WebRTCPeerConnection:
|
||||
|
||||
var pc = WebRTCPeerConnection.new()
|
||||
|
||||
# Configure STUN server
|
||||
# Configure STUN and TURN servers according to Godot documentation format
|
||||
# Each server should be a separate entry in the iceServers array
|
||||
var ice_servers = []
|
||||
|
||||
# Add STUN server as separate entry
|
||||
if not STUN_SERVER.is_empty():
|
||||
var stun_config = {
|
||||
"urls": [STUN_SERVER] # urls must be an array
|
||||
}
|
||||
ice_servers.append(stun_config)
|
||||
|
||||
# Add TURN server as separate entry with credentials
|
||||
if not TURN_SERVER.is_empty():
|
||||
# For TURN, create URLs array with both UDP and TCP transports
|
||||
var turn_urls = []
|
||||
if TURN_SERVER.begins_with("turns:"):
|
||||
# TURNS over TLS: primarily TCP
|
||||
turn_urls.append(TURN_SERVER + "?transport=tcp")
|
||||
else:
|
||||
# Standard TURN: try both UDP and TCP
|
||||
turn_urls.append(TURN_SERVER + "?transport=udp")
|
||||
turn_urls.append(TURN_SERVER + "?transport=tcp")
|
||||
|
||||
# Create TURN server configuration
|
||||
var turn_config = {
|
||||
"urls": turn_urls # urls must be an array
|
||||
}
|
||||
|
||||
# Add credentials if configured (for long-term credentials)
|
||||
if not TURN_USERNAME.is_empty() and not TURN_PASSWORD.is_empty():
|
||||
turn_config["username"] = TURN_USERNAME
|
||||
turn_config["credential"] = TURN_PASSWORD
|
||||
|
||||
ice_servers.append(turn_config)
|
||||
|
||||
var config = {
|
||||
"iceServers": [
|
||||
{
|
||||
"urls": [STUN_SERVER]
|
||||
}
|
||||
]
|
||||
"iceServers": ice_servers
|
||||
}
|
||||
|
||||
# Log ICE server configuration
|
||||
log_print("MatchboxClient: Configuring ICE servers for peer " + str(peer_id) + ":")
|
||||
for i in range(ice_servers.size()):
|
||||
var server = ice_servers[i]
|
||||
var urls = server.get("urls", [])
|
||||
var username = server.get("username", "")
|
||||
var credential = server.get("credential", "")
|
||||
var has_credential = not credential.is_empty()
|
||||
log_print(" ICE Server " + str(i) + ": " + str(urls) + (", username: " + username if not username.is_empty() else "") + (", has credential: " + str(has_credential)))
|
||||
|
||||
var error = pc.initialize(config)
|
||||
if error != OK:
|
||||
log_error("MatchboxClient: Failed to initialize peer connection: " + str(error))
|
||||
return null
|
||||
|
||||
# Log the actual config that was used (for debugging)
|
||||
log_print("MatchboxClient: Peer connection initialized for peer " + str(peer_id) + " with config: " + str(config))
|
||||
|
||||
# Connect signals
|
||||
# Note: session_description_created signal has signature (type: String, sdp: String)
|
||||
# We need to wrap it in a lambda to pass peer_id
|
||||
@@ -649,21 +698,12 @@ func _create_peer_connection(peer_id: int) -> WebRTCPeerConnection:
|
||||
log_error("MatchboxClient: Failed to connect ice_candidate_created signal: " + str(ice_connected))
|
||||
log_print("MatchboxClient: Signals connected for peer " + str(peer_id) + " (session: " + str(signal_connected) + ", ice: " + str(ice_connected) + ")")
|
||||
|
||||
# Create a data channel for multiplayer communication
|
||||
# This is required before creating an offer
|
||||
var data_channel = pc.create_data_channel("game", {
|
||||
"ordered": true
|
||||
})
|
||||
if not data_channel:
|
||||
log_error("MatchboxClient: Failed to create data channel for peer " + str(peer_id))
|
||||
return null
|
||||
|
||||
log_print("MatchboxClient: Created data channel for peer " + str(peer_id) + " (channel: " + str(data_channel) + ")")
|
||||
|
||||
peer_connections[peer_id] = pc
|
||||
|
||||
# IMPORTANT: Add peer connection to WebRTC mesh BEFORE starting offer/answer exchange
|
||||
# The peer connection must be in STATE_NEW when added to the mesh
|
||||
# Note: Do NOT create data channels manually - WebRTCMultiplayerPeer.add_peer()
|
||||
# automatically creates the required channels (reliable, unreliable, ordered)
|
||||
add_peer_to_mesh(peer_id)
|
||||
|
||||
# In a full mesh, the peer with the lower ID creates the offer to the peer with the higher ID
|
||||
@@ -697,6 +737,10 @@ func _create_offer_for_peer(peer_id: int):
|
||||
pc.poll()
|
||||
log_print("MatchboxClient: Peer connection state after create_offer: " + str(pc.get_connection_state()))
|
||||
|
||||
# Check ICE gathering state (if available)
|
||||
# Note: Godot's WebRTC might not expose ICE gathering state directly
|
||||
log_print("MatchboxClient: Waiting for ICE candidates to be generated...")
|
||||
|
||||
# Signal should be connected from _create_peer_connection
|
||||
log_print("MatchboxClient: Signal is connected, waiting for callback...")
|
||||
# Force another poll to ensure the signal fires
|
||||
@@ -795,12 +839,52 @@ func _handle_ice_candidate(peer_id: int, signal_data: Dictionary):
|
||||
if candidate.is_empty():
|
||||
return
|
||||
|
||||
# Log ICE candidate type to help debug STUN/TURN connectivity
|
||||
var candidate_type = "unknown"
|
||||
if candidate.contains("typ host"):
|
||||
candidate_type = "host (local)"
|
||||
elif candidate.contains("typ srflx"):
|
||||
candidate_type = "srflx (STUN)"
|
||||
elif candidate.contains("typ relay"):
|
||||
candidate_type = "relay (TURN)"
|
||||
elif candidate.contains("typ prflx"):
|
||||
candidate_type = "prflx (peer-reflexive)"
|
||||
|
||||
# Log non-host candidates to see if STUN/TURN is working
|
||||
if candidate_type != "host (local)":
|
||||
log_print("MatchboxClient: Received ICE candidate for peer " + str(peer_id) + " - Type: " + candidate_type)
|
||||
|
||||
# Track ICE candidates received
|
||||
if not ice_candidates_received.has(peer_id):
|
||||
ice_candidates_received[peer_id] = 0
|
||||
ice_candidates_received[peer_id] += 1
|
||||
|
||||
var error = pc.add_ice_candidate(sdp_mid, sdp_mline_index, candidate)
|
||||
if error != OK:
|
||||
log_error("MatchboxClient: Failed to add ICE candidate: " + str(error))
|
||||
return
|
||||
|
||||
func _on_session_description_created(peer_id: int, type: String, sdp: String):
|
||||
# According to Godot documentation, we MUST call set_local_description()
|
||||
# after session_description_created, otherwise ice_candidate_created won't be emitted!
|
||||
var pc = peer_connections.get(peer_id)
|
||||
if not pc:
|
||||
log_error("MatchboxClient: No peer connection found for peer " + str(peer_id) + " when setting local description")
|
||||
return
|
||||
|
||||
# Set the local description - this is REQUIRED for ICE candidates to be generated
|
||||
var set_error = pc.set_local_description(type, sdp)
|
||||
if set_error != OK:
|
||||
log_error("MatchboxClient: Failed to set local description for peer " + str(peer_id) + ": " + str(set_error))
|
||||
return
|
||||
|
||||
log_print("MatchboxClient: Set local " + type + " for peer " + str(peer_id) + " - ICE candidates should now be generated")
|
||||
|
||||
# Only log SDP summary, not full details - reduces console spam
|
||||
# Check for ICE candidates in SDP
|
||||
var candidate_count = sdp.split("a=candidate:").size() - 1
|
||||
log_print("MatchboxClient: Created " + type + " for peer " + str(peer_id) + " with " + str(candidate_count) + " ICE candidate(s) in SDP")
|
||||
|
||||
# Matchbox protocol uses "From" with UUID, not "id" with peer ID
|
||||
# Find the UUID for this peer_id
|
||||
var target_uuid = ""
|
||||
@@ -826,10 +910,8 @@ func _on_session_description_created(peer_id: int, type: String, sdp: String):
|
||||
# If we're a joiner (not hosting) and we just created an answer,
|
||||
# emit peer_connected after sending it (deferred to ensure answer is sent first)
|
||||
if not is_hosting and type == "answer":
|
||||
log_print("MatchboxClient: Joiner created answer for peer " + str(peer_id) + " - will emit peer_connected after sending")
|
||||
# Use call_deferred to ensure the answer is sent first, then check connection state
|
||||
call_deferred("_check_and_emit_peer_connected", peer_id)
|
||||
log_print("MatchboxClient: Sending Signal message: type=" + type + " from=" + my_uuid + " to=" + target_uuid + " sdp length=" + str(sdp.length()))
|
||||
_send_message(message)
|
||||
|
||||
# If we're a joiner and just sent an answer, check connection and emit peer_connected
|
||||
@@ -844,7 +926,150 @@ func _on_session_description_created(peer_id: int, type: String, sdp: String):
|
||||
# Also check immediately - the connection might be ready
|
||||
call_deferred("_check_and_emit_peer_connected", peer_id)
|
||||
|
||||
# Connection status methods for HUD display
|
||||
func is_matchbox_connected() -> bool:
|
||||
"""Check if Matchbox WebSocket is connected"""
|
||||
return is_network_connected and websocket != null and websocket.get_ready_state() == WebSocketPeer.STATE_OPEN
|
||||
|
||||
func get_ice_connection_status() -> Dictionary:
|
||||
"""Get ICE connection status for all peers
|
||||
Returns: Dictionary with peer_id -> {"connected": bool, "connection_state": int, "signaling_state": int, "candidates_sent": int, "candidates_received": int}
|
||||
"""
|
||||
var status = {}
|
||||
for peer_id in peer_connections.keys():
|
||||
var pc = peer_connections[peer_id]
|
||||
if pc:
|
||||
var connection_state = pc.get_connection_state() if pc.has_method("get_connection_state") else -1
|
||||
var signaling_state = pc.get_signaling_state() if pc.has_method("get_signaling_state") else -1
|
||||
# Connection state: 0=NEW, 1=CONNECTING, 2=CONNECTED, 3=DISCONNECTED, 4=FAILED
|
||||
status[peer_id] = {
|
||||
"connected": connection_state == 2, # CONNECTED
|
||||
"connection_state": connection_state,
|
||||
"signaling_state": signaling_state,
|
||||
"candidates_sent": ice_candidates_sent.get(peer_id, 0),
|
||||
"candidates_received": ice_candidates_received.get(peer_id, 0)
|
||||
}
|
||||
return status
|
||||
|
||||
func are_data_channels_connected() -> bool:
|
||||
"""Check if data channels are connected for at least one peer
|
||||
Returns true if any peer has data channels open (connected flag in WebRTCMultiplayerPeer)
|
||||
Also checks peer_connections directly and actual channel ready states (following Godot demo pattern)
|
||||
"""
|
||||
if not webrtc_peer:
|
||||
return false
|
||||
|
||||
# First, check if we have any peers with data channels open via multiplayer.get_peers()
|
||||
if multiplayer and multiplayer.has_multiplayer_peer():
|
||||
var peers = multiplayer.get_peers()
|
||||
if not peers.is_empty():
|
||||
# Check if at least one peer has connected data channels
|
||||
for peer_id in peers:
|
||||
if webrtc_peer.has_peer(peer_id):
|
||||
var peer_info = webrtc_peer.get_peer(peer_id)
|
||||
if peer_info:
|
||||
# Check connected flag first (fast path)
|
||||
if peer_info.get("connected", false):
|
||||
# Also verify channels are actually open (following demo pattern)
|
||||
var channels = peer_info.get("channels", [])
|
||||
if channels is Array and channels.size() > 0:
|
||||
var all_channels_open = true
|
||||
for channel in channels:
|
||||
if channel != null:
|
||||
# Check if channel has ready_state method
|
||||
if channel.has_method("get_ready_state"):
|
||||
var ready_state = channel.get_ready_state()
|
||||
# WebRTCDataChannel.State: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
|
||||
if ready_state != 1: # Not OPEN
|
||||
all_channels_open = false
|
||||
break
|
||||
elif "ready_state" in channel:
|
||||
var ready_state = channel.get("ready_state")
|
||||
if ready_state != 1: # Not OPEN
|
||||
all_channels_open = false
|
||||
break
|
||||
if all_channels_open:
|
||||
return true
|
||||
else:
|
||||
# No channels array, but connected flag is true - assume connected
|
||||
return true
|
||||
|
||||
# Fallback: Check peer_connections directly (useful for joiner where host might not be in get_peers() yet)
|
||||
# If we have peer connections with CONNECTED state, check data channels
|
||||
for peer_id in peer_connections.keys():
|
||||
var pc = peer_connections[peer_id]
|
||||
if pc and pc.has_method("get_connection_state"):
|
||||
var connection_state = pc.get_connection_state()
|
||||
var signaling_state = pc.get_signaling_state() if pc.has_method("get_signaling_state") else -1
|
||||
# Connection state: 0=NEW, 1=CONNECTING, 2=CONNECTED, 3=DISCONNECTED, 4=FAILED
|
||||
# Signaling state: 0=STABLE, 1=HAVE_LOCAL_OFFER, 2=HAVE_REMOTE_OFFER, etc.
|
||||
# If connection is CONNECTED (2) OR signaling is STABLE (0) with connection >= CONNECTING (1),
|
||||
# data channels should be working (following same logic as ICE status check)
|
||||
if connection_state == 2: # CONNECTED
|
||||
# Verify via webrtc_peer if available
|
||||
if webrtc_peer.has_peer(peer_id):
|
||||
var peer_info = webrtc_peer.get_peer(peer_id)
|
||||
if peer_info:
|
||||
# Check connected flag
|
||||
if peer_info.get("connected", false):
|
||||
# Also verify channels are actually open
|
||||
var channels = peer_info.get("channels", [])
|
||||
if channels is Array and channels.size() > 0:
|
||||
var all_channels_open = true
|
||||
for channel in channels:
|
||||
if channel != null:
|
||||
if channel.has_method("get_ready_state"):
|
||||
var ready_state = channel.get_ready_state()
|
||||
if ready_state != 1: # Not OPEN
|
||||
all_channels_open = false
|
||||
break
|
||||
elif "ready_state" in channel:
|
||||
var ready_state = channel.get("ready_state")
|
||||
if ready_state != 1: # Not OPEN
|
||||
all_channels_open = false
|
||||
break
|
||||
if all_channels_open:
|
||||
return true
|
||||
else:
|
||||
# Connection state is CONNECTED and connected flag is true
|
||||
# Data channels should be working even if channels array isn't available
|
||||
return true
|
||||
# Connection state is CONNECTED - even if peer not in webrtc_peer yet or connected flag not set,
|
||||
# if the WebRTC connection state is CONNECTED, data channels should be working
|
||||
# This is especially important for joiner where timing might be different
|
||||
return true
|
||||
elif signaling_state == 0 and connection_state >= 1: # STABLE signaling and at least CONNECTING
|
||||
# Signaling is STABLE (offer/answer exchange complete) and connection is at least CONNECTING
|
||||
# This means data channels should be working (same logic as ICE status)
|
||||
# This helps catch cases where connection_state hasn't reached CONNECTED yet but channels are open
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
func _on_ice_candidate_created(peer_id: int, media: String, index: int, candidate_name: String):
|
||||
# Log all ICE candidates to help debug connection issues
|
||||
var candidate_type = "unknown"
|
||||
if candidate_name.contains("typ host"):
|
||||
candidate_type = "host (local)"
|
||||
elif candidate_name.contains("typ srflx"):
|
||||
candidate_type = "srflx (STUN)"
|
||||
elif candidate_name.contains("typ relay"):
|
||||
candidate_type = "relay (TURN)"
|
||||
elif candidate_name.contains("typ prflx"):
|
||||
candidate_type = "prflx (peer-reflexive)"
|
||||
|
||||
# Log all candidates (including host) to debug connection issues
|
||||
log_print("MatchboxClient: ICE candidate created for peer " + str(peer_id) + " - Type: " + candidate_type + ", Media: " + media)
|
||||
# Log full candidate string for TURN debugging (first 200 chars to avoid spam)
|
||||
if candidate_type == "relay (TURN)":
|
||||
var candidate_preview = candidate_name.substr(0, 200) if candidate_name.length() > 200 else candidate_name
|
||||
log_print("MatchboxClient: TURN candidate details: " + candidate_preview)
|
||||
|
||||
# Track ICE candidates sent
|
||||
if not ice_candidates_sent.has(peer_id):
|
||||
ice_candidates_sent[peer_id] = 0
|
||||
ice_candidates_sent[peer_id] += 1
|
||||
|
||||
# Matchbox protocol uses "From" with UUID, not "id" with peer ID
|
||||
# Find the UUID for this peer_id
|
||||
var target_uuid = ""
|
||||
@@ -963,14 +1188,20 @@ func _poll_and_check_offer(peer_id: int):
|
||||
if not pc:
|
||||
return
|
||||
|
||||
# Poll multiple times to ensure async operations complete
|
||||
log_print("MatchboxClient: Performing additional polls for peer " + str(peer_id) + " offer creation")
|
||||
for i in range(10):
|
||||
# Poll multiple times to ensure async operations complete and ICE candidates are gathered
|
||||
log_print("MatchboxClient: Performing additional polls for peer " + str(peer_id) + " offer creation (waiting for ICE candidates)")
|
||||
var candidate_count_before = ice_candidates_sent.get(peer_id, 0)
|
||||
for i in range(20): # Increased from 10 to 20 to give more time for ICE gathering
|
||||
pc.poll()
|
||||
# Small delay between polls
|
||||
await get_tree().create_timer(0.01).timeout
|
||||
await get_tree().create_timer(0.05).timeout # Increased from 0.01 to 0.05
|
||||
var candidate_count_after = ice_candidates_sent.get(peer_id, 0)
|
||||
if candidate_count_after > candidate_count_before:
|
||||
log_print("MatchboxClient: ICE candidates detected during polling (count: " + str(candidate_count_after) + ")")
|
||||
candidate_count_before = candidate_count_after
|
||||
|
||||
log_print("MatchboxClient: Finished additional polling for peer " + str(peer_id))
|
||||
var final_candidate_count = ice_candidates_sent.get(peer_id, 0)
|
||||
log_print("MatchboxClient: Finished additional polling for peer " + str(peer_id) + " (total ICE candidates sent: " + str(final_candidate_count) + ")")
|
||||
|
||||
func _handle_connection_failure():
|
||||
"""Handle connection failure and decide whether to retry"""
|
||||
@@ -1018,6 +1249,16 @@ func _attempt_host_reconnect():
|
||||
|
||||
host_reconnect_count += 1
|
||||
log_print("MatchboxClient: Host retrying connection to room: " + room_name + " (attempt " + str(host_reconnect_count) + "/" + str(max_host_retries) + ")")
|
||||
|
||||
# Show chat message for host reconnection attempt
|
||||
var network_manager = get_parent()
|
||||
if network_manager:
|
||||
var game_world = network_manager.get_tree().get_first_node_in_group("game_world")
|
||||
if game_world:
|
||||
var chat_ui = game_world.get_node_or_null("ChatUI")
|
||||
if chat_ui and chat_ui.has_method("add_local_message"):
|
||||
chat_ui.add_local_message("System", "Host reconnecting... (attempt " + str(host_reconnect_count) + "/" + str(max_host_retries) + ")")
|
||||
|
||||
connection_failed_emitted = false
|
||||
|
||||
# Create new WebSocket connection
|
||||
@@ -1027,7 +1268,17 @@ func _attempt_host_reconnect():
|
||||
|
||||
if error != OK:
|
||||
log_error("MatchboxClient: Host failed to reconnect: " + str(error))
|
||||
# Will retry again in 1 minute (if under max retries)
|
||||
# Show chat message for failed attempt
|
||||
if network_manager:
|
||||
var game_world = network_manager.get_tree().get_first_node_in_group("game_world")
|
||||
if game_world:
|
||||
var chat_ui = game_world.get_node_or_null("ChatUI")
|
||||
if chat_ui and chat_ui.has_method("add_local_message"):
|
||||
if host_reconnect_count < max_host_retries:
|
||||
chat_ui.add_local_message("System", "Host reconnection failed, retrying in " + str(int(host_reconnect_delay)) + " seconds...")
|
||||
else:
|
||||
chat_ui.add_local_message("System", "Host reconnection failed, max retries reached")
|
||||
# Will retry again (if under max retries)
|
||||
if host_reconnect_count >= max_host_retries:
|
||||
is_host_reconnecting = false
|
||||
return
|
||||
@@ -1068,6 +1319,7 @@ func add_peer_to_mesh(peer_id: int):
|
||||
return
|
||||
|
||||
var error = webrtc_peer.add_peer(pc, peer_id)
|
||||
|
||||
if error != OK:
|
||||
log_error("MatchboxClient: Failed to add peer to mesh: " + str(error))
|
||||
return
|
||||
|
||||
@@ -15,7 +15,10 @@ signal rooms_fetched(rooms: Array) # Forwarded from room_registry
|
||||
const DEFAULT_PORT = 21212
|
||||
const MAX_PLAYERS = 8
|
||||
const MATCHBOX_SERVER = "wss://matchbox.thefirstboss.com"
|
||||
const STUN_SERVER = "stun:ruinborn.thefirstboss.com:3578"
|
||||
const STUN_SERVER = "stun:ruinborn.thefirstboss.com:3478"
|
||||
const TURN_SERVER = "turn:ruinborn.thefirstboss.com:3478"
|
||||
const TURN_USERNAME = "myuser"
|
||||
const TURN_PASSWORD = "mypassword"
|
||||
|
||||
var players_info = {} # Dictionary of peer_id -> {local_player_count: int, player_names: []}
|
||||
var local_player_count = 1 # How many local players on this machine
|
||||
@@ -32,7 +35,7 @@ var reconnection_room_id: String = "" # Store room_id for reconnection
|
||||
var reconnection_level: int = 0 # Store level for reconnection
|
||||
var reconnection_attempting: bool = false # Track if we're attempting to reconnect
|
||||
var reconnection_timer: float = 0.0 # Timer for reconnection delay
|
||||
const RECONNECTION_DELAY: float = 2.0 # Delay before attempting reconnection
|
||||
const RECONNECTION_DELAY: float = 1.0 # Delay before attempting reconnection (reduced from 2.0)
|
||||
|
||||
# Logging - use LogManager for categorized logging
|
||||
func log_print(message: String):
|
||||
@@ -651,8 +654,59 @@ func _on_matchbox_peer_connected(peer_id: int):
|
||||
log_print("NetworkManager: Joiner - already in game scene, connection_succeeded was already emitted")
|
||||
)
|
||||
|
||||
# Don't emit player_connected here - wait for multiplayer.peer_connected signal
|
||||
# which fires when the peer is actually available for RPCs
|
||||
# On web, multiplayer.peer_connected might not fire for either host or joiner
|
||||
# So we need to manually emit player_connected as fallback
|
||||
# For host: emit player_connected for client peers (peer_id > 1)
|
||||
# For joiner: emit player_connected for host (peer_id == 1)
|
||||
|
||||
# Host fallback: emit player_connected for client connections
|
||||
if is_hosting and peer_id > 1:
|
||||
var host_fallback_key = "player_connected_fallback_host_" + str(peer_id)
|
||||
if has_meta(host_fallback_key):
|
||||
return
|
||||
|
||||
set_meta(host_fallback_key, true)
|
||||
|
||||
# Wait a bit to see if multiplayer.peer_connected fires first
|
||||
get_tree().create_timer(1.0).timeout.connect(func():
|
||||
if not is_inside_tree():
|
||||
return
|
||||
|
||||
# Check if player_connected was already emitted (check if multiplayer peer is recognized)
|
||||
if players_info.has(peer_id):
|
||||
log_print("NetworkManager: Emitting player_connected as fallback for peer " + str(peer_id) + " (host, Matchbox connection established, multiplayer.peer_connected didn't fire)")
|
||||
player_connected.emit(peer_id, players_info[peer_id])
|
||||
remove_meta(host_fallback_key)
|
||||
)
|
||||
|
||||
# Joiner fallback: emit player_connected for host connection
|
||||
if not is_hosting and peer_id == 1:
|
||||
# Register player info if not already registered
|
||||
if not players_info.has(peer_id):
|
||||
players_info[peer_id] = {
|
||||
"local_player_count": 1, # Default, will be updated via RPC
|
||||
"player_names": _generate_player_names(1, peer_id)
|
||||
}
|
||||
|
||||
var fallback_key = "player_connected_fallback_" + str(peer_id)
|
||||
if has_meta(fallback_key):
|
||||
# Already scheduled fallback emission
|
||||
return
|
||||
|
||||
set_meta(fallback_key, true)
|
||||
|
||||
# Wait a bit to see if multiplayer.peer_connected fires first
|
||||
get_tree().create_timer(1.0).timeout.connect(func():
|
||||
if not is_inside_tree():
|
||||
return
|
||||
|
||||
# Check if player_connected was already emitted by checking if multiplayer peer is recognized
|
||||
# On web, we can't reliably check this, so we just emit anyway (idempotency handled in GameWorld)
|
||||
if players_info.has(peer_id):
|
||||
log_print("NetworkManager: Emitting player_connected as fallback for peer " + str(peer_id) + " (Matchbox connection established, multiplayer.peer_connected didn't fire)")
|
||||
player_connected.emit(peer_id, players_info[peer_id])
|
||||
remove_meta(fallback_key)
|
||||
)
|
||||
|
||||
func _emit_connection_succeeded_safe():
|
||||
"""Safely emit connection_succeeded signal - checks if node is still valid"""
|
||||
@@ -693,14 +747,27 @@ func _attempt_reconnect():
|
||||
|
||||
log_print("NetworkManager: Attempting to reconnect to room: " + reconnection_room_id)
|
||||
|
||||
# Show chat message for reconnection attempt
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if game_world:
|
||||
var chat_ui = game_world.get_node_or_null("ChatUI")
|
||||
if chat_ui and chat_ui.has_method("add_local_message"):
|
||||
chat_ui.add_local_message("System", "Attempting to reconnect...")
|
||||
|
||||
# Attempt to reconnect using the stored room_id
|
||||
var success = join_game(reconnection_room_id)
|
||||
if not success:
|
||||
log_error("NetworkManager: Reconnection attempt failed, will retry")
|
||||
log_error("NetworkManager: Reconnection attempt failed, will retry in " + str(RECONNECTION_DELAY) + " seconds")
|
||||
# Retry after delay
|
||||
reconnection_attempting = true
|
||||
reconnection_timer = RECONNECTION_DELAY
|
||||
|
||||
# Show chat message for failed attempt
|
||||
if game_world:
|
||||
var chat_ui = game_world.get_node_or_null("ChatUI")
|
||||
if chat_ui and chat_ui.has_method("add_local_message"):
|
||||
chat_ui.add_local_message("System", "Reconnection failed, retrying in " + str(int(RECONNECTION_DELAY)) + " seconds...")
|
||||
|
||||
func fetch_available_rooms() -> bool:
|
||||
"""Fetch available rooms from the registry"""
|
||||
if room_registry:
|
||||
@@ -736,17 +803,47 @@ func get_webrtc_peer() -> WebRTCMultiplayerPeer:
|
||||
return multiplayer.multiplayer_peer as WebRTCMultiplayerPeer
|
||||
return null
|
||||
|
||||
# Create a WebRTC peer connection with STUN server configured
|
||||
# Create a WebRTC peer connection with STUN and TURN servers configured
|
||||
func create_peer_connection() -> WebRTCPeerConnection:
|
||||
var peer_connection = WebRTCPeerConnection.new()
|
||||
|
||||
# Configure STUN server for NAT traversal
|
||||
# Configure STUN and TURN servers according to Godot documentation format
|
||||
# Each server should be a separate entry in the iceServers array
|
||||
var ice_servers = []
|
||||
|
||||
# Add STUN server as separate entry
|
||||
if not STUN_SERVER.is_empty():
|
||||
var stun_config = {
|
||||
"urls": [STUN_SERVER] # urls must be an array
|
||||
}
|
||||
ice_servers.append(stun_config)
|
||||
|
||||
# Add TURN server as separate entry with credentials
|
||||
if not TURN_SERVER.is_empty():
|
||||
# For TURN, create URLs array with both UDP and TCP transports
|
||||
var turn_urls = []
|
||||
if TURN_SERVER.begins_with("turns:"):
|
||||
# TURNS over TLS: primarily TCP
|
||||
turn_urls.append(TURN_SERVER + "?transport=tcp")
|
||||
else:
|
||||
# Standard TURN: try both UDP and TCP
|
||||
turn_urls.append(TURN_SERVER + "?transport=udp")
|
||||
turn_urls.append(TURN_SERVER + "?transport=tcp")
|
||||
|
||||
# Create TURN server configuration
|
||||
var turn_config = {
|
||||
"urls": turn_urls # urls must be an array
|
||||
}
|
||||
|
||||
# Add credentials if configured (for long-term credentials)
|
||||
if not TURN_USERNAME.is_empty() and not TURN_PASSWORD.is_empty():
|
||||
turn_config["username"] = TURN_USERNAME
|
||||
turn_config["credential"] = TURN_PASSWORD
|
||||
|
||||
ice_servers.append(turn_config)
|
||||
|
||||
var config = {
|
||||
"iceServers": [
|
||||
{
|
||||
"urls": [STUN_SERVER]
|
||||
}
|
||||
]
|
||||
"iceServers": ice_servers
|
||||
}
|
||||
|
||||
var error = peer_connection.initialize(config)
|
||||
@@ -754,7 +851,7 @@ func create_peer_connection() -> WebRTCPeerConnection:
|
||||
push_error("Failed to initialize WebRTC peer connection: " + str(error))
|
||||
return null
|
||||
|
||||
print("WebRTC peer connection initialized with STUN server: ", STUN_SERVER)
|
||||
print("WebRTC peer connection initialized with STUN/TURN servers: ", STUN_SERVER, " / ", TURN_SERVER)
|
||||
return peer_connection
|
||||
|
||||
# Add a peer connection for WebRTC mesh networking
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -68,6 +68,19 @@ func spawn_player(peer_id: int, local_index: int):
|
||||
# Fallback if no spawn points
|
||||
spawn_pos = Vector2.ZERO
|
||||
|
||||
# CRITICAL: Verify spawn position is safe (on floor, not in wall)
|
||||
# Use game_world's safety check if available
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if game_world and game_world.has_method("_is_safe_spawn_position"):
|
||||
if not game_world._is_safe_spawn_position(spawn_pos):
|
||||
# Spawn position is not safe, find a nearby safe position
|
||||
if game_world.has_method("_find_nearby_safe_spawn_position"):
|
||||
var safe_pos = game_world._find_nearby_safe_spawn_position(spawn_pos, 128.0)
|
||||
print("Player ", unique_id, " spawn position ", spawn_pos, " was unsafe, using safe position: ", safe_pos)
|
||||
spawn_pos = safe_pos
|
||||
else:
|
||||
print("Player ", unique_id, " WARNING: Spawn position ", spawn_pos, " is not safe, but no safe position finder available!")
|
||||
|
||||
player.position = spawn_pos
|
||||
|
||||
# Add to YSort node for automatic Y-sorting
|
||||
|
||||
@@ -119,7 +119,7 @@ func send_room_update(player_count: int, level: int = -1) -> bool:
|
||||
var json_string = JSON.stringify(json_data)
|
||||
var headers = ["Content-Type: application/json"]
|
||||
|
||||
log_print("RoomRegistry: Sending room update: " + json_string)
|
||||
log_print("RoomRegistry: Sending room update (room: " + current_room + ", players: " + str(player_count) + ", level: " + str(level) + ")")
|
||||
var error = http_request_update.request(ROOM_UPDATE_URL, headers, HTTPClient.METHOD_POST, json_string)
|
||||
if error != OK:
|
||||
log_error("RoomRegistry: Failed to send room update: " + str(error))
|
||||
|
||||
235
src/scripts/staff_projectile.gd
Normal file
235
src/scripts/staff_projectile.gd
Normal file
@@ -0,0 +1,235 @@
|
||||
extends Node2D
|
||||
|
||||
# Staff Projectile - Travels away from player and deals damage (magic ball)
|
||||
|
||||
@export var damage: float = 20.0
|
||||
@export var initial_speed: float = 300.0 # Faster than sword projectile
|
||||
@export var deceleration: float = 600.0 # Slower deceleration (travels further)
|
||||
@export var lifetime: float = 0.8 # Longer lifetime
|
||||
@export var max_distance: float = 200.0 # Travels further
|
||||
|
||||
var current_speed: float = 0.0
|
||||
|
||||
var travel_direction: Vector2 = Vector2.RIGHT
|
||||
var elapsed_time: float = 0.0
|
||||
var distance_traveled: float = 0.0
|
||||
var player_owner: Node = null
|
||||
var hit_targets = {} # Track what we've already hit (Dictionary for O(1) lookup)
|
||||
var color_replacements: Array = [] # Color replacements from staff item
|
||||
|
||||
@onready var sprite = $Sprite2D
|
||||
@onready var hit_area = $Area2D
|
||||
|
||||
func _ready():
|
||||
# Apply color replacements if available
|
||||
_apply_color_replacements()
|
||||
$SfxSwosh.play()
|
||||
$AnimationPlayer.play("flying")
|
||||
# Connect area signals
|
||||
if hit_area:
|
||||
hit_area.body_entered.connect(_on_body_entered)
|
||||
|
||||
func setup(direction: Vector2, owner_player: Node, damage_value: float = 20.0, staff_item: Item = null):
|
||||
travel_direction = direction.normalized()
|
||||
player_owner = owner_player
|
||||
damage = damage_value # Set damage from player
|
||||
current_speed = initial_speed
|
||||
|
||||
# Store color replacements from staff
|
||||
if staff_item and staff_item.colorReplacements:
|
||||
color_replacements = staff_item.colorReplacements
|
||||
|
||||
# Rotate sprite to face travel direction
|
||||
rotation = direction.angle()
|
||||
|
||||
# Apply color replacements after setup (in case sprite wasn't ready yet)
|
||||
_apply_color_replacements()
|
||||
|
||||
func _apply_color_replacements():
|
||||
# Apply color replacements to projectile sprite using shader parameters
|
||||
if not sprite or not sprite.material or not sprite.material is ShaderMaterial:
|
||||
return
|
||||
|
||||
if color_replacements.size() == 0:
|
||||
return
|
||||
|
||||
var shader_material = sprite.material as ShaderMaterial
|
||||
# Filter for "magic" colors only (RGB 174,39,30; RGB 109,29,32; RGB 246,57,48)
|
||||
# These are the colors that should be replaced on the projectile
|
||||
var magic_colors = [
|
||||
Color(174/255.0, 39/255.0, 30/255.0),
|
||||
Color(109/255.0, 29/255.0, 32/255.0),
|
||||
Color(246/255.0, 57/255.0, 48/255.0)
|
||||
]
|
||||
|
||||
var replacement_index = 0
|
||||
for color_replacement in color_replacements:
|
||||
if color_replacement.has("original") and color_replacement.has("replace"):
|
||||
var original_color = color_replacement["original"] as Color
|
||||
# Only apply replacements for magic colors
|
||||
for magic_color in magic_colors:
|
||||
# Check if this replacement matches a magic color (with some tolerance)
|
||||
if _colors_similar(original_color, magic_color, 0.1):
|
||||
var replace_color = color_replacement["replace"] as Color
|
||||
shader_material.set_shader_parameter("original_" + str(replacement_index), original_color)
|
||||
shader_material.set_shader_parameter("replace_" + str(replacement_index), replace_color)
|
||||
replacement_index += 1
|
||||
break # Found match, move to next replacement
|
||||
|
||||
func _colors_similar(color1: Color, color2: Color, tolerance: float = 0.1) -> bool:
|
||||
# Check if two colors are similar within tolerance
|
||||
var r_diff = abs(color1.r - color2.r)
|
||||
var g_diff = abs(color1.g - color2.g)
|
||||
var b_diff = abs(color1.b - color2.b)
|
||||
return r_diff <= tolerance and g_diff <= tolerance and b_diff <= tolerance
|
||||
|
||||
func _physics_process(delta):
|
||||
elapsed_time += delta
|
||||
|
||||
# Check lifetime
|
||||
if elapsed_time >= lifetime or distance_traveled >= max_distance:
|
||||
$Area2D.set_deferred("monitoring", false)
|
||||
self.visible = false
|
||||
if $SfxImpactWall.playing:
|
||||
await $SfxImpactWall.finished
|
||||
if $SfxImpact.playing:
|
||||
await $SfxImpact.finished
|
||||
queue_free()
|
||||
return
|
||||
|
||||
# Decelerate
|
||||
current_speed -= deceleration * delta
|
||||
current_speed = max(0.0, current_speed) # Don't go negative
|
||||
|
||||
# Move in travel direction
|
||||
var movement = travel_direction * current_speed * delta
|
||||
global_position += movement
|
||||
distance_traveled += movement.length()
|
||||
|
||||
# Fade out (based on speed)
|
||||
var alpha = current_speed / initial_speed # 1.0 at start, 0.0 when stopped
|
||||
if sprite:
|
||||
sprite.modulate.a = alpha
|
||||
|
||||
func _on_body_entered(body):
|
||||
# Don't hit the owner
|
||||
if body == player_owner:
|
||||
return
|
||||
|
||||
# Don't hit the same target twice - use Dictionary for O(1) lookup to prevent race conditions
|
||||
if body in hit_targets:
|
||||
return
|
||||
|
||||
# CRITICAL: Only the projectile owner (authority) should deal damage
|
||||
if player_owner and not player_owner.is_multiplayer_authority():
|
||||
return # Only the authority (creator) of the projectile can deal damage
|
||||
|
||||
# Add to hit_targets IMMEDIATELY to prevent multiple hits (mark as hit before processing)
|
||||
hit_targets[body] = true
|
||||
|
||||
# Deal damage to players - call RPC to let victim apply damage on their client
|
||||
if body.is_in_group("player") and body.has_method("rpc_take_damage"):
|
||||
$SfxImpact.play()
|
||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
||||
var player_peer_id = body.get_multiplayer_authority()
|
||||
if player_peer_id != 0:
|
||||
if multiplayer.is_server() and player_peer_id == multiplayer.get_unique_id():
|
||||
body.rpc_take_damage(damage, attacker_pos)
|
||||
else:
|
||||
body.rpc_take_damage.rpc_id(player_peer_id, damage, attacker_pos)
|
||||
else:
|
||||
body.rpc_take_damage.rpc(damage, attacker_pos)
|
||||
print("Staff projectile hit player: ", body.name, " for ", damage, " damage!")
|
||||
|
||||
# Deal damage to enemies - only authority (creator) deals damage
|
||||
elif body.is_in_group("enemy") and body.has_method("rpc_take_damage"):
|
||||
var attacker_pos = player_owner.global_position if player_owner else global_position
|
||||
var is_crit = get_meta("is_crit") if has_meta("is_crit") else false
|
||||
|
||||
# Check hit chance (based on player's DEX stat)
|
||||
var hit_roll = randf()
|
||||
var hit_chance = 0.95 # Base hit chance
|
||||
if player_owner and player_owner.character_stats:
|
||||
hit_chance = player_owner.character_stats.hit_chance
|
||||
var is_miss = hit_roll >= hit_chance
|
||||
|
||||
if is_miss:
|
||||
# Attack missed
|
||||
print("Player MISSED enemy: ", body.name, "! (hit chance: ", hit_chance * 100.0, "%)")
|
||||
if body.has_method("_show_damage_number"):
|
||||
body._show_damage_number(0.0, attacker_pos, false, true, false) # is_miss = true
|
||||
return
|
||||
|
||||
# Hit successful
|
||||
$SfxImpact.play()
|
||||
|
||||
# Use game_world to route damage request
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
var enemy_name = body.name
|
||||
var enemy_index = body.get_meta("enemy_index") if body.has_meta("enemy_index") else -1
|
||||
|
||||
if game_world and game_world.has_method("_request_enemy_damage"):
|
||||
if multiplayer.is_server():
|
||||
game_world._request_enemy_damage(enemy_name, enemy_index, damage, attacker_pos, is_crit)
|
||||
else:
|
||||
game_world._request_enemy_damage.rpc_id(1, enemy_name, enemy_index, damage, attacker_pos, is_crit)
|
||||
else:
|
||||
# Fallback
|
||||
var enemy_peer_id = body.get_multiplayer_authority()
|
||||
if enemy_peer_id != 0:
|
||||
if multiplayer.is_server() and enemy_peer_id == multiplayer.get_unique_id():
|
||||
body.rpc_take_damage(damage, attacker_pos, is_crit)
|
||||
else:
|
||||
body.rpc_take_damage.rpc_id(enemy_peer_id, damage, attacker_pos, is_crit)
|
||||
else:
|
||||
body.rpc_take_damage.rpc(damage, attacker_pos, is_crit)
|
||||
|
||||
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("Staff projectile hit enemy: ", body.name, " for ", damage, " damage! (owner: ", owner_name, " is_authority: ", is_authority, ")")
|
||||
return
|
||||
|
||||
# Deal damage to boxes or other damageable objects
|
||||
elif "health" in body:
|
||||
body.health -= damage
|
||||
$SfxImpact.play()
|
||||
if body.health <= 0:
|
||||
# Get object identifier
|
||||
var obj_name = body.name
|
||||
var obj_index = -1
|
||||
|
||||
if body.has_meta("object_index"):
|
||||
obj_index = body.get_meta("object_index")
|
||||
if obj_index >= 0:
|
||||
obj_name = "InteractableObject_%d" % obj_index
|
||||
|
||||
if not obj_name.begins_with("InteractableObject_") and obj_index < 0:
|
||||
print("Staff projectile: Warning - object ", body.name, " doesn't have consistent naming!")
|
||||
|
||||
# Sync break to server
|
||||
if multiplayer.has_multiplayer_peer():
|
||||
var game_world = get_tree().get_first_node_in_group("game_world")
|
||||
if game_world and is_instance_valid(game_world) and game_world.is_inside_tree() and game_world.has_method("_sync_object_break"):
|
||||
if multiplayer.is_server():
|
||||
if game_world.has_method("_rpc_to_ready_peers"):
|
||||
game_world._rpc_to_ready_peers("_sync_object_break", [obj_name])
|
||||
print("Staff projectile synced box break to all clients: ", obj_name)
|
||||
else:
|
||||
game_world._sync_object_break.rpc_id(1, obj_name)
|
||||
print("Staff projectile requested box break on server: ", obj_name, " (index: ", obj_index, ")")
|
||||
else:
|
||||
print("Staff projectile: GameWorld not ready, skipping box break sync for ", obj_name)
|
||||
|
||||
# Break locally AFTER syncing
|
||||
if body.has_method("_break_into_pieces"):
|
||||
body._break_into_pieces()
|
||||
print("Staff projectile broke box locally: ", body.name)
|
||||
print("Staff projectile hit object: ", body.name)
|
||||
|
||||
# Push the hit target away slightly (only for non-enemies)
|
||||
if body is CharacterBody2D and not body.is_in_group("enemy"):
|
||||
var knockback_dir = (body.global_position - global_position).normalized()
|
||||
body.velocity = knockback_dir * 200.0
|
||||
1
src/scripts/staff_projectile.gd.uid
Normal file
1
src/scripts/staff_projectile.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bn5vp502u6pf5
|
||||
60
src/shaders/cloth.gdshader
Normal file
60
src/shaders/cloth.gdshader
Normal file
@@ -0,0 +1,60 @@
|
||||
shader_type canvas_item;
|
||||
render_mode unshaded;
|
||||
|
||||
uniform vec4 original_0: source_color;
|
||||
uniform vec4 original_1: source_color;
|
||||
uniform vec4 original_2: source_color;
|
||||
uniform vec4 original_3: source_color;
|
||||
uniform vec4 original_4: source_color;
|
||||
uniform vec4 original_5: source_color;
|
||||
uniform vec4 original_6: source_color;
|
||||
uniform vec4 replace_0: source_color;
|
||||
uniform vec4 replace_1: source_color;
|
||||
uniform vec4 replace_2: source_color;
|
||||
uniform vec4 replace_3: source_color;
|
||||
uniform vec4 replace_4: source_color;
|
||||
uniform vec4 replace_5: source_color;
|
||||
uniform vec4 replace_6: source_color;
|
||||
|
||||
uniform vec4 tint: source_color = vec4(1.0);
|
||||
|
||||
const float precision = 0.1;
|
||||
const int Colz = 7;
|
||||
|
||||
vec4 swap_color(vec4 color){
|
||||
vec4 original_colors[Colz] = vec4[Colz] (original_0, original_1, original_2, original_3, original_4, original_5, original_6);
|
||||
vec4 replace_colors[Colz] = vec4[Colz] (replace_0, replace_1, replace_2, replace_3, replace_4, replace_5, replace_6);
|
||||
for (int i = 0; i < Colz; i ++) {
|
||||
if (distance(color, original_colors[i]) <= precision){
|
||||
return replace_colors[i];
|
||||
}
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
|
||||
void fragment() {
|
||||
vec4 col = swap_color(texture(TEXTURE, UV));
|
||||
//#COLOR = mix(col, tint, 1.0);
|
||||
COLOR = col * tint;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
1
src/shaders/cloth.gdshader.uid
Normal file
1
src/shaders/cloth.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ldl7vaq5n13f
|
||||
Reference in New Issue
Block a user