diff --git a/Assets/Scripts/Runtime/Gameplay/GameplaySession.cs b/Assets/Scripts/Runtime/Gameplay/GameplaySession.cs
index 81a32b6..1807ea6 100644
--- a/Assets/Scripts/Runtime/Gameplay/GameplaySession.cs
+++ b/Assets/Scripts/Runtime/Gameplay/GameplaySession.cs
@@ -31,6 +31,11 @@ namespace DanroJump.Gameplay
public string SessionId { get; private set; } = CreateSessionId();
+ ///
+ /// Текущий комбо-множитель очков (увеличивается при сборе монет/врагах, сбрасывается на платформах).
+ ///
+ public int CurrentCombo { get; private set; } = 1;
+
///
/// Итоговые очки текущего забега: игровые начисления плюс бонус за максимальную высоту.
///
@@ -122,11 +127,33 @@ namespace DanroJump.Gameplay
{
this.player.Died += HandlePlayerDied;
this.player.Respawned += HandlePlayerRespawned;
+ this.player.LandedOnNormalPlatform += ResetCombo;
}
}
///
- /// Добавляет очки в текущий забег.
+ /// Увеличивает комбо-множитель.
+ ///
+ public void IncrementCombo()
+ {
+ CurrentCombo++;
+ Changed?.Invoke();
+ }
+
+ ///
+ /// Сбрасывает комбо-множитель в 1.
+ ///
+ public void ResetCombo()
+ {
+ if (CurrentCombo != 1)
+ {
+ CurrentCombo = 1;
+ Changed?.Invoke();
+ }
+ }
+
+ ///
+ /// Добавляет очки в текущий забег (с учетом комбо-множителя).
///
public void AddScore(int amount)
{
@@ -135,7 +162,8 @@ namespace DanroJump.Gameplay
return;
}
- baseScore += amount;
+ baseScore += amount * CurrentCombo;
+ CurrentCombo++;
Changed?.Invoke();
}
@@ -165,6 +193,7 @@ namespace DanroJump.Gameplay
DeathCount = 0;
RemainingLives = startingLives;
State = GameplaySessionState.Running;
+ CurrentCombo = 1;
player?.ResetRun();
Changed?.Invoke();
}
@@ -186,6 +215,7 @@ namespace DanroJump.Gameplay
sceneFlow?.CancelGameOverReturn();
RemainingLives = ContinueGameLives;
State = GameplaySessionState.Respawning;
+ CurrentCombo = 1;
if (player != null)
{
@@ -215,6 +245,7 @@ namespace DanroJump.Gameplay
player.Died -= HandlePlayerDied;
player.Respawned -= HandlePlayerRespawned;
+ player.LandedOnNormalPlatform -= ResetCombo;
}
///
@@ -226,6 +257,8 @@ namespace DanroJump.Gameplay
DeathCount++;
RemainingLives = Mathf.Max(0, RemainingLives - 1);
State = RemainingLives > 0 ? GameplaySessionState.Respawning : GameplaySessionState.GameOver;
+ CurrentCombo = 1;
+
if (State == GameplaySessionState.GameOver)
{
audioService?.Play(GameAudioEvent.GameOver);
diff --git a/Assets/Scripts/Runtime/Gameplay/IPlayerController.cs b/Assets/Scripts/Runtime/Gameplay/IPlayerController.cs
index b4f04a4..8e136fb 100644
--- a/Assets/Scripts/Runtime/Gameplay/IPlayerController.cs
+++ b/Assets/Scripts/Runtime/Gameplay/IPlayerController.cs
@@ -9,6 +9,7 @@ namespace DanroJump.Gameplay
{
event System.Action Died;
event System.Action Respawned;
+ event System.Action LandedOnNormalPlatform;
Transform Transform { get; }
float HighestY { get; }
diff --git a/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs b/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs
index e16798c..1462166 100644
--- a/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs
+++ b/Assets/Scripts/Runtime/Gameplay/JumpPlayerController.cs
@@ -186,6 +186,7 @@ namespace DanroJump.Gameplay
public event System.Action Died;
public event System.Action Respawned;
+ public event System.Action LandedOnNormalPlatform;
public Transform Transform => transform;
@@ -949,6 +950,7 @@ namespace DanroJump.Gameplay
if (!suppressAutoJump)
{
Bounce(autoJumpVelocity);
+ LandedOnNormalPlatform?.Invoke();
}
}
diff --git a/Assets/Scripts/Runtime/UI/Gameplay/GameplayHudController.cs b/Assets/Scripts/Runtime/UI/Gameplay/GameplayHudController.cs
index 1bd26bd..99961b7 100644
--- a/Assets/Scripts/Runtime/UI/Gameplay/GameplayHudController.cs
+++ b/Assets/Scripts/Runtime/UI/Gameplay/GameplayHudController.cs
@@ -52,6 +52,7 @@ namespace DanroJump.UI.Gameplay
private Label coinsLabel;
private Label deathsLabel;
private Label stateLabel;
+ private Label comboLabel;
private CancellationTokenSource refreshCancellation;
private CancellationTokenSource qrTimeoutCancellation;
private CancellationTokenSource physicalDispenseCancellation;
@@ -185,6 +186,7 @@ namespace DanroJump.UI.Gameplay
coinsLabel = FindLabel(root, coinsLabelName);
deathsLabel = FindLabel(root, deathsLabelName);
stateLabel = FindLabel(root, stateLabelName);
+ comboLabel = FindLabel(root, "ComboValue");
}
private void BindPrizeOverlay()
@@ -248,9 +250,48 @@ namespace DanroJump.UI.Gameplay
SetLabel(coinsLabel, session.Coins.ToString());
SetLabel(deathsLabel, session.DeathCount.ToString());
SetLabel(stateLabel, FormatState(session.State));
+
+ if (comboLabel != null)
+ {
+ if (session.CurrentCombo > 1)
+ {
+ var comboText = $"x{session.CurrentCombo} COMBO!";
+ if (comboLabel.text != comboText)
+ {
+ comboLabel.text = comboText;
+ comboLabel.style.display = DisplayStyle.Flex;
+ PulseComboLabel();
+ }
+ }
+ else
+ {
+ comboLabel.style.display = DisplayStyle.None;
+ }
+ }
+
RefreshContinueOverlay();
}
+ private void PulseComboLabel()
+ {
+ if (comboLabel == null)
+ {
+ return;
+ }
+
+ comboLabel.style.scale = new StyleScale(new Scale(new Vector3(1.4f, 1.4f, 1f)));
+ ResetComboScaleAsync().Forget();
+ }
+
+ private async UniTaskVoid ResetComboScaleAsync()
+ {
+ await UniTask.Delay(TimeSpan.FromMilliseconds(80));
+ if (comboLabel != null)
+ {
+ comboLabel.style.scale = new StyleScale(new Scale(new Vector3(1.0f, 1.0f, 1f)));
+ }
+ }
+
///
/// Безопасно обновляет текст label, если элемент найден в UXML.
///
diff --git a/Assets/UI/Gameplay/GameplayHud.uxml b/Assets/UI/Gameplay/GameplayHud.uxml
index ed985a0..777c914 100644
--- a/Assets/UI/Gameplay/GameplayHud.uxml
+++ b/Assets/UI/Gameplay/GameplayHud.uxml
@@ -17,6 +17,7 @@
+