feat(progression): combo de mining + multiplicateur XP
Nouvel autoload ComboTracker: chaque cassage de bloc dans les 1.8s du précédent étend le combo (+15% par palier, cap x3.0). Le multiplicateur s'applique au gain XP de base. HUD: panneau "COMBO xN (+X%)" animé (pop scale) à partir de x2, avec barre de temps restant qui se vide. Combo ≥5 au break → bonus XP final (3 * count) avec popup. Rétention: incite à chaîner les actions, récompense la dextérité et crée des pics d'intensité dans la boucle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -247,7 +247,12 @@ func _award_break_xp(block_id: int, hit_position: Vector3) -> void:
|
||||
var pp: Node = get_node_or_null("/root/PlayerProgress")
|
||||
if pp == null:
|
||||
return
|
||||
var gain: int = pp.XP_BREAK_BY_BLOCK.get(block_id, pp.XP_BREAK_DEFAULT)
|
||||
var base_gain: int = pp.XP_BREAK_BY_BLOCK.get(block_id, pp.XP_BREAK_DEFAULT)
|
||||
var combo: Node = get_node_or_null("/root/ComboTracker")
|
||||
var mult: float = 1.0
|
||||
if combo != null:
|
||||
mult = combo.bump()
|
||||
var gain: int = int(round(base_gain * mult))
|
||||
pp.award(gain, "bloc", hit_position)
|
||||
_spawn_xp_popup(gain, hit_position)
|
||||
var qm: Node = get_node_or_null("/root/QuestManager")
|
||||
|
||||
@@ -53,9 +53,14 @@ func _ready() -> void:
|
||||
_on_quests_updated()
|
||||
_build_toast_container()
|
||||
_build_consumable_hint()
|
||||
_build_combo_panel()
|
||||
var am: Node = get_node_or_null("/root/AchievementManager")
|
||||
if am != null:
|
||||
am.achievement_unlocked.connect(_on_achievement_unlocked)
|
||||
var combo: Node = get_node_or_null("/root/ComboTracker")
|
||||
if combo != null:
|
||||
combo.combo_changed.connect(_on_combo_changed)
|
||||
combo.combo_broken.connect(_on_combo_broken)
|
||||
|
||||
|
||||
func _build_info_panel() -> void:
|
||||
@@ -242,6 +247,91 @@ func _on_quest_completed(q: Dictionary) -> void:
|
||||
|
||||
var _consumable_hint: Label = null
|
||||
|
||||
var _combo_panel: PanelContainer = null
|
||||
var _combo_label: Label = null
|
||||
var _combo_bar: ProgressBar = null
|
||||
|
||||
|
||||
func _build_combo_panel() -> void:
|
||||
_combo_panel = PanelContainer.new()
|
||||
_combo_panel.anchor_left = 0.5
|
||||
_combo_panel.anchor_right = 0.5
|
||||
_combo_panel.anchor_top = 0.5
|
||||
_combo_panel.anchor_bottom = 0.5
|
||||
_combo_panel.offset_left = 120.0
|
||||
_combo_panel.offset_right = 320.0
|
||||
_combo_panel.offset_top = 30.0
|
||||
_combo_panel.offset_bottom = 80.0
|
||||
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.2, 0.08, 0.05, 0.85)
|
||||
style.border_color = Color(1.0, 0.55, 0.15)
|
||||
style.set_border_width_all(2)
|
||||
style.set_corner_radius_all(8)
|
||||
_combo_panel.add_theme_stylebox_override("panel", style)
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.add_theme_constant_override("separation", 2)
|
||||
_combo_panel.add_child(vbox)
|
||||
|
||||
_combo_label = Label.new()
|
||||
_combo_label.text = "COMBO x1"
|
||||
_combo_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_combo_label.add_theme_font_size_override("font_size", 20)
|
||||
_combo_label.add_theme_color_override("font_color", Color(1.0, 0.85, 0.3))
|
||||
_combo_label.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.9))
|
||||
_combo_label.add_theme_constant_override("outline_size", 4)
|
||||
vbox.add_child(_combo_label)
|
||||
|
||||
_combo_bar = ProgressBar.new()
|
||||
_combo_bar.max_value = 1.0
|
||||
_combo_bar.value = 0.0
|
||||
_combo_bar.custom_minimum_size = Vector2(180, 4)
|
||||
_combo_bar.show_percentage = false
|
||||
var fill := StyleBoxFlat.new()
|
||||
fill.bg_color = Color(1.0, 0.55, 0.15)
|
||||
fill.set_corner_radius_all(2)
|
||||
_combo_bar.add_theme_stylebox_override("fill", fill)
|
||||
var bg := StyleBoxFlat.new()
|
||||
bg.bg_color = Color(0.1, 0.05, 0.03, 0.8)
|
||||
bg.set_corner_radius_all(2)
|
||||
_combo_bar.add_theme_stylebox_override("background", bg)
|
||||
vbox.add_child(_combo_bar)
|
||||
|
||||
_combo_panel.visible = false
|
||||
add_child(_combo_panel)
|
||||
|
||||
|
||||
func _on_combo_changed(cnt: int, mult: float) -> void:
|
||||
if _combo_panel == null:
|
||||
return
|
||||
if cnt < 2:
|
||||
_combo_panel.visible = false
|
||||
return
|
||||
_combo_panel.visible = true
|
||||
_combo_label.text = "COMBO x%d (+%d%%)" % [cnt, int(round((mult - 1.0) * 100.0))]
|
||||
_combo_panel.scale = Vector2(1.15, 1.15)
|
||||
var tween := create_tween()
|
||||
tween.tween_property(_combo_panel, "scale", Vector2.ONE, 0.18).set_trans(Tween.TRANS_SINE)
|
||||
|
||||
|
||||
func _on_combo_broken(final_count: int) -> void:
|
||||
if final_count >= 5:
|
||||
var pp: Node = get_node_or_null("/root/PlayerProgress")
|
||||
if pp != null:
|
||||
var bonus: int = final_count * 3
|
||||
pp.award(bonus, "bonus combo x%d" % final_count, Vector3.ZERO)
|
||||
var main: Node = get_tree().get_first_node_in_group("main")
|
||||
if main != null and is_instance_valid(_dolphin) and main.has_method("_spawn_xp_popup"):
|
||||
main.call("_spawn_xp_popup", bonus, _dolphin.global_position + Vector3(0, 2.0, 0))
|
||||
|
||||
|
||||
func _update_combo_bar() -> void:
|
||||
var combo: Node = get_node_or_null("/root/ComboTracker")
|
||||
if combo == null or _combo_bar == null:
|
||||
return
|
||||
_combo_bar.value = combo.time_remaining_ratio()
|
||||
|
||||
|
||||
func _build_consumable_hint() -> void:
|
||||
_consumable_hint = Label.new()
|
||||
@@ -455,6 +545,7 @@ func _process(delta: float) -> void:
|
||||
|
||||
_update_toasts(delta)
|
||||
_update_consumable_hint()
|
||||
_update_combo_bar()
|
||||
|
||||
if not is_instance_valid(_dolphin):
|
||||
return
|
||||
|
||||
48
scripts/progression/ComboTracker.gd
Normal file
48
scripts/progression/ComboTracker.gd
Normal file
@@ -0,0 +1,48 @@
|
||||
extends Node
|
||||
|
||||
signal combo_changed(count: int, multiplier: float)
|
||||
signal combo_broken(final_count: int)
|
||||
|
||||
const WINDOW_SEC: float = 1.8
|
||||
const STEP_BONUS: float = 0.15 # +15% per combo step
|
||||
const MAX_MULT: float = 3.0
|
||||
|
||||
var count: int = 0
|
||||
var multiplier: float = 1.0
|
||||
var _timer: float = 0.0
|
||||
var _active: bool = false
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not _active:
|
||||
return
|
||||
_timer -= delta
|
||||
if _timer <= 0.0:
|
||||
_break()
|
||||
|
||||
|
||||
func bump() -> float:
|
||||
# Advances combo and returns multiplier to apply to the triggering gain.
|
||||
count += 1
|
||||
multiplier = minf(1.0 + float(count - 1) * STEP_BONUS, MAX_MULT)
|
||||
_timer = WINDOW_SEC
|
||||
_active = true
|
||||
combo_changed.emit(count, multiplier)
|
||||
return multiplier
|
||||
|
||||
|
||||
func _break() -> void:
|
||||
var final_count: int = count
|
||||
_active = false
|
||||
combo_changed.emit(0, 1.0)
|
||||
if final_count >= 2:
|
||||
combo_broken.emit(final_count)
|
||||
count = 0
|
||||
multiplier = 1.0
|
||||
_timer = 0.0
|
||||
|
||||
|
||||
func time_remaining_ratio() -> float:
|
||||
if not _active:
|
||||
return 0.0
|
||||
return clampf(_timer / WINDOW_SEC, 0.0, 1.0)
|
||||
Reference in New Issue
Block a user