extends Node3D @export var player: NodePath = NodePath("") @export var spawn_radius: float = 28.0 @export var despawn_radius: float = 55.0 @export var max_pearls: int = 3 const PEARL_SCENE: String = "res://scenes/world/Pearl.tscn" const SPAWN_INTERVAL: float = 9.0 var _player_node: CharacterBody3D = null var _chunk_manager: Node = null var _pearls: Array[Area3D] = [] var _timer: float = 0.0 func _ready() -> void: _resolve_player() _find_chunk_manager() func _resolve_player() -> void: if not player.is_empty(): var n: Node = get_node_or_null(player) if n is CharacterBody3D: _player_node = n if _player_node == null: var players := get_tree().get_nodes_in_group("player") if players.size() > 0: _player_node = players[0] as CharacterBody3D func _find_chunk_manager() -> void: var cm: Node = get_node_or_null("/root/Main/World/ChunkManager") if cm != null: _chunk_manager = cm func _physics_process(delta: float) -> void: _timer += delta if _timer < SPAWN_INTERVAL: return _timer = 0.0 _resolve_player() _tick() func _tick() -> void: if not is_instance_valid(_player_node): return _cull() if _pearls.size() >= max_pearls: return var pos: Vector3 = _random_spawn_pos() if not _is_valid_spawn(pos): return var packed: PackedScene = load(PEARL_SCENE) if packed == null: return var pearl: Area3D = packed.instantiate() as Area3D if pearl == null: return get_tree().current_scene.add_child(pearl) pearl.global_position = pos if pearl.has_signal("collected"): pearl.collected.connect(_on_pearl_collected) _pearls.append(pearl) func _random_spawn_pos() -> Vector3: var player_pos: Vector3 = _player_node.global_position var angle: float = randf_range(0.0, TAU) var dist: float = randf_range(12.0, spawn_radius) # Pearls float just above the seabed in the shallow-to-mid zone. var depth: float = randf_range(-22.0, 0.0) return Vector3( player_pos.x + cos(angle) * dist, depth, player_pos.z + sin(angle) * dist ) func _is_valid_spawn(pos: Vector3) -> bool: if _chunk_manager == null: return true if not _chunk_manager.has_method("get_block"): return true var block_id: int = _chunk_manager.call("get_block", pos) # Avoid spawning inside solid blocks (ids 2..9) if block_id >= 2 and block_id <= 9: return false return true func _cull() -> void: if not is_instance_valid(_player_node): return var ppos: Vector3 = _player_node.global_position var i: int = _pearls.size() - 1 while i >= 0: if not is_instance_valid(_pearls[i]): _pearls.remove_at(i) elif _pearls[i].global_position.distance_to(ppos) > despawn_radius: _pearls[i].queue_free() _pearls.remove_at(i) i -= 1 func _on_pearl_collected(item_id: int) -> void: var main: Node = get_tree().get_first_node_in_group("main") if main == null or not ("inventory" in main): return main.inventory.add_item(item_id, 1) var am: Node = get_node_or_null("/root/AudioManager") var player_pos: Vector3 = Vector3.ZERO if is_instance_valid(_player_node): player_pos = _player_node.global_position if am != null and am.has_method("play_bubble_sfx") and is_instance_valid(_player_node): am.call("play_bubble_sfx", player_pos) var pp: Node = get_node_or_null("/root/PlayerProgress") if pp != null: pp.award(pp.XP_PEARL, "perle", player_pos) if main.has_method("_spawn_xp_popup"): main.call("_spawn_xp_popup", pp.XP_PEARL, player_pos + Vector3(0, 1.2, 0)) var qm: Node = get_node_or_null("/root/QuestManager") if qm != null: qm.note_pearl_collected()