Implement score combo multiplier with animated HUD text

Подробный список изменений:
- В IPlayerController.cs и JumpPlayerController.cs добавлено событие LandedOnNormalPlatform, срабатывающее при обычном автопрыжке.
- В GameplaySession.cs внедрено свойство CurrentCombo. Очки при сборе монет и убийстве врагов умножаются на текущий комбо-множитель, который растет с каждым набором очков. Сброс комбо в 1 происходит по событию приземления на обычную платформу, при смерти или сбросе сессии.
- В GameplayHud.uxml добавлен элемент ComboValue с оранжевым стилем и тенью.
- В GameplayHudController.cs реализовано отображение комбо с анимацией пульсации scale.
This commit is contained in:
Данил Ярош 2026-07-03 16:40:34 +03:00
parent 19ac082fed
commit 15adf94b21
5 changed files with 80 additions and 2 deletions

View File

@ -31,6 +31,11 @@ namespace DanroJump.Gameplay
public string SessionId { get; private set; } = CreateSessionId();
/// <summary>
/// Текущий комбо-множитель очков (увеличивается при сборе монет/врагах, сбрасывается на платформах).
/// </summary>
public int CurrentCombo { get; private set; } = 1;
/// <summary>
/// Итоговые очки текущего забега: игровые начисления плюс бонус за максимальную высоту.
/// </summary>
@ -122,11 +127,33 @@ namespace DanroJump.Gameplay
{
this.player.Died += HandlePlayerDied;
this.player.Respawned += HandlePlayerRespawned;
this.player.LandedOnNormalPlatform += ResetCombo;
}
}
/// <summary>
/// Добавляет очки в текущий забег.
/// Увеличивает комбо-множитель.
/// </summary>
public void IncrementCombo()
{
CurrentCombo++;
Changed?.Invoke();
}
/// <summary>
/// Сбрасывает комбо-множитель в 1.
/// </summary>
public void ResetCombo()
{
if (CurrentCombo != 1)
{
CurrentCombo = 1;
Changed?.Invoke();
}
}
/// <summary>
/// Добавляет очки в текущий забег (с учетом комбо-множителя).
/// </summary>
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;
}
/// <summary>
@ -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);

View File

@ -9,6 +9,7 @@ namespace DanroJump.Gameplay
{
event System.Action<IPlayerController> Died;
event System.Action<IPlayerController> Respawned;
event System.Action LandedOnNormalPlatform;
Transform Transform { get; }
float HighestY { get; }

View File

@ -186,6 +186,7 @@ namespace DanroJump.Gameplay
public event System.Action<IPlayerController> Died;
public event System.Action<IPlayerController> Respawned;
public event System.Action LandedOnNormalPlatform;
public Transform Transform => transform;
@ -949,6 +950,7 @@ namespace DanroJump.Gameplay
if (!suppressAutoJump)
{
Bounce(autoJumpVelocity);
LandedOnNormalPlatform?.Invoke();
}
}

View File

@ -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)));
}
}
/// <summary>
/// Безопасно обновляет текст label, если элемент найден в UXML.
/// </summary>

View File

@ -17,6 +17,7 @@
</ui:VisualElement>
<ui:VisualElement class="hud-icon hud-icon-crystal hud-icon-crystal-right" style="padding-left: 32px; padding-right: 64px; display: none;"/>
</ui:VisualElement>
<ui:Label name="ComboValue" text="x2 COMBO!" style="position: absolute; bottom: -80px; align-self: center; font-size: 54px; -unity-font-definition: url(&quot;project://database/Assets/Fonts/RifficFree-Bold.ttf?fileID=12800000&amp;guid=63991fec8a53abe4d9a7907ef7f0db77&amp;type=3#RifficFree-Bold&quot;); color: rgb(255, 140, 0); -unity-text-outline-width: 3px; -unity-text-outline-color: rgb(0, 0, 0); text-shadow: 3px 3px 0px rgba(0,0,0,0.5); display: none; transition-property: scale; transition-duration: 0.08s; transition-timing-function: ease-out;" />
</ui:VisualElement>
<ui:VisualElement name="RightStats" class="hud-column hud-column-right">
<ui:VisualElement class="hud-stat-list">