Nouvel autoload AchievementManager avec 12 succès (Premier éclat, Mineur confirmé, Chasseur de perles, Fouilleur d'épaves, Abysses, Légende des océans, etc.). Chaque déblocage donne un bonus XP et joue un son. HUD: toasts dorés avec icône + titre + desc + fade-in/out, empilés en haut centre. Son bulle au déblocage. Long-terme: cible des milestones cumulatifs qui donnent un sentiment de progression durable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
131 lines
3.5 KiB
GDScript
131 lines
3.5 KiB
GDScript
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()
|
|
var ach: Node = get_node_or_null("/root/AchievementManager")
|
|
if ach != null:
|
|
ach.note_pearl_collected()
|