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:
@@ -14,6 +14,12 @@ var _depth_label: Label = null
|
||||
var _biome_label: Label = null
|
||||
var _compass_label: Label = null
|
||||
|
||||
var _xp_panel: PanelContainer = null
|
||||
var _xp_bar: ProgressBar = null
|
||||
var _xp_label: Label = null
|
||||
var _level_banner: Label = null
|
||||
var _level_banner_timer: float = 0.0
|
||||
|
||||
var _biome_cache_pos: Vector3 = Vector3(99999, 99999, 99999)
|
||||
var _biome_cached_name: String = ""
|
||||
|
||||
@@ -23,6 +29,14 @@ func _ready() -> void:
|
||||
_style_bar(_health_bar, Color(0.91, 0.30, 0.24))
|
||||
_style_bar(_hunger_bar, Color(0.95, 0.61, 0.07))
|
||||
_build_info_panel()
|
||||
_build_xp_panel()
|
||||
_build_level_banner()
|
||||
if Engine.has_singleton("PlayerProgress") or has_node("/root/PlayerProgress"):
|
||||
var pp: Node = get_node_or_null("/root/PlayerProgress")
|
||||
if pp != null:
|
||||
pp.xp_changed.connect(_on_xp_changed)
|
||||
pp.level_up.connect(_on_level_up)
|
||||
_on_xp_changed(pp.current_xp, pp.xp_for_next(), pp.level)
|
||||
|
||||
|
||||
func _build_info_panel() -> void:
|
||||
@@ -72,6 +86,93 @@ func _build_info_panel() -> void:
|
||||
add_child(_info_panel)
|
||||
|
||||
|
||||
func _build_xp_panel() -> void:
|
||||
_xp_panel = PanelContainer.new()
|
||||
_xp_panel.anchor_left = 0.5
|
||||
_xp_panel.anchor_right = 0.5
|
||||
_xp_panel.anchor_top = 1.0
|
||||
_xp_panel.anchor_bottom = 1.0
|
||||
_xp_panel.offset_left = -180.0
|
||||
_xp_panel.offset_right = 180.0
|
||||
_xp_panel.offset_top = -78.0
|
||||
_xp_panel.offset_bottom = -68.0
|
||||
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.03, 0.05, 0.09, 0.75)
|
||||
style.set_corner_radius_all(6)
|
||||
style.border_color = Color(1.0, 0.84, 0.2, 0.55)
|
||||
style.set_border_width_all(1)
|
||||
_xp_panel.add_theme_stylebox_override("panel", style)
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.add_theme_constant_override("separation", 0)
|
||||
_xp_panel.add_child(vbox)
|
||||
|
||||
_xp_label = Label.new()
|
||||
_xp_label.text = "Niv. 1 — 0 / 50"
|
||||
_xp_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_xp_label.add_theme_font_size_override("font_size", 11)
|
||||
_xp_label.add_theme_color_override("font_color", Color(1.0, 0.9, 0.4))
|
||||
vbox.add_child(_xp_label)
|
||||
|
||||
_xp_bar = ProgressBar.new()
|
||||
_xp_bar.custom_minimum_size = Vector2(340, 6)
|
||||
_xp_bar.max_value = 50.0
|
||||
_xp_bar.value = 0.0
|
||||
_xp_bar.show_percentage = false
|
||||
var fill := StyleBoxFlat.new()
|
||||
fill.bg_color = Color(1.0, 0.82, 0.25)
|
||||
fill.set_corner_radius_all(3)
|
||||
_xp_bar.add_theme_stylebox_override("fill", fill)
|
||||
var bg := StyleBoxFlat.new()
|
||||
bg.bg_color = Color(0.08, 0.08, 0.10, 0.8)
|
||||
bg.set_corner_radius_all(3)
|
||||
_xp_bar.add_theme_stylebox_override("background", bg)
|
||||
vbox.add_child(_xp_bar)
|
||||
|
||||
add_child(_xp_panel)
|
||||
|
||||
|
||||
func _build_level_banner() -> void:
|
||||
_level_banner = Label.new()
|
||||
_level_banner.anchor_left = 0.5
|
||||
_level_banner.anchor_right = 0.5
|
||||
_level_banner.anchor_top = 0.25
|
||||
_level_banner.anchor_bottom = 0.25
|
||||
_level_banner.offset_left = -200.0
|
||||
_level_banner.offset_right = 200.0
|
||||
_level_banner.offset_top = -28.0
|
||||
_level_banner.offset_bottom = 28.0
|
||||
_level_banner.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_level_banner.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
_level_banner.add_theme_font_size_override("font_size", 40)
|
||||
_level_banner.add_theme_color_override("font_color", Color(1.0, 0.95, 0.45))
|
||||
_level_banner.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
|
||||
_level_banner.add_theme_constant_override("outline_size", 6)
|
||||
_level_banner.modulate.a = 0.0
|
||||
add_child(_level_banner)
|
||||
|
||||
|
||||
func _on_xp_changed(current_xp: int, xp_for_next: int, level: int) -> void:
|
||||
if _xp_bar == null or _xp_label == null:
|
||||
return
|
||||
_xp_bar.max_value = float(xp_for_next)
|
||||
_xp_bar.value = float(current_xp)
|
||||
_xp_label.text = "Niv. %d — %d / %d" % [level, current_xp, xp_for_next]
|
||||
|
||||
|
||||
func _on_level_up(new_level: int) -> void:
|
||||
if _level_banner == null:
|
||||
return
|
||||
_level_banner.text = "⬆ NIVEAU %d" % new_level
|
||||
_level_banner.modulate.a = 1.0
|
||||
_level_banner_timer = 2.2
|
||||
var am: Node = get_node_or_null("/root/AudioManager")
|
||||
if am != null and is_instance_valid(_dolphin):
|
||||
if am.has_method("play_bubble_sfx"):
|
||||
am.call("play_bubble_sfx", _dolphin.global_position)
|
||||
|
||||
|
||||
func _style_bar(bar: ProgressBar, color: Color) -> void:
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = color
|
||||
@@ -107,7 +208,14 @@ func _on_stats_changed(oxygen: float, hp: float, hunger: float) -> void:
|
||||
_hunger_bar.value = hunger
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
func _process(delta: float) -> void:
|
||||
if _level_banner_timer > 0.0:
|
||||
_level_banner_timer -= delta
|
||||
if _level_banner != null:
|
||||
var a: float = clampf(_level_banner_timer / 0.6, 0.0, 1.0)
|
||||
_level_banner.modulate.a = a
|
||||
_level_banner.scale = Vector2.ONE * (1.0 + (1.0 - a) * 0.2)
|
||||
|
||||
if not is_instance_valid(_dolphin):
|
||||
return
|
||||
|
||||
@@ -115,6 +223,11 @@ func _process(_delta: float) -> void:
|
||||
if _depth_label != null:
|
||||
_depth_label.text = "Prof. %d m" % depth_m
|
||||
|
||||
if depth_m > 0:
|
||||
var pp: Node = get_node_or_null("/root/PlayerProgress")
|
||||
if pp != null and pp.has_method("note_depth"):
|
||||
pp.call("note_depth", depth_m)
|
||||
|
||||
if _compass_label != null:
|
||||
_compass_label.text = _compass_text(_dolphin.rotation.y)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user