Nouvel autoload PlayerProgress qui gagne de l'XP sur: - cassage de blocs (gain variable selon le type: perle, épave, coral > sand) - collecte de perles (+25) - crafting (+10) - paliers de profondeur atteints (+20 à 10/25/50/75/100m) HUD: barre XP + label niveau en bas centre, bannière "NIVEAU X" au level-up (son bulle + fade). Popup 3D "+N XP" spawn au point d'action. Boucle court-terme dopamine: chaque action = feedback visuel immédiat. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
125 lines
3.3 KiB
GDScript
125 lines
3.3 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))
|