fixed finally webrtc

This commit is contained in:
2026-01-19 23:51:57 +01:00
parent 454c065cf3
commit 1c247f3d82
44 changed files with 5264 additions and 486 deletions

Binary file not shown.

View 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

Binary file not shown.

View 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

Binary file not shown.

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View 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

View File

@@ -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

View File

@@ -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]

View File

@@ -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")

View File

@@ -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

View 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"

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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:
# 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:
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)
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)
break
else:
LogManager.log("DungeonGenerator: room_puzzle_data is empty for room (" + str(room.x) + "," + str(room.y) + ")", LogManager.CATEGORY_DUNGEON)
@@ -1905,7 +1931,32 @@ 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.")
# 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

View File

@@ -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,51 +549,167 @@ 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
# 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+
# 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:
# 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
var item = ItemDatabase.get_random_enemy_drop()
# 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, global_position, entities_node, game_world)
LogManager.log(str(name) + " ✓ dropped item: " + str(item.item_name) + " at " + str(safe_spawn_pos), LogManager.CATEGORY_ENEMY)
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()
@@ -604,25 +721,42 @@ func _spawn_loot():
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)
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:
# Generate unique loot ID
# 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
var loot_id = game_world.loot_id_counter
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 spawn to clients", LogManager.CATEGORY_ENEMY)
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()

View File

@@ -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
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:

View File

@@ -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:

View File

@@ -103,8 +103,8 @@ func _on_body_exited(body):
if object_type == "Pillar":
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)
@@ -113,8 +113,8 @@ func _on_body_exited(body):
# 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
_check_activation()

46
src/scripts/fog_of_war.gd Normal file
View 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)

View File

@@ -0,0 +1 @@
uid://dvexhx0su0ung

View File

@@ -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
# 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()
elif actual_mode != 1: # Not WebRTC mode
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

View File

@@ -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

View File

@@ -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,7 +807,24 @@ 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)
# 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")

View File

@@ -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" + \

View File

@@ -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

View File

@@ -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

View File

@@ -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,6 +184,11 @@ func _physics_process(delta):
if collected:
return
var is_client = multiplayer.has_multiplayer_peer() and not is_multiplayer_authority()
var is_server = not multiplayer.has_multiplayer_peer() or is_multiplayer_authority()
# Server (authority): Run physics normally
if is_server:
# Update bounce timer
if bounce_timer > 0.0:
bounce_timer -= delta
@@ -179,52 +197,31 @@ func _physics_process(delta):
# Update Z-axis physics
if is_airborne:
# Apply gravity to Z-axis
# Apply gravity to Z-axis (matches old code)
acceleration_z = -300.0 # Gravity
velocity_z += acceleration_z * delta
# 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))
position_z += velocity_z * delta
# Apply air resistance to slow down horizontal movement while airborne
velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-8.0 * delta))
# Ground collision and bounce (skip if collected to prevent bounce sounds)
# Ground collision and bounce (matches old code - simpler, no aggressive damping)
if position_z <= 0.0:
position_z = 0.0
# Apply friction when on ground (dampen X/Y momentum faster)
# Apply friction ONLY when on ground (matches old code behavior)
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
# Check if we should bounce (simpler logic matching old code)
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)
# 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:
# 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))
# 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.12 # Prevent rapid bounce sounds but allow reasonable bounce rate
bounce_timer = 0.08 # Matches old code timing
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:
velocity_z = 0.0
is_airborne = false
else:
# Simple bounce (matches old code)
velocity_z = -velocity_z * bounce_restitution
is_airborne = true # Still bouncing
else:
# Velocity too small or collected - stop bouncing
@@ -234,13 +231,14 @@ func _physics_process(delta):
is_airborne = false
# Ensure velocity_z is zero when on ground
velocity_z = 0.0
# Apply friction even when not airborne (on ground)
# 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()
# 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)
@@ -248,16 +246,22 @@ func _physics_process(delta):
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 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
# 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
bounce_timer = 0.08 # Matches old code timing
# Update visual position based on Z
_update_visuals()
@@ -266,6 +270,89 @@ func _physics_process(delta):
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 = -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))
# Move and check for collisions
move_and_slide()
# 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

View File

@@ -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

View File

@@ -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
var config = {
"iceServers": [
{
"urls": [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": 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

View File

@@ -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

View File

@@ -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))

View 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

View File

@@ -0,0 +1 @@
uid://bn5vp502u6pf5

View 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;
}

View File

@@ -0,0 +1 @@
uid://ldl7vaq5n13f