feat(progression): XP, niveaux et popups flottants (+N XP)

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>
This commit is contained in:
2026-04-20 18:08:26 +00:00
parent 982a4ec23d
commit 984754183f
7 changed files with 260 additions and 2 deletions

View File

@@ -0,0 +1,67 @@
extends Node
signal xp_gained(amount: int, source: String, world_position: Vector3)
signal xp_changed(current_xp: int, xp_for_next: int, level: int)
signal level_up(new_level: int)
const XP_BREAK_DEFAULT: int = 2
const XP_BREAK_BY_BLOCK: Dictionary = {
2: 1, # sand
3: 2, # rock
4: 5, # coral rouge
5: 5, # coral bleu
6: 1, # kelp
7: 8, # epave
8: 3, # glace
}
const XP_PEARL: int = 25
const XP_CRAFT: int = 10
const XP_ECHOLOCATE: int = 3
const XP_KILL_HOSTILE: int = 15
const XP_DEPTH_MILESTONE: int = 20
var current_xp: int = 0
var level: int = 1
var _reached_depths: Array[int] = []
func _ready() -> void:
emit_state()
func xp_for_level(lv: int) -> int:
return 50 + (lv - 1) * 40 + (lv - 1) * (lv - 1) * 10
func xp_for_next() -> int:
return xp_for_level(level)
func award(amount: int, source: String = "", world_pos: Vector3 = Vector3.ZERO) -> void:
if amount <= 0:
return
current_xp += amount
xp_gained.emit(amount, source, world_pos)
while current_xp >= xp_for_next():
current_xp -= xp_for_next()
level += 1
level_up.emit(level)
emit_state()
func award_block_break(block_id: int, world_pos: Vector3) -> void:
var gain: int = XP_BREAK_BY_BLOCK.get(block_id, XP_BREAK_DEFAULT)
award(gain, "bloc", world_pos)
func emit_state() -> void:
xp_changed.emit(current_xp, xp_for_next(), level)
func note_depth(depth_m: int) -> void:
var milestones: Array[int] = [10, 25, 50, 75, 100]
for m: int in milestones:
if depth_m >= m and not _reached_depths.has(m):
_reached_depths.append(m)
award(XP_DEPTH_MILESTONE, "profondeur %dm" % m, Vector3.ZERO)

View File

@@ -0,0 +1,42 @@
extends Node3D
var _label: Label3D = null
var _elapsed: float = 0.0
var _lifetime: float = 1.4
var _drift: float = 1.6
static func spawn(parent: Node, text: String, world_pos: Vector3, color: Color = Color(1.0, 0.9, 0.2)) -> void:
var popup := Node3D.new()
popup.set_script(load("res://scripts/progression/XpPopup.gd"))
parent.add_child(popup)
popup.global_position = world_pos
popup.configure(text, color)
func configure(text: String, color: Color) -> void:
_label = Label3D.new()
_label.text = text
_label.font_size = 54
_label.outline_size = 6
_label.modulate = color
_label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
_label.no_depth_test = true
_label.render_priority = 4
_label.pixel_size = 0.008
add_child(_label)
func _process(delta: float) -> void:
_elapsed += delta
var t: float = _elapsed / _lifetime
position.y += _drift * delta
if _label != null:
var alpha: float = 1.0 - clampf(t, 0.0, 1.0)
var c: Color = _label.modulate
c.a = alpha
_label.modulate = c
var scale_factor: float = 1.0 + (1.0 - alpha) * 0.4
_label.scale = Vector3.ONE * scale_factor
if _elapsed >= _lifetime:
queue_free()