Подробные изменения: - Добавлена система призовых уровней и выдачи призов с QR-контуром Telegram, шаблоном сообщения и генерацией QR без внешних библиотек. - Доработаны сервисные настройки: действия кнопок, модальные окна, ввод с джойстика/клавиатуры, статистика, настройки призов и маршрутизация QR. - Переработаны gameplay HUD и overlay-панели на UI Toolkit со спрайтовыми плашками, DEV BUILD-статистикой, экраном продолжения и экраном результата приза. - Исправлены сценарии смерти, GameOver, продолжения игры, начисления очков за высоту и перехода в главное меню. - Перенесены UI-шаблоны из Resources в Assets/UI, подключена Addressables-загрузка шаблонов настроек и standalone-стили для MainMenuModals. - Добавлены EditMode/PlayMode тесты для призов, QR, arcade input, UI-модалок, scene flow, HUD и gameplay-сессии. - Обновлены сцены, настройки Addressables, service settings database, документация и дорожная карта. Проверки: - Targeted EditMode QR/UI/settings: 23/23 passed - Targeted UI templates/EditMode: 17/17 passed - MainMenuUxmlTests: 3/3 passed - Full EditMode: 129/129 passed - PlayMode: 6/6 passed - Runtime Addressables template load check: loaded=5 - Unity Console после финального refresh: 0 errors
283 lines
12 KiB
C#
283 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using DanroJump.Gameplay;
|
|
using DanroJump.Hardware.Com;
|
|
using DanroJump.Prizes;
|
|
using DanroJump.SceneFlow;
|
|
using DanroJump.Settings;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
|
|
namespace DanroJump.Tests.EditMode
|
|
{
|
|
public sealed class GameplaySessionContinueTests
|
|
{
|
|
[Test]
|
|
public void CanContinueGame_IsTrueOnlyWhenEnabledAndBalanceIsEnough()
|
|
{
|
|
var session = CreateGameOverSession(
|
|
new StubSettings(canContinue: true, freePlay: false, continueCost: 5, additionalLives: 2),
|
|
new StubWallet(balance: 5),
|
|
out _,
|
|
out _);
|
|
|
|
Assert.That(session.CanContinueGame, Is.True);
|
|
Assert.That(session.ContinueGameCost, Is.EqualTo(5));
|
|
Assert.That(session.ContinueGameLives, Is.EqualTo(2));
|
|
}
|
|
|
|
[Test]
|
|
public void TryContinueGame_SpendsBalanceAddsLivesAndCancelsMenuReturn()
|
|
{
|
|
var session = CreateGameOverSession(
|
|
new StubSettings(canContinue: true, freePlay: false, continueCost: 5, additionalLives: 2),
|
|
new StubWallet(balance: 8),
|
|
out var sceneFlow,
|
|
out var wallet);
|
|
|
|
var continued = session.TryContinueGame();
|
|
|
|
Assert.That(continued, Is.True);
|
|
Assert.That(wallet.Credits, Is.EqualTo(3));
|
|
Assert.That(wallet.SpendCalls, Is.EqualTo(1));
|
|
Assert.That(session.RemainingLives, Is.EqualTo(2));
|
|
Assert.That(session.State, Is.EqualTo(GameplaySessionState.Running));
|
|
Assert.That(sceneFlow.CancelGameOverReturnCalls, Is.EqualTo(1));
|
|
}
|
|
|
|
[Test]
|
|
public void TryContinueGame_ReturnsFalseWhenBalanceIsInsufficient()
|
|
{
|
|
var session = CreateGameOverSession(
|
|
new StubSettings(canContinue: true, freePlay: false, continueCost: 5, additionalLives: 2),
|
|
new StubWallet(balance: 4),
|
|
out var sceneFlow,
|
|
out var wallet);
|
|
|
|
var continued = session.TryContinueGame();
|
|
|
|
Assert.That(continued, Is.False);
|
|
Assert.That(wallet.Credits, Is.EqualTo(4));
|
|
Assert.That(wallet.SpendCalls, Is.EqualTo(0));
|
|
Assert.That(session.State, Is.EqualTo(GameplaySessionState.GameOver));
|
|
Assert.That(sceneFlow.CancelGameOverReturnCalls, Is.EqualTo(0));
|
|
}
|
|
|
|
[Test]
|
|
public void TryContinueGame_UnblocksPlayerRespawnAfterGameOver()
|
|
{
|
|
var playerObject = new GameObject("Continue Player");
|
|
var cameraObject = new GameObject("Continue Camera");
|
|
try
|
|
{
|
|
playerObject.AddComponent<Rigidbody2D>();
|
|
playerObject.AddComponent<BoxCollider2D>();
|
|
var player = playerObject.AddComponent<JumpPlayerController>();
|
|
var camera = cameraObject.AddComponent<Camera>();
|
|
camera.orthographic = true;
|
|
camera.orthographicSize = 5f;
|
|
camera.transform.position = new Vector3(0f, 0f, -10f);
|
|
player.Construct(camera);
|
|
typeof(JumpPlayerController)
|
|
.GetMethod("Awake", BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.Invoke(player, null);
|
|
player.Kill();
|
|
player.BlockPendingRespawn();
|
|
|
|
var session = new GameplaySession(
|
|
player,
|
|
new StubSettings(canContinue: true, freePlay: true, continueCost: 0, additionalLives: 3),
|
|
new RecordingSceneFlowService());
|
|
TestObjectFactory.SetPrivateField(session, "<State>k__BackingField", GameplaySessionState.GameOver);
|
|
|
|
var continued = session.TryContinueGame();
|
|
|
|
Assert.That(continued, Is.True);
|
|
Assert.That(session.RemainingLives, Is.EqualTo(3));
|
|
Assert.That(session.State, Is.EqualTo(GameplaySessionState.Running));
|
|
Assert.That(player.IsDead, Is.False);
|
|
|
|
session.Dispose();
|
|
}
|
|
finally
|
|
{
|
|
UnityEngine.Object.DestroyImmediate(playerObject);
|
|
UnityEngine.Object.DestroyImmediate(cameraObject);
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public void FinalDeath_SchedulesDelayedMenuReturnSoGameOverPanelCanBeShown()
|
|
{
|
|
var sceneFlow = new RecordingSceneFlowService();
|
|
var session = new GameplaySession(null, new StubSettings(canContinue: false, freePlay: false, continueCost: 0, additionalLives: 1), sceneFlow);
|
|
TestObjectFactory.SetPrivateField(session, "<RemainingLives>k__BackingField", 1);
|
|
|
|
typeof(GameplaySession)
|
|
.GetMethod("HandlePlayerDied", BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.Invoke(session, new object[] { null });
|
|
|
|
Assert.That(session.State, Is.EqualTo(GameplaySessionState.GameOver));
|
|
Assert.That(sceneFlow.LastGameOverReturnDelay, Is.EqualTo(TimeSpan.FromSeconds(8)).Within(TimeSpan.FromMilliseconds(1)));
|
|
}
|
|
|
|
[Test]
|
|
public void FinalDeath_WithPrizeFlowResult_DoesNotScheduleAutomaticMenuReturn()
|
|
{
|
|
var sceneFlow = new RecordingSceneFlowService();
|
|
var prizeFlow = new StubPrizeFlow(PrizeRewardKind.Big);
|
|
var session = new GameplaySession(
|
|
null,
|
|
new StubSettings(canContinue: false, freePlay: false, continueCost: 0, additionalLives: 1),
|
|
sceneFlow,
|
|
prizeFlow);
|
|
TestObjectFactory.SetPrivateField(session, "<RemainingLives>k__BackingField", 1);
|
|
|
|
typeof(GameplaySession)
|
|
.GetMethod("HandlePlayerDied", BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.Invoke(session, new object[] { null });
|
|
|
|
Assert.That(session.State, Is.EqualTo(GameplaySessionState.GameOver));
|
|
Assert.That(prizeFlow.StartCalls, Is.EqualTo(1));
|
|
Assert.That(sceneFlow.LastGameOverReturnDelay, Is.Null);
|
|
}
|
|
|
|
private static GameplaySession CreateGameOverSession(
|
|
IReadOnlyServiceSettings settings,
|
|
StubWallet wallet,
|
|
out RecordingSceneFlowService sceneFlow,
|
|
out StubWallet returnedWallet)
|
|
{
|
|
sceneFlow = new RecordingSceneFlowService();
|
|
returnedWallet = wallet;
|
|
var session = new GameplaySession(null, settings, sceneFlow, null, wallet);
|
|
TestObjectFactory.SetPrivateField(session, "<State>k__BackingField", GameplaySessionState.GameOver);
|
|
return session;
|
|
}
|
|
|
|
private sealed class StubSettings : IReadOnlyServiceSettings
|
|
{
|
|
private readonly bool canContinue;
|
|
private readonly bool freePlay;
|
|
private readonly int continueCost;
|
|
private readonly int additionalLives;
|
|
|
|
public StubSettings(bool canContinue, bool freePlay, int continueCost, int additionalLives)
|
|
{
|
|
this.canContinue = canContinue;
|
|
this.freePlay = freePlay;
|
|
this.continueCost = continueCost;
|
|
this.additionalLives = additionalLives;
|
|
}
|
|
|
|
public ServiceSettingsDatabase Database => null;
|
|
public event Action SettingsLoaded { add { } remove { } }
|
|
public event Action SettingsSaved { add { } remove { } }
|
|
public event Action<string, ServiceSettingValue> SettingChanged { add { } remove { } }
|
|
public bool TryGetDefinition(string key, out ServiceSettingDefinition definition) { definition = null; return false; }
|
|
public IReadOnlyDictionary<string, ServiceSettingValue> Snapshot() => new Dictionary<string, ServiceSettingValue>();
|
|
public ServiceSettingValue Get(string key) => default;
|
|
|
|
public bool GetBool(string key)
|
|
{
|
|
return key == ServiceSettingsKeys.CanContinueGame && canContinue ||
|
|
key == ServiceSettingsKeys.FreePlay && freePlay;
|
|
}
|
|
|
|
public int GetInt(string key)
|
|
{
|
|
return key switch
|
|
{
|
|
ServiceSettingsKeys.ContinueGameCost => continueCost,
|
|
ServiceSettingsKeys.AdditionalLives => additionalLives,
|
|
_ => 0,
|
|
};
|
|
}
|
|
|
|
public float GetFloat(string key) => 0f;
|
|
public string GetString(string key) => string.Empty;
|
|
public int GetOptionIndex(string key) => 0;
|
|
}
|
|
|
|
private sealed class StubWallet : IComCreditWallet
|
|
{
|
|
private readonly UnityEvent<int> creditAdded = new();
|
|
private readonly UnityEvent<int> creditRemoved = new();
|
|
private readonly UnityEvent<int> creditsChanged = new();
|
|
|
|
public StubWallet(int balance)
|
|
{
|
|
Credits = balance;
|
|
}
|
|
|
|
public int Credits { get; private set; }
|
|
public int SpendCalls { get; private set; }
|
|
public UnityEvent<int> CreditAdded => creditAdded;
|
|
public UnityEvent<int> CreditRemoved => creditRemoved;
|
|
public UnityEvent<int> CreditsChanged => creditsChanged;
|
|
public void AddCredit(int amount = 1) { Credits += amount; creditsChanged.Invoke(Credits); }
|
|
|
|
public bool TrySpendCredits(int amount = 1)
|
|
{
|
|
if (Credits < amount)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
SpendCalls++;
|
|
Credits -= amount;
|
|
creditRemoved.Invoke(Credits);
|
|
creditsChanged.Invoke(Credits);
|
|
return true;
|
|
}
|
|
|
|
public void SetCredits(int value)
|
|
{
|
|
Credits = Mathf.Max(0, value);
|
|
creditsChanged.Invoke(Credits);
|
|
}
|
|
}
|
|
|
|
private sealed class RecordingSceneFlowService : ISceneFlowService
|
|
{
|
|
public int CancelGameOverReturnCalls { get; private set; }
|
|
public TimeSpan? LastGameOverReturnDelay { get; private set; }
|
|
public void LoadScene(string sceneName) { }
|
|
public void OpenMainMenu() { }
|
|
public void StartGameplay(string sceneName) { }
|
|
public void OpenSettingsScene(string sceneName) { }
|
|
public void ReturnToMainMenuAfterGameOver(TimeSpan delay) => LastGameOverReturnDelay = delay;
|
|
public void CancelGameOverReturn() => CancelGameOverReturnCalls++;
|
|
public bool SceneExists(string sceneName) => true;
|
|
}
|
|
|
|
private sealed class StubPrizeFlow : IPrizeFlowService
|
|
{
|
|
private readonly PrizeRewardKind rewardKind;
|
|
|
|
public StubPrizeFlow(PrizeRewardKind rewardKind)
|
|
{
|
|
this.rewardKind = rewardKind;
|
|
}
|
|
|
|
public event Action<PrizeFlowResult> PrizeFlowStarted { add { } remove { } }
|
|
public PrizeFlowResult LastResult { get; private set; }
|
|
public int StartCalls { get; private set; }
|
|
|
|
public PrizeFlowResult EvaluateCompletedSession(GameplaySession session)
|
|
{
|
|
return new PrizeFlowResult(rewardKind, session?.Score ?? 0, session?.Coins ?? 0, session?.MaxRunHeight ?? 0f);
|
|
}
|
|
|
|
public PrizeFlowResult StartPrizeFlow(GameplaySession session)
|
|
{
|
|
StartCalls++;
|
|
LastResult = EvaluateCompletedSession(session);
|
|
return LastResult;
|
|
}
|
|
}
|
|
}
|
|
}
|