diff --git a/src/scenes/floating_text.tscn b/src/scenes/floating_text.tscn index b710dec..d0cf827 100644 --- a/src/scenes/floating_text.tscn +++ b/src/scenes/floating_text.tscn @@ -1,27 +1,23 @@ -[gd_scene format=3 uid="uid://floating_text"] +[gd_scene format=3 uid="uid://dhhejfqlmhv52"] -[ext_resource type="Script" path="res://scripts/floating_text.gd" id="1"] +[ext_resource type="Script" uid="uid://dx5oym20rr2ei" path="res://scripts/floating_text.gd" id="1"] [ext_resource type="FontFile" uid="uid://cbmcfue0ek0tk" path="res://assets/fonts/dmg_numbers.png" id="2_dmg_font"] [sub_resource type="Theme" id="Theme_floating_text"] default_font = ExtResource("2_dmg_font") default_font_size = 12 -[node name="FloatingText" type="Node2D"] +[node name="FloatingText" type="Node2D" unique_id=1350559946] script = ExtResource("1") -[node name="ItemSprite" type="Sprite2D" parent="."] +[node name="ItemSprite" type="Sprite2D" parent="." unique_id=1657362510] visible = false -offset = Vector2(0, -20) -scale = Vector2(1, 1) +offset = Vector2(0, -8) -[node name="Label" type="Label" parent="."] +[node name="Label" type="Label" parent="." unique_id=1387220833] offset_right = 64.0 offset_bottom = 24.0 theme = SubResource("Theme_floating_text") text = "+1 coin" horizontal_alignment = 1 vertical_alignment = 1 -outline_size = 2 -outline_color = Color(0, 0, 0, 1) - diff --git a/src/scripts/enemy_slime.gd b/src/scripts/enemy_slime.gd index 48006bb..f8448fb 100644 --- a/src/scripts/enemy_slime.gd +++ b/src/scripts/enemy_slime.gd @@ -13,14 +13,44 @@ var detection_range: float = 70.0 # Range to detect players (much smaller) # Jump mechanics var is_jumping: bool = false -var jump_anim_frames = [2, 3, 4, 5, 7, 2] # Jump animation sequence -var jump_anim_index: int = 0 -# Animation frames -const FRAME_IDLE = 0 -const FRAMES_MOVE = [0, 1, 2] # Slow move -const FRAMES_DAMAGE = [8, 9] -const FRAMES_DEATH = [8, 9, 10, 11, 12, 13, 14] +# Animation system (similar to player) +const ANIMATIONS = { + "IDLE": { + "frames": [0], + "frameDurations": [500], + "loop": true, + "nextAnimation": null + }, + "MOVE": { + "frames": [0, 1, 2], + "frameDurations": [200, 200, 200], + "loop": true, + "nextAnimation": null + }, + "JUMP": { + "frames": [2, 3, 4, 5, 7, 2], + "frameDurations": [100, 100, 100, 100, 100, 100], + "loop": false, + "nextAnimation": "MOVE" + }, + "DAMAGE": { + "frames": [8, 9], + "frameDurations": [150, 150], + "loop": false, + "nextAnimation": "IDLE" + }, + "DIE": { + "frames": [8, 9, 10, 11, 12, 13, 14], + "frameDurations": [70, 70, 70, 70, 70, 70, 200], + "loop": false, + "nextAnimation": null + } +} + +var current_animation = "IDLE" +var current_frame = 0 +var time_since_last_frame = 0.0 func _ready(): super._ready() @@ -71,7 +101,7 @@ func _ai_behavior(delta): func _idle_behavior(_delta): velocity = Vector2.ZERO - anim_frame = FRAME_IDLE + _set_animation("IDLE") # Check if player is nearby if target_player: @@ -94,6 +124,8 @@ func _idle_behavior(_delta): state_timer = move_duration func _moving_behavior(_delta): + _set_animation("MOVE") + # Move slowly towards player if target_player and is_instance_valid(target_player): var direction = (target_player.global_position - global_position).normalized() @@ -117,9 +149,8 @@ func _moving_behavior(_delta): func _start_jump(): state = SlimeState.JUMPING is_jumping = true - jump_anim_index = 0 state_timer = 0.6 # Jump duration - anim_time = 0.0 + _set_animation("JUMP") # Jump towards player if nearby if target_player and is_instance_valid(target_player): @@ -142,57 +173,46 @@ func _jumping_behavior(_delta): func _damaged_behavior(_delta): velocity = Vector2.ZERO + _set_animation("DAMAGE") # Stay in damaged state briefly if state_timer <= 0: state = SlimeState.IDLE state_timer = idle_duration +func _set_animation(anim_name: String): + if current_animation != anim_name: + current_animation = anim_name + current_frame = 0 + time_since_last_frame = 0.0 + func _update_animation(delta): - if state == SlimeState.DYING or state == SlimeState.DAMAGED: - return # Animation handled elsewhere + # Update animation frame timing + time_since_last_frame += delta + if time_since_last_frame >= ANIMATIONS[current_animation]["frameDurations"][current_frame] / 1000.0: + current_frame += 1 + if current_frame >= len(ANIMATIONS[current_animation]["frames"]): + current_frame -= 1 # Prevent out of bounds + if ANIMATIONS[current_animation]["loop"]: + current_frame = 0 + elif ANIMATIONS[current_animation]["nextAnimation"] != null: + current_frame = 0 + current_animation = ANIMATIONS[current_animation]["nextAnimation"] + time_since_last_frame = 0.0 - if state == SlimeState.IDLE: - anim_frame = FRAME_IDLE - elif state == SlimeState.JUMPING: - # Animate jump sequence - anim_time += delta - if anim_time >= 0.1: # Fast jump animation - anim_time = 0.0 - jump_anim_index += 1 - if jump_anim_index < jump_anim_frames.size(): - anim_frame = jump_anim_frames[jump_anim_index] - elif state == SlimeState.MOVING: - # Animate slow move (frames 0, 1, 2) - anim_time += delta - if anim_time >= anim_speed: - anim_time = 0.0 - var move_index = FRAMES_MOVE.find(anim_frame) - if move_index == -1: - move_index = 0 - else: - move_index = (move_index + 1) % FRAMES_MOVE.size() - anim_frame = FRAMES_MOVE[move_index] + # Calculate frame index + var frame_index = ANIMATIONS[current_animation]["frames"][current_frame] # Set sprite frame (slime looks same in all directions) if sprite: - sprite.frame = anim_frame + sprite.frame = frame_index + anim_frame = frame_index # Keep anim_frame updated for compatibility func _on_take_damage(): # Play damage animation state = SlimeState.DAMAGED state_timer = 0.3 - anim_time = 0.0 - - # Animate damage frames - _play_damage_anim() - -func _play_damage_anim(): - for frame in FRAMES_DAMAGE: - anim_frame = frame - if sprite: - sprite.frame = frame - await get_tree().create_timer(0.1).timeout + _set_animation("DAMAGE") func _die(): if is_dead: @@ -204,6 +224,7 @@ func _die(): # Set state before calling parent _die() state = SlimeState.DYING velocity = Vector2.ZERO + _set_animation("DIE") # Call parent _die() which handles death sync and _play_death_animation() super._die() @@ -220,12 +241,13 @@ func _update_client_visuals(): sprite.frame = anim_frame func _play_death_animation(): - # Play death animation sequence - for frame in FRAMES_DEATH: - anim_frame = frame - if sprite: - sprite.frame = frame - await get_tree().create_timer(0.15).timeout + _set_animation("DIE") + + # Wait for death animation to complete + var total_duration = 0.0 + for duration in ANIMATIONS["DIE"]["frameDurations"]: + total_duration += duration / 1000.0 + await get_tree().create_timer(total_duration).timeout # Fade out if sprite: diff --git a/src/scripts/floating_text.gd b/src/scripts/floating_text.gd index 603729b..6128fb2 100644 --- a/src/scripts/floating_text.gd +++ b/src/scripts/floating_text.gd @@ -22,6 +22,7 @@ func setup(text_value: String, text_color: Color, show_time: float = 0.5, fade_t label.text = text label.modulate = color label.modulate.a = 1.0 # Start fully visible + label.z_index = 10 # Render above other objects # Setup item sprite if texture provided if item_sprite and item_texture: @@ -32,9 +33,10 @@ func setup(text_value: String, text_color: Color, show_time: float = 0.5, fade_t item_sprite.frame = sprite_frame item_sprite.modulate = Color.WHITE item_sprite.modulate.a = 1.0 + item_sprite.z_index = 10 # Render above other objects # Position sprite above label (if label exists) or centered if label: - item_sprite.position = Vector2(0, -24) # Above the text (sprite is ~16px tall) + item_sprite.position = Vector2(0, -12) # Just above the text (sprite is ~16px tall) else: item_sprite.position = Vector2(0, 0) @@ -96,4 +98,3 @@ func _animate_coin(): await get_tree().create_timer(coin_frame_time).timeout if item_sprite and is_instance_valid(item_sprite) and item_sprite.visible: item_sprite.frame = target_frame - diff --git a/src/scripts/game_world.gd b/src/scripts/game_world.gd index 8345d39..cab3de0 100644 --- a/src/scripts/game_world.gd +++ b/src/scripts/game_world.gd @@ -2634,9 +2634,10 @@ func _place_key_in_room(room: Dictionary): valid_positions.append(Vector2(world_x, world_y)) if valid_positions.size() > 0: - # Pick a random position + # Use deterministic seed for key placement (ensures same position on host and clients) var rng = RandomNumberGenerator.new() - rng.randomize() + var key_seed = dungeon_seed + (room.x * 1000) + (room.y * 100) + 5000 # Offset to avoid collisions with other objects + rng.seed = key_seed var key_pos = valid_positions[rng.randi() % valid_positions.size()] # Spawn key loot diff --git a/src/scripts/interactable_object.gd b/src/scripts/interactable_object.gd index 032244f..5aef0a0 100644 --- a/src/scripts/interactable_object.gd +++ b/src/scripts/interactable_object.gd @@ -530,21 +530,21 @@ func _open_chest(by_player: Node = null): by_player.heal(heal_amount) # Show pickup notification with apple graphic var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") - _show_item_pickup_notification(by_player, "+" + str(int(heal_amount)) + " HP", selected_loot.color, items_texture, 20, 14, (8 * 20) + 11) + _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) + 12) + _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) + 13) + _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) @@ -562,7 +562,8 @@ func _open_chest(by_player: Node = null): # Sync chest opening visual to all clients (item already given on server) if multiplayer.has_multiplayer_peer(): - _sync_chest_open.rpc(selected_loot.type if by_player else "coin") + var player_peer_id = by_player.get_multiplayer_authority() if by_player else 0 + _sync_chest_open.rpc(selected_loot.type if by_player else "coin", player_peer_id) @rpc("any_peer", "reliable") func _request_chest_open(player_peer_id: int): @@ -593,7 +594,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"): +func _sync_chest_open(loot_type_str: String = "coin", player_peer_id: int = 0): # 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 @@ -602,6 +603,37 @@ func _sync_chest_open(_loot_type_str: String = "coin"): # Play chest open sound on clients if has_node("SfxChestOpen"): $SfxChestOpen.play() + + # Show pickup notification on client side + if player_peer_id > 0: + var players = get_tree().get_nodes_in_group("player") + var player = null + for p in players: + if p.get_multiplayer_authority() == player_peer_id: + player = p + break + + if player and is_instance_valid(player): + # Show notification based on loot type (same as server) + match loot_type_str: + "coin": + var coin_texture = load("res://assets/gfx/pickups/gold_coin.png") + _show_item_pickup_notification(player, "+1 COIN", Color(1.0, 0.84, 0.0), coin_texture, 6, 1, 0) + "apple": + var heal_amount = 20.0 + var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") + _show_item_pickup_notification(player, "+" + str(int(heal_amount)) + " HP", Color.GREEN, items_texture, 20, 14, (8 * 20) + 10) + "banana": + var heal_amount = 20.0 + var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") + _show_item_pickup_notification(player, "+" + str(int(heal_amount)) + " HP", Color.YELLOW, items_texture, 20, 14, (8 * 20) + 11) + "cherry": + var heal_amount = 20.0 + var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") + _show_item_pickup_notification(player, "+" + str(int(heal_amount)) + " HP", Color.RED, items_texture, 20, 14, (8 * 20) + 12) + "key": + var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") + _show_item_pickup_notification(player, "+1 KEY", Color.YELLOW, items_texture, 20, 14, (13 * 20) + 10) func _show_item_pickup_notification(player: Node, text: String, text_color: Color, item_texture: Texture2D = null, sprite_hframes: int = 1, sprite_vframes: int = 1, sprite_frame: int = 0): # Show item graphic and text above player's head for 0.5s, then fade out over 0.5s @@ -611,7 +643,8 @@ func _show_item_pickup_notification(player: Node, text: String, text_color: Colo var parent = player.get_parent() if parent: parent.add_child(floating_text) - floating_text.global_position = player.global_position + Vector2(0, -20) + # Position at player.position.y - 20 (just above head) + floating_text.global_position = Vector2(player.global_position.x, player.global_position.y - 20) floating_text.setup(text, text_color, 0.5, 0.5, item_texture, sprite_hframes, sprite_vframes, sprite_frame) # Show for 0.5s, fade over 0.5s func play_destroy_sound(): diff --git a/src/scripts/loot.gd b/src/scripts/loot.gd index b02fc51..d74cf3d 100644 --- a/src/scripts/loot.gd +++ b/src/scripts/loot.gd @@ -381,7 +381,7 @@ func _process_pickup_on_server(player: Node): player.heal(heal_amount) # Show floating text with item graphic and heal amount var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") - _show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 11) + _show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 10) self.visible = false @@ -399,7 +399,7 @@ func _process_pickup_on_server(player: Node): player.heal(heal_amount) # Show floating text with item graphic and heal amount var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") - _show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 12) + _show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 11) self.visible = false @@ -417,7 +417,7 @@ func _process_pickup_on_server(player: Node): player.heal(heal_amount) # Show floating text with item graphic and heal amount var items_texture = load("res://assets/gfx/pickups/items_n_shit.png") - _show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 13) + _show_floating_text(player, "+" + str(int(actual_heal)) + " HP", Color.GREEN, 0.5, 0.5, items_texture, 20, 14, (8 * 20) + 12) self.visible = false @@ -545,5 +545,5 @@ func _show_floating_text(player: Node, text: String, color: Color, show_time: fl var parent = player.get_parent() if parent: parent.add_child(floating_text) - floating_text.global_position = player.global_position + Vector2(0, -20) + floating_text.global_position = Vector2(player.global_position.x, player.global_position.y) floating_text.setup(text, color, show_time, fade_time, item_texture, sprite_hframes, sprite_vframes, sprite_frame) diff --git a/src/scripts/player.gd b/src/scripts/player.gd index f854a41..314766b 100644 --- a/src/scripts/player.gd +++ b/src/scripts/player.gd @@ -2148,12 +2148,39 @@ func _die(): is_dead = true # Ensure flag is set velocity = Vector2.ZERO is_knocked_back = false - is_lifting = false - is_pushing = false - held_object = null - # Don't force release - let them carry the corpse - # We'll handle release during respawn + # CRITICAL: Release any held object/player BEFORE dying to restore their collision layers + if held_object: + var released_obj = held_object + held_object = null + is_lifting = false + is_pushing = false + grab_offset = Vector2.ZERO + push_axis = Vector2.ZERO + + # Re-enable collision for released object/player + if _is_box(released_obj): + released_obj.set_collision_layer_value(2, true) + released_obj.set_collision_mask_value(1, true) + released_obj.set_collision_mask_value(2, true) + if "is_being_held" in released_obj: + released_obj.is_being_held = false + if "held_by_player" in released_obj: + released_obj.held_by_player = null + elif _is_player(released_obj): + released_obj.set_collision_layer_value(1, true) + released_obj.set_collision_mask_value(1, true) + if released_obj.has_method("set_being_held"): + released_obj.set_being_held(false) + + # Sync release to network + if multiplayer.has_multiplayer_peer() and is_multiplayer_authority() and can_send_rpcs and is_inside_tree(): + _sync_release.rpc(released_obj.get_path()) + + print(name, " released ", released_obj.name, " on death") + else: + is_lifting = false + is_pushing = false print(name, " died!")