diff --git a/src/scenes/door.tscn b/src/scenes/door.tscn index 5e30d03..68efdc8 100644 --- a/src/scenes/door.tscn +++ b/src/scenes/door.tscn @@ -37,6 +37,13 @@ texture = ExtResource("1_hpvv5") shape = SubResource("RectangleShape2D_uvdjg") metadata/_edit_lock_ = true +[node name="KeyInteractionArea" type="Area2D" parent="." unique_id=982067740] +metadata/_edit_lock_ = true + +[node name="CollisionShape2D" type="CollisionShape2D" parent="KeyInteractionArea" unique_id=1640987231] +shape = SubResource("RectangleShape2D_la1wf") +debug_color = Color(0.70196074, 0.67558956, 0.17869899, 0.41960785) + [node name="SfxOpenKeyDoor" type="AudioStreamPlayer2D" parent="." unique_id=47303726] stream = ExtResource("3_la1wf") max_distance = 1485.0 @@ -45,30 +52,29 @@ panning_strength = 1.09 [node name="SfxOpenStoneDoor" type="AudioStreamPlayer2D" parent="." unique_id=885417421] stream = ExtResource("4_18pbm") +volume_db = -3.382 max_distance = 1204.0 attenuation = 6.498014 panning_strength = 1.25 [node name="SfxOpenGateDoor" type="AudioStreamPlayer2D" parent="." unique_id=442358170] stream = ExtResource("6_ju5n0") -volume_db = -4.65 +volume_db = -5.073 pitch_scale = 1.1 max_distance = 1246.0 attenuation = 7.999997 panning_strength = 1.3 -[node name="KeyInteractionArea" type="Area2D" parent="." unique_id=982067740] -metadata/_edit_lock_ = true - -[node name="CollisionShape2D" type="CollisionShape2D" parent="KeyInteractionArea" unique_id=1640987231] -shape = SubResource("RectangleShape2D_la1wf") -debug_color = Color(0.70196074, 0.67558956, 0.17869899, 0.41960785) - [node name="SfxDoorCloses" type="AudioStreamPlayer2D" parent="." unique_id=1074871158] stream = SubResource("AudioStreamRandomizer_ey00f") -volume_db = -1.268 +volume_db = -6.925 max_distance = 1289.0 -attenuation = 3.7321312 +attenuation = 7.209997 +panning_strength = 1.14 [node name="SfxCloseGateDoor" type="AudioStreamPlayer2D" parent="." unique_id=1825261269] stream = ExtResource("8_pg2b6") +volume_db = -4.227 +max_distance = 1246.0 +attenuation = 8.282116 +panning_strength = 1.02 diff --git a/src/scenes/game_world.tscn b/src/scenes/game_world.tscn index 5d30711..dbde1d2 100644 --- a/src/scenes/game_world.tscn +++ b/src/scenes/game_world.tscn @@ -4,6 +4,7 @@ [ext_resource type="PackedScene" uid="uid://cxfvw8y7jqn2p" path="res://scenes/player.tscn" id="2"] [ext_resource type="Script" uid="uid://db58xcyo4cjk" path="res://scripts/game_world.gd" id="4"] [ext_resource type="Script" uid="uid://wff5063ctp7g" path="res://scripts/debug_overlay.gd" id="5"] +[ext_resource type="AudioStream" uid="uid://dthr2w8x0cj6v" path="res://assets/audio/sfx/ambience/wind-castle-loop.wav.mp3" id="6_6c6v5"] [ext_resource type="TileSet" uid="uid://dqem5tbvooxrg" path="res://assets/gfx/RPG DUNGEON VOL 3.tres" id="9"] [node name="GameWorld" type="Node2D" unique_id=430665106] @@ -37,3 +38,8 @@ script = ExtResource("5") light_mask = 1048575 visibility_layer = 1048575 color = Color(0.671875, 0.671875, 0.671875, 1) + +[node name="AudioStreamPlayer2D" type="AudioStreamPlayer2D" parent="." unique_id=1141138343] +stream = ExtResource("6_6c6v5") +autoplay = true +stream_paused = true diff --git a/src/scenes/interactable_object.tscn b/src/scenes/interactable_object.tscn index 6341e7c..5078075 100644 --- a/src/scenes/interactable_object.tscn +++ b/src/scenes/interactable_object.tscn @@ -27,6 +27,7 @@ [ext_resource type="AudioStream" uid="uid://dsokwxmutlwk5" path="res://assets/audio/sfx/environment/move_rock/rock_push_loop_02.mp3" id="25_1u1k0"] [ext_resource type="AudioStream" uid="uid://4ilddgc4lgyq" path="res://assets/audio/sfx/environment/crate/crash_table-04.wav" id="26_vfomk"] [ext_resource type="AudioStream" uid="uid://c7kc0aw0wevah" path="res://assets/audio/sfx/environment/crate/wood_impact_break.mp3" id="27_2p257"] +[ext_resource type="Texture2D" uid="uid://bknascfv4twmi" path="res://assets/gfx/smoke_puffs.png" id="28_2p257"] [sub_resource type="CapsuleShape2D" id="CapsuleShape2D_nyc8x"] radius = 4.0 @@ -80,6 +81,71 @@ streams_count = 2 stream_0/stream = ExtResource("26_vfomk") stream_1/stream = ExtResource("27_2p257") +[sub_resource type="CanvasItemMaterial" id="CanvasItemMaterial_ik3co"] +particles_animation = true +particles_anim_h_frames = 4 +particles_anim_v_frames = 2 +particles_anim_loop = false + +[sub_resource type="Curve" id="Curve_dh4ly"] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(0.780549, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0] +point_count = 3 + +[sub_resource type="CurveTexture" id="CurveTexture_m11t2"] +curve = SubResource("Curve_dh4ly") + +[sub_resource type="Curve" id="Curve_c8svp"] +_limits = [0.0, 100.0, 0.0, 1.0] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(0.733167, 4.55855), 0.0, 0.0, 0, 0, Vector2(0.815461, 91.8906), 0.0, 0.0, 0, 0, Vector2(0.892768, 100), 0.0, 0.0, 0, 0] +point_count = 4 + +[sub_resource type="CurveTexture" id="CurveTexture_ui3li"] +curve = SubResource("Curve_c8svp") + +[sub_resource type="Curve" id="Curve_oexrv"] +_limits = [0.0, 1.0, -1.0, 1.0] +_data = [Vector2(-1, 0), 0.0, 0.0, 0, 0, Vector2(0.0124688, 1), 0.0, 0.0, 0, 0, Vector2(0.516209, 1), 0.0, 0.0, 0, 0, Vector2(0.947631, 0), 0.0, 0.0, 0, 0] +point_count = 4 + +[sub_resource type="Curve" id="Curve_27s1c"] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0] +point_count = 2 + +[sub_resource type="Curve" id="Curve_igjib"] +_data = [Vector2(0, 1), 0.0, 0.0, 0, 0, Vector2(1, 1), 0.0, 0.0, 0, 0] +point_count = 2 + +[sub_resource type="CurveXYZTexture" id="CurveXYZTexture_tjjlx"] +curve_x = SubResource("Curve_oexrv") +curve_y = SubResource("Curve_27s1c") +curve_z = SubResource("Curve_igjib") + +[sub_resource type="Curve" id="Curve_by4wh"] +_data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(0.0224439, 1), 0.0, 0.0, 0, 0, Vector2(0.880299, 1), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0] +point_count = 4 + +[sub_resource type="CurveTexture" id="CurveTexture_sp8mg"] +curve = SubResource("Curve_by4wh") + +[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_ejwle"] +particle_flag_disable_z = true +direction = Vector3(1, 0.2, 0) +spread = 62.79 +initial_velocity_min = -30.0 +initial_velocity_max = 30.0 +directional_velocity_min = -25.0 +directional_velocity_max = 25.0 +directional_velocity_curve = SubResource("CurveXYZTexture_tjjlx") +gravity = Vector3(0, 0, 0) +damping_max = 100.0 +damping_curve = SubResource("CurveTexture_ui3li") +scale_min = 0.8 +scale_max = 1.2 +scale_curve = SubResource("CurveTexture_sp8mg") +color = Color(1, 1, 1, 0.709804) +alpha_curve = SubResource("CurveTexture_m11t2") +anim_offset_max = 1.0 + [node name="InteractableObject" type="CharacterBody2D" unique_id=1472163831] collision_layer = 2 collision_mask = 71 @@ -147,3 +213,24 @@ volume_db = -2.611 [node name="SfxBreakCrate" type="AudioStreamPlayer2D" parent="." unique_id=1799447869] stream = SubResource("AudioStreamRandomizer_ik3co") volume_db = -6.092 + +[node name="DragParticles" type="GPUParticles2D" parent="." unique_id=2123830325] +z_index = -1 +y_sort_enabled = true +material = SubResource("CanvasItemMaterial_ik3co") +emitting = false +amount = 16 +texture = ExtResource("28_2p257") +lifetime = 0.66 +interp_to_end = 0.026 +preprocess = 0.16 +explosiveness = 0.15 +randomness = 0.63 +use_fixed_seed = true +seed = 1565624367 +process_material = SubResource("ParticleProcessMaterial_ejwle") + +[node name="TimerSmokeParticles" type="Timer" parent="DragParticles" unique_id=2105569542] +wait_time = 0.07 + +[connection signal="timeout" from="DragParticles/TimerSmokeParticles" to="." method="_on_timer_smoke_particles_timeout"] diff --git a/src/scenes/player.tscn b/src/scenes/player.tscn index f48e1aa..d507959 100644 --- a/src/scenes/player.tscn +++ b/src/scenes/player.tscn @@ -58,7 +58,7 @@ radius = 8.0 [sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_l71n6"] playback_mode = 1 -random_pitch = 1.0059091 +random_pitch = 1.0118532 streams_count = 6 stream_0/stream = ExtResource("13_fulsm") stream_1/stream = ExtResource("14_4r5pv") @@ -69,7 +69,7 @@ stream_5/stream = ExtResource("18_4ni07") [sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_487ah"] playback_mode = 1 -random_pitch = 1.0059091 +random_pitch = 1.0118532 streams_count = 7 stream_0/stream = ExtResource("20_ujl30") stream_1/stream = ExtResource("21_31cv2") @@ -182,8 +182,10 @@ horizontal_alignment = 1 [node name="SfxWalk" type="AudioStreamPlayer2D" parent="." unique_id=1693322702] stream = SubResource("AudioStreamRandomizer_l71n6") -volume_db = -12.0 +volume_db = -18.527 +max_distance = 1412.0 attenuation = 8.282109 +panning_strength = 1.11 [node name="TimerWalk" type="Timer" parent="SfxWalk" unique_id=590325386] wait_time = 0.3 @@ -191,12 +193,15 @@ one_shot = true [node name="SfxDie" type="AudioStreamPlayer2D" parent="." unique_id=1173215688] stream = ExtResource("19_4r5pv") +volume_db = -2.537 attenuation = 8.876548 bus = &"Sfx" [node name="SfxTakeDamage" type="AudioStreamPlayer2D" parent="." unique_id=322150091] stream = SubResource("AudioStreamRandomizer_487ah") +volume_db = -6.092 attenuation = 7.7274756 +panning_strength = 1.1 bus = &"Sfx" [node name="SfxThrow" type="AudioStreamPlayer2D" parent="." unique_id=961008127] diff --git a/src/scenes/smoke_puff.tscn b/src/scenes/smoke_puff.tscn index becb2fc..ea71da3 100644 --- a/src/scenes/smoke_puff.tscn +++ b/src/scenes/smoke_puff.tscn @@ -4,6 +4,7 @@ [ext_resource type="Texture2D" uid="uid://bknascfv4twmi" path="res://assets/gfx/smoke_puffs.png" id="2_smoke"] [node name="SmokePuff" type="Node2D" unique_id=243995580] +y_sort_enabled = true script = ExtResource("1_puff") [node name="Sprite2D" type="Sprite2D" parent="." unique_id=1282738570] diff --git a/src/scripts/door.gd b/src/scripts/door.gd index 6bcf6ec..59cbaaf 100644 --- a/src/scripts/door.gd +++ b/src/scripts/door.gd @@ -211,7 +211,12 @@ func _process(delta: float) -> void: global_position = open_position # Also set global position # When moved from closed position (open), collision should be DISABLED set_collision_layer_value(7, false) - print("Door: Opening animation complete - moved to open position: ", open_position, " (closed: ", closed_position, ", offset: ", open_offset, ") - collision DISABLED") + print("Door: Opening animation complete - moved to open position: ", open_position, " (closed: ", closed_position, ", offset: ", open_offset, ") - collision DISABLED", " (key_used=", key_used, ")" if type == "KeyDoor" else "") + + # CRITICAL: For KeyDoors, ensure key_used is true after animation completes + # This prevents the door from being reset to closed in _process() + if type == "KeyDoor": + key_used = true # Spawn smoke puffs when StoneDoor finishes opening (1-2 puffs) if type == "StoneDoor": @@ -1231,6 +1236,10 @@ func _sync_door_open(): is_closed = true set_collision_layer_value(7, true) + # CRITICAL: For KeyDoors, set key_used to true so it doesn't get reset to closed + if type == "KeyDoor": + key_used = true + animation_start_position = position is_opening = true is_closing = false @@ -1244,7 +1253,7 @@ func _sync_door_open(): else: $SfxOpenStoneDoor.play() - print("Door: Client received door open RPC for ", name, " - starting open animation") + print("Door: Client received door open RPC for ", name, " - starting open animation", " (key_used=", key_used, ")" if type == "KeyDoor" else "") @rpc("authority", "reliable") func _sync_puzzle_solved(is_solved: bool): diff --git a/src/scripts/enemy_base.gd b/src/scripts/enemy_base.gd index cd074ae..0331791 100644 --- a/src/scripts/enemy_base.gd +++ b/src/scripts/enemy_base.gd @@ -431,6 +431,7 @@ func _die(): if killer_peer_id != 0 and killer_peer_id != multiplayer.get_unique_id() and killer_player.has_method("_sync_stats_update"): # Server is updating a client's player stats - sync to the client var coins = killer_player.character_stats.coin if "coin" in killer_player.character_stats else 0 + print(name, " syncing kill stats to client peer_id=", killer_peer_id, " kills=", killer_player.character_stats.kills, " coins=", coins) killer_player._sync_stats_update.rpc_id(killer_peer_id, killer_player.character_stats.kills, coins) # Spawn loot immediately (before death animation) diff --git a/src/scripts/enemy_humanoid.gd b/src/scripts/enemy_humanoid.gd index 6f44b5b..a9ad044 100644 --- a/src/scripts/enemy_humanoid.gd +++ b/src/scripts/enemy_humanoid.gd @@ -35,6 +35,7 @@ var is_charging_attack: bool = false var attack_charge_time: float = 0.0 var base_attack_charge_time: float = 0.4 # Base charge time before attack var dex: int = 10 # Dexterity stat (affects attack speed) +var blood_scene = preload("res://scenes/blood_clot.tscn") # AI state enum AIState {IDLE, WANDERING, NOTICED, CHASING, ATTACKING, GROUPING} @@ -69,14 +70,50 @@ const ANIMATIONS = { "nextAnimation": null }, "RUN": { - "frames": [2, 3, 4, 5], - "frameDurations": [150, 150, 150, 150], + "frames": [3, 2, 3, 4], + "frameDurations": [140, 140, 140, 140], "loop": true, "nextAnimation": null }, "SWORD": { - "frames": [6, 7, 8], - "frameDurations": [100, 100, 200], + "frames": [5, 6, 7, 8], + "frameDurations": [40, 60, 90, 80], + "loop": false, + "nextAnimation": "IDLE" + }, + "AXE": { + "frames": [5, 6, 7, 8], + "frameDurations": [50, 70, 100, 90], + "loop": false, + "nextAnimation": "IDLE" + }, + "PUNCH": { + "frames": [16, 17, 18], + "frameDurations": [50, 70, 100], + "loop": false, + "nextAnimation": "IDLE" + }, + "BOW": { + "frames": [9, 10, 11, 12], + "frameDurations": [80, 110, 110, 80], + "loop": false, + "nextAnimation": "IDLE" + }, + "STAFF": { + "frames": [13, 14, 15], + "frameDurations": [200, 200, 400], + "loop": false, + "nextAnimation": "IDLE" + }, + "THROW": { + "frames": [16, 17, 18], + "frameDurations": [80, 80, 300], + "loop": false, + "nextAnimation": "IDLE" + }, + "CONJURE": { + "frames": [19], + "frameDurations": [400], "loop": false, "nextAnimation": "IDLE" }, @@ -91,6 +128,42 @@ const ANIMATIONS = { "frameDurations": [200, 200, 200, 800], "loop": false, "nextAnimation": null + }, + "IDLE_HOLD": { + "frames": [25], + "frameDurations": [500], + "loop": true, + "nextAnimation": null + }, + "RUN_HOLD": { + "frames": [25, 26, 25, 27], + "frameDurations": [150, 150, 150, 150], + "loop": true, + "nextAnimation": null + }, + "JUMP": { + "frames": [25, 26, 27, 28], + "frameDurations": [80, 80, 80, 800], + "loop": false, + "nextAnimation": "IDLE" + }, + "LIFT": { + "frames": [19, 30], + "frameDurations": [70, 70], + "loop": false, + "nextAnimation": "IDLE_HOLD" + }, + "IDLE_PUSH": { + "frames": [30], + "frameDurations": [10], + "loop": true, + "nextAnimation": null + }, + "RUN_PUSH": { + "frames": [30, 29, 30, 31], + "frameDurations": [260, 260, 260, 260], + "loop": true, + "nextAnimation": null } } @@ -1328,6 +1401,19 @@ func _play_death_animation(): # Play death sound effect if sfx_die: + for i in 12: + var angle = randf_range(0, TAU) + var speed = randf_range(50, 100) + var initial_velocityZ = randf_range(50, 90) + var b = blood_scene.instantiate() as CharacterBody2D + b.scale = Vector2(randf_range(0.3, 2), randf_range(0.3, 2)) + b.global_position = global_position + + # Set initial velocities from the synchronized data + var direction = Vector2.from_angle(angle) + b.velocity = direction * speed + b.velocityZ = initial_velocityZ + get_parent().call_deferred("add_child", b) sfx_die.play() # Wait for death animation to finish (same duration as player: 200+200+200+800 = 1400ms = 1.4s) diff --git a/src/scripts/game_world.gd b/src/scripts/game_world.gd index cab3de0..5dfe6fd 100644 --- a/src/scripts/game_world.gd +++ b/src/scripts/game_world.gd @@ -24,6 +24,9 @@ var level_coins_collected: int = 0 # Client ready tracking (server only) var clients_ready: Dictionary = {} # peer_id -> bool +# Level complete tracking +var level_complete_triggered: bool = false # Prevent multiple level complete triggers + func _ready(): # Add to group for easy access add_to_group("game_world") @@ -376,6 +379,31 @@ func _request_loot_pickup(loot_id: int, loot_position: Vector2, player_peer_id: else: print("GameWorld: Could not find loot for pickup request: id=", loot_id, " pos=", loot_position) +@rpc("authority", "reliable") +func _sync_player_exit_stairs(player_peer_id: int): + # Client receives notification that a player reached exit stairs + if multiplayer.is_server(): + return # Server ignores this (it's the sender) + + # Find the player by peer ID + var players = get_tree().get_nodes_in_group("player") + var target_player = null + for p in players: + if p.has_method("get_multiplayer_authority") and p.get_multiplayer_authority() == player_peer_id: + target_player = p + break + + # Only disable controls/collision if this is our local player + if target_player: + var my_peer_id = multiplayer.get_unique_id() + if player_peer_id == my_peer_id: + # This is our local player - disable controls and collision + target_player.controls_disabled = true + target_player.set_collision_layer_value(1, false) + print("GameWorld: Client disabled controls and collision for local player ", target_player.name) + # Show black fade overlay for this player + _show_black_fade_overlay() + @rpc("authority", "reliable") func _sync_show_level_complete(level_time: float): # Clients receive level complete UI sync from server @@ -400,6 +428,30 @@ func _sync_hide_level_complete(): if level_complete_ui: level_complete_ui.visible = false +@rpc("authority", "reliable") +func _sync_restore_player_controls(): + # Clients receive restore controls/collision sync from server + if multiplayer.is_server(): + return # Server ignores this (it's the sender) + + # Restore controls and collision for local player + var my_peer_id = multiplayer.get_unique_id() + var players = get_tree().get_nodes_in_group("player") + for player in players: + if player.has_method("get_multiplayer_authority") and player.get_multiplayer_authority() == my_peer_id: + player.controls_disabled = false + player.set_collision_layer_value(1, true) + print("GameWorld: Client restored controls and collision for local player ", player.name) + break + +@rpc("authority", "reliable") +func _sync_remove_black_fade(): + # Clients receive remove black fade sync from server + if multiplayer.is_server(): + return # Server ignores this (it's the sender) + + _remove_black_fade_overlay() + @rpc("authority", "reliable") func _sync_show_level_number(level: int): # Clients receive level number UI sync from server @@ -478,6 +530,9 @@ func _generate_dungeon(): print("GameWorld: Not server, skipping dungeon generation") return + # Reset level complete flag for new level + level_complete_triggered = false + print("GameWorld: Generating dungeon level ", current_level) # Generate seed (deterministic for level 1, can be random for future levels) @@ -1555,8 +1610,32 @@ func _on_player_reached_stairs(player: Node): if not multiplayer.is_server() and multiplayer.has_multiplayer_peer(): return # Only server handles this + # Prevent multiple triggers - if already triggered, ignore + if level_complete_triggered: + print("GameWorld: Level complete already triggered, ignoring duplicate trigger") + return + print("GameWorld: Player ", player.name, " reached stairs!") + # Mark as triggered to prevent re-triggering + level_complete_triggered = true + + # Disable controls and collision for the player who reached stairs + var player_peer_id = player.get_multiplayer_authority() if player.has_method("get_multiplayer_authority") else 0 + player.controls_disabled = true + # Remove collision layer (layer 1 = players) + player.set_collision_layer_value(1, false) + print("GameWorld: Disabled controls and collision for player ", player.name) + + # Show black fade overlay for server's local player if this is the server's player + var my_peer_id = multiplayer.get_unique_id() if multiplayer.has_multiplayer_peer() else 1 + if player_peer_id == my_peer_id: + _show_black_fade_overlay() + + # Sync controls disabled and collision removal to clients + if multiplayer.has_multiplayer_peer() and player_peer_id > 0: + _sync_player_exit_stairs.rpc(player_peer_id) + # Drop any held objects for all players before level completion var entities_node = get_node_or_null("Entities") if entities_node: @@ -1629,6 +1708,18 @@ func _on_player_reached_stairs(player: Node): if hud and hud.has_method("start_timer"): hud.start_timer() + # Restore controls and collision for all players (server side) + _restore_player_controls_and_collision() + + # Sync restore to all clients + if multiplayer.has_multiplayer_peer(): + _sync_restore_player_controls.rpc() + + # Remove black fade overlay (server and clients) + _remove_black_fade_overlay() + if multiplayer.has_multiplayer_peer(): + _sync_remove_black_fade.rpc() + # Move all players to start room (server side) _move_all_players_to_start_room() @@ -1665,9 +1756,10 @@ func _get_local_player_stats() -> Dictionary: var local_player = null if multiplayer.has_multiplayer_peer(): - # Multiplayer: find player with matching authority + # Multiplayer: find player with matching authority (client's own player) + var my_peer_id = multiplayer.get_unique_id() for player in players: - if player.has_method("is_multiplayer_authority") and player.is_multiplayer_authority(): + if player.has_method("get_multiplayer_authority") and player.get_multiplayer_authority() == my_peer_id: local_player = player break else: @@ -1759,6 +1851,54 @@ func _fade_in_player(player: Node): sprite_layer.modulate.a = 0.0 # Start invisible fade_tween.tween_property(sprite_layer, "modulate:a", 1.0, 1.0) +func _show_black_fade_overlay(): + # Create black fade overlay for player who reached exit + # Remove existing fade if any + var existing_fade = get_node_or_null("BlackFadeOverlay") + if existing_fade: + existing_fade.queue_free() + + # Create CanvasLayer with z_index 999 (below level complete UI which is 1000) + var fade_layer = CanvasLayer.new() + fade_layer.name = "BlackFadeOverlay" + fade_layer.layer = 999 # Below level complete UI (1000) but above gameplay + add_child(fade_layer) + + # Create ColorRect that fills the screen + var fade_rect = ColorRect.new() + fade_rect.name = "FadeRect" + fade_rect.color = Color(0, 0, 0, 1) # Black, fully opaque + fade_rect.set_anchors_preset(Control.PRESET_FULL_RECT) # Fill entire screen + fade_layer.add_child(fade_rect) + + # Fade in from transparent to black + fade_rect.modulate.a = 0.0 # Start transparent + var fade_tween = create_tween() + fade_tween.tween_property(fade_rect, "modulate:a", 1.0, 0.5) # Fade in over 0.5 seconds + print("GameWorld: Created black fade overlay for player who reached exit") + +func _remove_black_fade_overlay(): + # Remove black fade overlay when new level starts + var existing_fade = get_node_or_null("BlackFadeOverlay") + if existing_fade: + # Fade out quickly before removing + var fade_rect = existing_fade.get_node_or_null("FadeRect") + if fade_rect: + var fade_tween = create_tween() + fade_tween.tween_property(fade_rect, "modulate:a", 0.0, 0.2) # Fade out over 0.2 seconds + await fade_tween.finished + existing_fade.queue_free() + print("GameWorld: Removed black fade overlay") + +func _restore_player_controls_and_collision(): + # Restore controls and collision for all players when new level starts + var players = get_tree().get_nodes_in_group("player") + for player in players: + player.controls_disabled = false + # Restore collision layer (layer 1 = players) + player.set_collision_layer_value(1, true) + print("GameWorld: Restored controls and collision for player ", player.name) + func _show_level_complete_ui(level_time: float = 0.0): # Create or show level complete UI var level_complete_ui = get_node_or_null("LevelCompleteUI") @@ -1894,6 +2034,7 @@ func _create_level_complete_ui_programmatically() -> Node: # Create level complete UI programmatically var canvas_layer = CanvasLayer.new() canvas_layer.name = "LevelCompleteUI" + canvas_layer.layer = 1000 # Very high z_index so it appears above black fade add_child(canvas_layer) # Load standard font (as FontFile) diff --git a/src/scripts/inspiration_scripts/pot.tscn30323093389.tmp b/src/scripts/inspiration_scripts/pot.tscn30323093389.tmp new file mode 100644 index 0000000..003ecda --- /dev/null +++ b/src/scripts/inspiration_scripts/pot.tscn30323093389.tmp @@ -0,0 +1,152 @@ +[gd_scene load_steps=21 format=3 uid="uid://bdlg5orah64m5"] + +[ext_resource type="Script" uid="uid://bj0ueurl3vovc" path="res://scripts/entities/world/pot.gd" id="1_hsjxb"] +[ext_resource type="Texture2D" uid="uid://bu4dq78f8lgj5" path="res://assets/gfx/sheet_18.png" id="1_rxnv2"] +[ext_resource type="AudioStream" uid="uid://fl0rfi4in3n4" path="res://assets/audio/sfx/environment/pot/Drunk lad destroys plant pot.mp3" id="3_vktry"] +[ext_resource type="AudioStream" uid="uid://dejjc0uqthi1b" path="res://assets/audio/sfx/environment/pot/pot_destroy_sound1.mp3" id="4_nb533"] +[ext_resource type="AudioStream" uid="uid://iuxunaogc8xr" path="res://assets/audio/sfx/environment/pot/pot_destroy_sound2.mp3" id="5_cmff4"] +[ext_resource type="AudioStream" uid="uid://bfqusej0pbxem" path="res://assets/audio/sfx/environment/pot/pot_destroy_sound3.mp3" id="6_lq20m"] +[ext_resource type="AudioStream" uid="uid://dq461vpiih3lc" path="res://assets/audio/sfx/environment/pot/pot_destroy_sound4.mp3" id="7_76fyq"] +[ext_resource type="AudioStream" uid="uid://cg1ndvx4t7xtd" path="res://assets/audio/sfx/environment/pot/pot_destroy_sound5.mp3" id="8_m11t2"] +[ext_resource type="AudioStream" uid="uid://bt5npaenq15h2" path="res://assets/audio/sfx/environment/pot/smaller_pot_crash.mp3" id="9_sb38x"] +[ext_resource type="Texture2D" uid="uid://b1twy68vd7f20" path="res://assets/gfx/pickups/indicator.png" id="10_nb533"] + +[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_hsjxb"] +properties/0/path = NodePath(".:position") +properties/0/spawn = true +properties/0/replication_mode = 2 + +[sub_resource type="Gradient" id="Gradient_nb533"] +offsets = PackedFloat32Array(0.847255, 0.861575) +colors = PackedColorArray(0, 0, 0, 0.764706, 0, 0, 0, 0) + +[sub_resource type="GradientTexture2D" id="GradientTexture2D_87nuj"] +gradient = SubResource("Gradient_nb533") +width = 16 +height = 6 +fill = 1 +fill_from = Vector2(0.504274, 0.478632) +fill_to = Vector2(0.897436, 0.0769231) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_hsjxb"] +size = Vector2(12, 8) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_87nuj"] +size = Vector2(18, 15) + +[sub_resource type="AudioStreamRandomizer" id="AudioStreamRandomizer_ui3li"] +streams_count = 7 +stream_0/stream = ExtResource("3_vktry") +stream_1/stream = ExtResource("4_nb533") +stream_2/stream = ExtResource("5_cmff4") +stream_3/stream = ExtResource("6_lq20m") +stream_4/stream = ExtResource("7_76fyq") +stream_5/stream = ExtResource("8_m11t2") +stream_6/stream = ExtResource("9_sb38x") + +[sub_resource type="Animation" id="Animation_cmff4"] +resource_name = "indicate" +length = 0.8 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:offset") +tracks/0/interp = 2 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.4, 0.8), +"transitions": PackedFloat32Array(1, 1, 1), +"update": 0, +"values": [Vector2(0, 0), Vector2(0, -1), Vector2(0, 0)] +} + +[sub_resource type="Animation" id="Animation_lq20m"] +length = 0.001 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath(".:offset") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector2(0, 0)] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_76fyq"] +_data = { +&"RESET": SubResource("Animation_lq20m"), +&"indicate": SubResource("Animation_cmff4") +} + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_nb533"] +size = Vector2(14, 9) + +[node name="Pot" type="CharacterBody2D"] +collision_layer = 128 +collision_mask = 64 +script = ExtResource("1_hsjxb") + +[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."] +replication_config = SubResource("SceneReplicationConfig_hsjxb") + +[node name="Sprite2DShadow" type="Sprite2D" parent="."] +position = Vector2(0, 3) +texture = SubResource("GradientTexture2D_87nuj") + +[node name="Sprite2D" type="Sprite2D" parent="."] +position = Vector2(0, -4) +texture = ExtResource("1_rxnv2") +hframes = 19 +vframes = 19 +frame = 14 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +position = Vector2(0, -1) +shape = SubResource("RectangleShape2D_hsjxb") + +[node name="Area2DPickup" type="Area2D" parent="."] +collision_layer = 1024 +collision_mask = 512 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2DPickup"] +position = Vector2(0, -1) +shape = SubResource("RectangleShape2D_87nuj") +debug_color = Color(0.688142, 0.7, 0.0440007, 0.42) + +[node name="SfxShatter" type="AudioStreamPlayer2D" parent="."] +stream = SubResource("AudioStreamRandomizer_ui3li") +attenuation = 9.84915 +panning_strength = 1.46 +bus = &"Sfx" + +[node name="SfxThrow" type="AudioStreamPlayer2D" parent="."] + +[node name="SfxDrop" type="AudioStreamPlayer2D" parent="."] + +[node name="Indicator" type="Sprite2D" parent="."] +position = Vector2(0, -11) +texture = ExtResource("10_nb533") + +[node name="AnimationPlayer" type="AnimationPlayer" parent="Indicator"] +libraries = { +&"": SubResource("AnimationLibrary_76fyq") +} +autoplay = "indicate" + +[node name="Area2DCollision" type="Area2D" parent="."] +collision_layer = 1024 +collision_mask = 704 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2DCollision"] +position = Vector2(0, -1) +shape = SubResource("RectangleShape2D_nb533") +debug_color = Color(0.7, 0.132592, 0.232379, 0.42) + +[connection signal="body_entered" from="Area2DPickup" to="." method="_on_area_2d_pickup_body_entered"] +[connection signal="body_exited" from="Area2DPickup" to="." method="_on_area_2d_pickup_body_exited"] +[connection signal="body_entered" from="Area2DCollision" to="." method="_on_area_2d_collision_body_entered"] +[connection signal="body_exited" from="Area2DCollision" to="." method="_on_area_2d_collision_body_exited"] diff --git a/src/scripts/interactable_object.gd b/src/scripts/interactable_object.gd index 5aef0a0..d455d1b 100644 --- a/src/scripts/interactable_object.gd +++ b/src/scripts/interactable_object.gd @@ -140,6 +140,9 @@ func _land(): tween.tween_property(sprite, "scale", Vector2(1.0, 1.0), 0.1) print(name, " landed!") + $SfxLand.play() + $DragParticles.emitting = true + $DragParticles/TimerSmokeParticles.start() func _handle_air_collision(): # Handle collision while airborne @@ -273,11 +276,13 @@ func _break_into_pieces(): play_destroy_sound() self.set_deferred("collision_layer", 0) - self.visible = false + $Shadow.visible = false + $Sprite2DAbove.visible = false + $Sprite2D.visible = false if ($SfxShatter.playing): await $SfxShatter.finished if ($SfxBreakCrate.playing): - await $SfxShatter.finished + await $SfxBreakCrate.finished # Remove self queue_free() @@ -652,6 +657,8 @@ func play_destroy_sound(): $SfxShatter.play() else: $SfxBreakCrate.play() + $DragParticles.emitting = true + $DragParticles/TimerSmokeParticles.start() pass func play_drag_sound(): @@ -661,9 +668,16 @@ func play_drag_sound(): else: if !$SfxDragRock.playing: $SfxDragRock.play() + $DragParticles.emitting = true pass func stop_drag_sound(): $SfxDrag.stop() $SfxDragRock.stop() + $DragParticles.emitting = false pass + + +func _on_timer_smoke_particles_timeout() -> void: + $DragParticles.emitting = false + pass # Replace with function body. diff --git a/src/scripts/loot.gd b/src/scripts/loot.gd index bc27d5b..993ffe0 100644 --- a/src/scripts/loot.gd +++ b/src/scripts/loot.gd @@ -552,7 +552,7 @@ func _sync_remove(): queue_free() @rpc("any_peer", "reliable") -func _sync_show_floating_text(loot_type_value: int, text: String, color_value: Color, value: int, sprite_frame_value: int, player_peer_id: int): +func _sync_show_floating_text(loot_type_value: int, text: String, color_value: Color, _value: int, sprite_frame_value: int, player_peer_id: int): # Client receives floating text sync from server if multiplayer.is_server(): return # Server ignores this (it's the sender) diff --git a/src/scripts/player.gd b/src/scripts/player.gd index 314766b..71c791e 100644 --- a/src/scripts/player.gd +++ b/src/scripts/player.gd @@ -40,6 +40,9 @@ var initial_player_position = Vector2.ZERO # Position of player when first grabb var object_blocked_by_wall = false # True if pushed object is blocked by a wall var was_dragging_last_frame = false # Track if we were dragging last frame to detect start/stop +# Level complete state +var controls_disabled: bool = false # True when player has reached exit and controls should be disabled + # Being held state var being_held_by: Node = null var struggle_time: float = 0.0 @@ -57,6 +60,7 @@ var attack_cooldown: float = 0.0 # No cooldown - instant attacks! var is_attacking: bool = false var sword_slash_scene = preload("res://scenes/sword_slash.tscn") # Old rotating version (kept for reference) var sword_projectile_scene = preload("res://scenes/sword_projectile.tscn") # New projectile version +var blood_scene = preload("res://scenes/blood_clot.tscn") # Simulated Z-axis for height (when thrown) var position_z: float = 0.0 @@ -737,6 +741,11 @@ func _physics_process(delta): if is_dead: return + # Skip input if controls are disabled (e.g., when player reached exit) + if controls_disabled: + velocity = velocity.lerp(Vector2.ZERO, delta * 8.0) # Slow down movement + return + # Handle knockback timer if is_knocked_back: knockback_time += delta @@ -2186,6 +2195,19 @@ func _die(): # Play death sound effect if sfx_die: + for i in 12: + var angle = randf_range(0, TAU) + var speed = randf_range(50, 100) + var initial_velocityZ = randf_range(50, 90) + var b = blood_scene.instantiate() as CharacterBody2D + b.scale = Vector2(randf_range(0.3, 2), randf_range(0.3, 2)) + b.global_position = global_position + + # Set initial velocities from the synchronized data + var direction = Vector2.from_angle(angle) + b.velocity = direction * speed + b.velocityZ = initial_velocityZ + get_parent().call_deferred("add_child", b) sfx_die.play() # Play DIE animation @@ -2419,20 +2441,26 @@ func add_coins(amount: int): # Sync coins to client if this is server-side coin collection # (e.g., when loot is collected on server, sync to client) - if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and not is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): - # Server is adding coins to a client's player - sync to the client - var peer_id = get_multiplayer_authority() - if peer_id != 0: - _sync_stats_update.rpc_id(peer_id, character_stats.kills, character_stats.coin) + if multiplayer.has_multiplayer_peer() and multiplayer.is_server() and can_send_rpcs and is_inside_tree(): + # Server is adding coins to a player - sync to the client if it's a client player + var the_peer_id = get_multiplayer_authority() + # Only sync if this is a client player (not server's own player) + if the_peer_id != 0 and the_peer_id != multiplayer.get_unique_id(): + print(name, " syncing stats to client peer_id=", the_peer_id, " kills=", character_stats.kills, " coins=", character_stats.coin) + _sync_stats_update.rpc_id(the_peer_id, character_stats.kills, character_stats.coin) else: coins += amount print(name, " picked up ", amount, " coin(s)! Total coins: ", coins) -@rpc("authority", "reliable") +@rpc("any_peer", "reliable") func _sync_stats_update(kills_count: int, coins_count: int): # Client receives stats update from server (for kills and coins) # Update local stats to match server + # Only process on client (not on server where the update originated) + if multiplayer.is_server(): + return # Server ignores this (it's the sender) + if character_stats: character_stats.kills = kills_count character_stats.coin = coins_count