Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
- добавлена новая система призов: PrizeBase, PrizeType, PrizeDatabase, LevelPrizeConfig и реализации для hopper, QR-кода, отсутствия приза и стойки с призом - добавлена конфигурация призов уровней через `prizes.json` и связанная логика загрузки/обработки в менеджерах игры - расширен UI главного меню и настроек уровней: добавлены LevelSettingsModal, LevelPrizeSettingsModal, QRCodeModal и PrizeStandModal - обновлена навигация модальных окон и связанный UI-код: ModalManager, MainMenu, RulesModal, PauseMenu, PrizeErrorDisplay, UIRowPointer и WinBannerController - доработаны настройки и генерация UI-элементов: обновлены SettingsDatabase, SettingsUIGenerator, SettingUIItem, ValueSelectorRow и editor drawer для настроек - обновлены и добавлены UI-prefab'ы для кнопок, строк настроек, панели групп и кнопки настроек уровня - обновлены сцены `MainMenu` и `Level UI` под новый флоу настройки уровней, выбора призов и отображения модалок - добавлены файлы лаунчера в `StreamingAssets`, включая `RRLauncher.exe`, служебные meta-файлы и документацию - улучшено логирование через FileLogger и удален устаревший код лицензирования - удалены больше неиспользуемые файлы: `Licensing.cs`, `SteppedSlider.cs` и recovery-сцены
1015 lines
36 KiB
C#
1015 lines
36 KiB
C#
using System;
|
||
using System.IO;
|
||
using System.Runtime.InteropServices;
|
||
using RR.Common.Controls;
|
||
using UnityEngine;
|
||
using UnityEngine.Audio;
|
||
using UnityEngine.SceneManagement;
|
||
|
||
using RR.Common.Managers;
|
||
using RR.Common.Settings;
|
||
using RR.Common.Utility;
|
||
using RR.Game2D.Behaviour;
|
||
using RR.Game2D.Entities;
|
||
using UnityEditor;
|
||
|
||
namespace RR.Game2D.Core.Managers
|
||
{
|
||
[DefaultExecutionOrder(-1000)]
|
||
public class GameManager : MonoBehaviour
|
||
{
|
||
// Singleton instance
|
||
public static GameManager Instance { get; private set; }
|
||
// Logger
|
||
public static CompositeLogger Logger { get; private set; }
|
||
// Game settings (новая система)
|
||
public SettingsManager Settings { get; private set; }
|
||
// Audio control for game
|
||
public AudioManager Audio {get; private set;}
|
||
// Payment control for game
|
||
public PaymentManager Payment {get; private set;}
|
||
// Balance control for game
|
||
public BalanceManager Balance {get; private set;}
|
||
// Inactivity control for game
|
||
public InactivityManager Inactivity {get; private set;}
|
||
|
||
// Plate control for game
|
||
public ComSystemManager Plate { get; private set; }
|
||
// Light control for game
|
||
public LightController Light { get; private set; }
|
||
|
||
// Globals
|
||
[Header("Globals")]
|
||
[SerializeField] private string globalsPrefabResourcePath = "Prefabs/Global/Globals";
|
||
[SerializeField] private string AudioMixerResourcePath = "Audio/Master";
|
||
|
||
private GameObject globals;
|
||
|
||
[Header("Пауза")]
|
||
public bool IsPaused { get; private set; }
|
||
|
||
[Header("Базовые настройки игры")]
|
||
public int StartingLives { get; private set; } = 3;
|
||
public int MaxLives { get; private set; } = 3;
|
||
public int LivesIncrement { get; private set; } = 0;
|
||
|
||
[Header("Информация для игры")]
|
||
public int World { get; private set; } = 1;
|
||
public int Stage { get; private set; } = 1;
|
||
private int StagesByWorld { get; set; } = 3;
|
||
public int Lives { get; set; } = 3;
|
||
public int Coins { get; private set; } = 0;
|
||
|
||
private float _gameStartTime;
|
||
|
||
[Header("Игрок")]
|
||
[SerializeField] private GameObject playerPrefab;
|
||
[SerializeField] private string playerPrefabResourcePath = "Prefabs/Player_New"; // Путь в Resources для загрузки префаба игрока, если не назначен в инспекторе
|
||
public GameObject PlayerObj { get; private set; }
|
||
private bool _shouldSpawnPlayer;
|
||
|
||
private void OnEnable() => SceneManager.sceneLoaded += OnSceneLoaded;
|
||
private void OnDisable() => SceneManager.sceneLoaded -= OnSceneLoaded;
|
||
|
||
// События менеджера
|
||
public event Action<bool> PauseStateChanged; // Когда меняется состояние паузы
|
||
public event Action<GameObject> PlayerSpawned; // Когда игрос спавнится на сцене
|
||
public event Action<int> CoinsChanged; // Когда меняется кол-во монет
|
||
public event Action<int> MaxLivesChanged; // Когда меняется максимальное кол-во жизней
|
||
public event Action<int> LivesChanged; // Когда меняется кол-во жизней
|
||
public event Action<float> GameOverEvent; // Когда игрок проигрывает
|
||
public event Action<float> GameWonEvent; // Когда игрок выигрывает
|
||
|
||
#region Unity Lifetime
|
||
|
||
// Singleton
|
||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||
private static void Initialize()
|
||
{
|
||
if (Instance != null) return;
|
||
GameObject go = new(nameof(GameManager));
|
||
Instance = go.AddComponent<GameManager>();
|
||
go.AddComponent<AutoCursorHider>();
|
||
DontDestroyOnLoad(go);
|
||
}
|
||
|
||
private void Awake()
|
||
{
|
||
// ---- Singleton ----
|
||
if (Instance != null && Instance != this)
|
||
{
|
||
Destroy(gameObject);
|
||
return;
|
||
}
|
||
|
||
Instance = this;
|
||
DontDestroyOnLoad(gameObject);
|
||
// -------------------
|
||
|
||
StartCoroutine(CheckLicenseCoroutine());
|
||
|
||
// Initialize Logger
|
||
if (Logger == null)
|
||
{
|
||
Logger = new CompositeLogger();
|
||
FileLogger fileLogger = new(Path.Combine(Directory.GetParent(Application.dataPath)?.ToString() ?? Application.dataPath, "game.log"));
|
||
Logger.Initialize(fileLogger);
|
||
}
|
||
|
||
// Initialize Audio control
|
||
AudioMixer audioMixer = Resources.Load<AudioMixer>(AudioMixerResourcePath);
|
||
if (Audio == null)
|
||
{
|
||
Audio = gameObject.AddComponent<AudioManager>();
|
||
Audio.Initialize(audioMixer);
|
||
}
|
||
|
||
// Initialize Global Scripts from prefab
|
||
if (globals == null)
|
||
{
|
||
GameObject prefab = Resources.Load<GameObject>(globalsPrefabResourcePath);
|
||
if (prefab != null)
|
||
{
|
||
Debug.Log($"[GameManager] Loading Globals prefab from {globalsPrefabResourcePath}");
|
||
globals = Instantiate(prefab);
|
||
DontDestroyOnLoad(globals);
|
||
Debug.Log($"[GameManager] Globals instantiated. SettingsManager.Instance = {(SettingsManager.Instance != null ? "exists" : "null")}");
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError($"[GameManager] Failed to load Globals prefab from {globalsPrefabResourcePath}");
|
||
}
|
||
}
|
||
|
||
// Initialize Settings Manager (новая система) from Globals
|
||
if (Settings == null) Settings = globals.GetComponent<SettingsManager>();
|
||
if (Settings == null) Settings = SettingsManager.Instance;
|
||
|
||
if (Settings != null)
|
||
{
|
||
Settings.OnSettingsLoaded += OnSettingsChanged;
|
||
Settings.OnSettingsSaved += OnSettingsChanged;
|
||
}
|
||
|
||
// Initialize Payment control
|
||
if (Payment == null) Payment = globals.GetComponent<PaymentManager>();
|
||
|
||
// Initialize Balance control
|
||
if (Balance == null) Balance = globals.GetComponent<BalanceManager>();
|
||
if (Balance == null) Balance = BalanceManager.Instance;
|
||
|
||
// Initialize Inactivity control
|
||
if (Inactivity == null) Inactivity = gameObject.AddComponent<InactivityManager>();
|
||
if (Inactivity == null) Inactivity = InactivityManager.Instance;
|
||
|
||
if (Plate == null) Plate = globals.GetComponent<ComSystemManager>();
|
||
|
||
if (Plate != null) Light = Plate.Light;
|
||
StartIdleEffects();
|
||
|
||
Payment.OnCreditAdded.AddListener(OnCreditAdded);
|
||
|
||
_gameStartTime = Time.unscaledTime;
|
||
|
||
MaxLives = StartingLives;
|
||
MaxLivesChanged?.Invoke(MaxLives);
|
||
|
||
if (SceneManager.GetActiveScene().name != "MainMenu") _shouldSpawnPlayer = true;
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
if (!Light) return;
|
||
Light.StopBlink(false);
|
||
Light.StopSequence();
|
||
|
||
if (Instance == this) Instance = null;
|
||
}
|
||
|
||
private void OnApplicationQuit()
|
||
{
|
||
OnDestroy();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Поиск префаба игрока.
|
||
/// Если не найдет префаб из инспектора, ищет в "Resources"
|
||
/// </summary>
|
||
/// <returns> Префаб игрока </returns>
|
||
private GameObject ResolvePlayerPrefab()
|
||
{
|
||
if (playerPrefab != null) return playerPrefab;
|
||
|
||
GameObject loaded = Resources.Load<GameObject>(playerPrefabResourcePath);
|
||
|
||
if (loaded == null)
|
||
{
|
||
Logger.LogError(
|
||
gameObject.name,
|
||
$"playerPrefab is null. Resources.Load('{playerPrefabResourcePath}') вернул null." +
|
||
$" Ожидается файл: Assets/Resources/{playerPrefabResourcePath}.prefab (точное совпадение регистра)."
|
||
);
|
||
}
|
||
return loaded;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Payment Event Handlers
|
||
|
||
private void OnCreditAdded(int newCount)
|
||
{
|
||
if (SceneManager.GetActiveScene().name == "MainMenu")
|
||
{
|
||
if (newCount > 1)
|
||
{
|
||
MaxLives += LivesIncrement;
|
||
}
|
||
}
|
||
// Debug.Log($"Max Lives: {MaxLives}");
|
||
|
||
MaxLivesChanged?.Invoke(MaxLives);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Settings Event Handlers
|
||
|
||
private void OnSettingsChanged()
|
||
{
|
||
if (Settings == null) return;
|
||
|
||
// Получаем настройки из нового SettingsManager
|
||
StartingLives = Settings.GetInt("gameplay.startLives");
|
||
LivesIncrement = Settings.GetInt("gameplay.additionalLives");
|
||
|
||
int currentCredits = Payment?.CreditsCount ?? 0;
|
||
MaxLives = currentCredits > 1 ? StartingLives + (currentCredits - 1) * LivesIncrement : StartingLives;
|
||
MaxLivesChanged?.Invoke(MaxLives);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Pause
|
||
|
||
/// <summary>
|
||
/// Метод паузы игры.
|
||
/// Ставит на паузу все SFX и саму игру
|
||
/// </summary>
|
||
/// <param name="pause">Ставить ли на паузу</param>
|
||
public void SetPause(bool pause)
|
||
{
|
||
if (IsPaused == pause) return;
|
||
IsPaused = pause;
|
||
|
||
Time.timeScale = pause ? 0f : 1f;
|
||
Audio.SmoothPauseMusic(pause);
|
||
|
||
PauseStateChanged?.Invoke(IsPaused);
|
||
}
|
||
|
||
// Можно заменить на другое поведение
|
||
/// <summary>
|
||
/// Метод переключателя паузы.
|
||
/// </summary>
|
||
public void TogglePause() => SetPause(!IsPaused);
|
||
// --
|
||
|
||
#endregion
|
||
|
||
#region Levels and Scene management
|
||
|
||
/// <summary>
|
||
/// Начало новой игры.
|
||
/// Загружает первый уровень.
|
||
/// Устанавливает жизни на 3, монеты на 0
|
||
/// </summary>
|
||
public void NewGame()
|
||
{
|
||
Debug.Log("[GameManager] NewGame called");
|
||
bool isFreeGame = Settings != null && Settings.GetBool("payment.freeGame");
|
||
|
||
// Проверка баланса перед началом игры
|
||
if (!isFreeGame && Balance != null && !Balance.CanStartGame())
|
||
{
|
||
Logger?.Log(gameObject.name, "Cannot start game - insufficient balance");
|
||
Debug.LogWarning("[GameManager] Cannot start - insufficient balance");
|
||
return;
|
||
}
|
||
|
||
int livesToGive = StartingLives;
|
||
int balanceToSpend = 0;
|
||
|
||
if (!isFreeGame && Balance != null)
|
||
{
|
||
int currentBalance = Balance.CurrentBalance;
|
||
int impulseCost = Settings?.GetInt("payment.paymentImpulseCost") ?? 10;
|
||
|
||
// Рассчитываем количество оплат
|
||
int totalImpulses = currentBalance / impulseCost;
|
||
|
||
// Первая оплата даёт базовые жизни
|
||
// Каждая дополнительная оплата даёт LivesIncrement жизней
|
||
if (totalImpulses > 0)
|
||
{
|
||
int additionalImpulses = totalImpulses - 1;
|
||
int bonusLives = additionalImpulses * LivesIncrement;
|
||
livesToGive = StartingLives + bonusLives;
|
||
|
||
Debug.Log($"[GameManager] Balance: {currentBalance}, Impulses: {totalImpulses}, Base lives: {StartingLives}, Bonus lives: {bonusLives}, Total lives: {livesToGive}");
|
||
}
|
||
|
||
// Списываем ВЕСЬ баланс
|
||
balanceToSpend = currentBalance;
|
||
|
||
if (!Balance.SpendBalance(balanceToSpend, "начало игры (полный баланс)"))
|
||
{
|
||
Logger?.Log(gameObject.name, "Failed to spend balance for game start");
|
||
Debug.LogError("[GameManager] Failed to spend balance!");
|
||
return;
|
||
}
|
||
|
||
Debug.Log($"[GameManager] Spent entire balance: {balanceToSpend}, Lives given: {livesToGive}");
|
||
}
|
||
else if (isFreeGame)
|
||
{
|
||
Debug.Log("[GameManager] Free game mode enabled. Balance spend skipped.");
|
||
}
|
||
|
||
Lives = livesToGive;
|
||
LivesChanged?.Invoke(Lives);
|
||
Coins = 0;
|
||
CoinsChanged?.Invoke(Coins);
|
||
ScoreSystem.Instance?.ResetStats();
|
||
|
||
_gameStartTime = Time.time;
|
||
Logger.Log(gameObject.name,$"Game start time = {_gameStartTime}");
|
||
|
||
// Активируем шапку и эффекты при начале игры
|
||
if (Light)
|
||
{
|
||
Light.StopBlink();
|
||
if (Light.IsSequencePlaying) Light.StopSequence();
|
||
Light.SetLight(LightMode.Rainbow).Loop();
|
||
}
|
||
|
||
// Включаем отслеживание бездействия
|
||
if (Inactivity != null) Inactivity.EnableInactivityCheck();
|
||
|
||
LoadLevel(1, 1);
|
||
}
|
||
|
||
public void ContinueGame()
|
||
{
|
||
// Проверка баланса перед продолжением игры
|
||
if (Balance != null && !Balance.CanContinueGame())
|
||
{
|
||
Logger?.Log(gameObject.name, "Cannot continue game - insufficient balance");
|
||
return;
|
||
}
|
||
|
||
// Списание баланса за продолжение игры
|
||
if (Balance != null && !Balance.SpendForContinue())
|
||
{
|
||
Logger?.Log(gameObject.name, "Failed to spend balance for continue");
|
||
return;
|
||
}
|
||
|
||
Lives = StartingLives + 1;
|
||
LivesChanged?.Invoke(Lives);
|
||
Coins = 0;
|
||
CoinsChanged?.Invoke(Coins);
|
||
ScoreSystem.Instance?.ResetStats();
|
||
|
||
// Сбрасываем таймер бездействия
|
||
if (Inactivity != null) Inactivity.ResetInactivityTimer();
|
||
|
||
RespawnPlayer();
|
||
}
|
||
|
||
// Можно заменить на другое поведение
|
||
/// <summary>
|
||
/// Окончание игры.
|
||
/// Загружает сцену главного меню
|
||
/// </summary>
|
||
private void GameOver()
|
||
{
|
||
float totalTime = Time.unscaledTime - _gameStartTime;
|
||
|
||
// Отключаем отслеживание бездействия
|
||
if (Inactivity != null) Inactivity.DisableInactivityCheck();
|
||
|
||
Light?.SetLight(LightMode.BlueWave, 30f);
|
||
|
||
GameOverEvent?.Invoke(totalTime);
|
||
}
|
||
// --
|
||
|
||
private void GameWin()
|
||
{
|
||
float totalTime = Time.unscaledTime - _gameStartTime;
|
||
|
||
// Отключаем отслеживание бездействия
|
||
if (Inactivity != null) Inactivity.DisableInactivityCheck();
|
||
|
||
Light?.SetLight(LightMode.FinalWave, 30f);
|
||
|
||
GameWonEvent?.Invoke(totalTime);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Метод переход на следующий уровень.
|
||
/// Может загружать уровни и миры в прямой зависимости от
|
||
/// Количества уровней на мир(StagesByWorld)
|
||
/// </summary>
|
||
public void NextLevel()
|
||
{
|
||
if (Stage >= StagesByWorld)
|
||
{
|
||
World++;
|
||
Stage = 1;
|
||
}
|
||
else Stage++;
|
||
LoadLevel(World, Stage);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Метод сброса уровня.
|
||
/// При сбросе также сбрасывает монеты и отнимает жизни
|
||
/// </summary>
|
||
[Obsolete("Используйте RespawnPlayer() для респавна игрока без сброса уровня и жизней.")]
|
||
public void ResetLevel()
|
||
{
|
||
Lives--;
|
||
LivesChanged?.Invoke(Lives);
|
||
Coins = 0;
|
||
CoinsChanged?.Invoke(Coins);
|
||
|
||
//print($"Lives: {lives}");
|
||
if (Lives > 0) LoadLevel(World, Stage);
|
||
else GameOver();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Загрузка уровня по данным.
|
||
/// Загружает уровень по номерам мира и уровня
|
||
/// </summary>
|
||
/// <param name="world">Номер мира</param>
|
||
/// <param name="stage">Номер уровня</param>
|
||
public void LoadLevel(int world, int stage)
|
||
{
|
||
this.World = world;
|
||
this.Stage = stage;
|
||
|
||
string sceneName = $"{world}-{stage}";
|
||
if (SceneInBuildSettingsExists(sceneName))
|
||
{
|
||
LoadScene(sceneName);
|
||
}
|
||
else
|
||
{
|
||
// Можно заменить на другое поведение
|
||
|
||
GameWin();
|
||
|
||
// --
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Загрузка сцены по названию.
|
||
/// Вызывает одноименный метод в <see cref="SceneManager"/>.
|
||
/// Может загружать сцены с UI и без.
|
||
/// В сценах без UI игрок не спавнится
|
||
/// </summary>
|
||
/// <param name="sceneName">Строковое название сцены</param>
|
||
/// <param name="useUI">Использовать ли UI</param>
|
||
public void LoadScene(string sceneName, bool useUI = true)
|
||
{
|
||
_shouldSpawnPlayer = true;
|
||
|
||
SceneManager.LoadScene(sceneName);
|
||
|
||
if (useUI) SceneManager.LoadScene("Level UI", LoadSceneMode.Additive);
|
||
else
|
||
{
|
||
_shouldSpawnPlayer = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Метод проверки сцены на существование в настройках билда
|
||
/// </summary>
|
||
/// <param name="sceneName">Строковое название сцены</param>
|
||
/// <returns></returns>
|
||
private static bool SceneInBuildSettingsExists(string sceneName)
|
||
{
|
||
var count = SceneManager.sceneCountInBuildSettings;
|
||
for (var i = 0; i < count; i++)
|
||
{
|
||
var path = SceneUtility.GetScenePathByBuildIndex(i);
|
||
if (Path.GetFileNameWithoutExtension(path) == sceneName)
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Метод обработки события загрузки сцены.
|
||
/// При загрузке сцены проверяет её название на наличие "-" (уровень формата "1-1")
|
||
/// Если находит, спавнит игрока
|
||
/// Также обновляет номера мира и уровня
|
||
/// </summary>
|
||
/// <param name="scene">Сцена</param>
|
||
/// <param name="mode">Режим загрузки сцены</param>
|
||
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||
{
|
||
string currentSceneName = SceneManager.GetActiveScene().name;
|
||
|
||
// При переходе в главное меню — гарантированно запускаем подсветку и мигание
|
||
if (currentSceneName == "MainMenu")
|
||
{
|
||
StartIdleEffects();
|
||
return;
|
||
}
|
||
|
||
if (!currentSceneName.Contains("-")) return;
|
||
TrySpawnPlayerForActiveScene();
|
||
|
||
if (currentSceneName == $"{World}-{Stage}") return;
|
||
string[] levelInfo = currentSceneName.Split("-");
|
||
World = Convert.ToInt32(levelInfo[0]);
|
||
Stage = Convert.ToInt32(levelInfo[1]);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Запускает подсветку и мигание шляпы в режиме ожидания (главное меню).
|
||
/// Гарантированно останавливает всё предыдущее и запускает заново.
|
||
/// Безопасно вызывать повторно.
|
||
/// </summary>
|
||
public void StartIdleEffects()
|
||
{
|
||
if (!Light) return;
|
||
|
||
Light.StopBlink(enableHat: false);
|
||
Light.StopSequence();
|
||
|
||
Light.Blink();
|
||
Light.SetLight(LightMode.DiagonalWave)
|
||
.Then(LightMode.DiagonalWave2)
|
||
.Then(LightMode.DiagonalWave3)
|
||
.Then(LightMode.Rainbow)
|
||
.Then(LightMode.WaveUp)
|
||
.Then(LightMode.FallingRows)
|
||
.Then(LightMode.OrangeWave)
|
||
.Then(LightMode.SeaWave)
|
||
.Loop();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Player management
|
||
|
||
/// <summary>
|
||
/// Метод для спавна игрока.
|
||
/// Ищет первый спавнпоинт, спавнит игрока на его позиции.
|
||
/// Если на карте есть объект игрока, удаляет его и спавнит свой
|
||
/// </summary>
|
||
private void TrySpawnPlayerForActiveScene()
|
||
{
|
||
if (!_shouldSpawnPlayer) return;
|
||
PlayerSpawn spawn = FindFirstObjectByType<PlayerSpawn>();
|
||
Vector3 pos = spawn ? spawn.transform.position : new Vector3(0, 1.3f, 0);
|
||
Quaternion rot = spawn ? spawn.transform.rotation : Quaternion.identity;
|
||
|
||
PlayerObj = GameObject.FindGameObjectWithTag("Player");
|
||
if (PlayerObj != null) Destroy(PlayerObj);
|
||
|
||
GameObject prefab = ResolvePlayerPrefab();
|
||
if (prefab == null) return;
|
||
|
||
PlayerObj = Instantiate(prefab, pos, rot);
|
||
SceneManager.MoveGameObjectToScene(PlayerObj, SceneManager.GetActiveScene());
|
||
|
||
PlayerSpawned?.Invoke(PlayerObj);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Метод респавна игрока.
|
||
/// Отнимает жизнь, если жизней не осталось — вызывает GameOver()
|
||
/// Спавнит игрока на активном чекпоинте или на дефолтной позиции
|
||
/// </summary>
|
||
public void RespawnPlayer()
|
||
{
|
||
// отнимем жизнь (оставляем текущее поведение); монеты не сбрасываем
|
||
Lives--;
|
||
LivesChanged?.Invoke(Lives);
|
||
|
||
if (Lives <= 0)
|
||
{
|
||
GameOver();
|
||
return;
|
||
}
|
||
|
||
// Определяем дефолтные позицию и направление из PlayerSpawn в сцене
|
||
PlayerSpawn spawn = FindFirstObjectByType<PlayerSpawn>();
|
||
Vector3 defaultPos = spawn ? spawn.transform.position : new Vector3(0, 1.3f, 0);
|
||
Vector2 defaultForward = spawn ? spawn.defaultForward : Vector2.right;
|
||
|
||
// Если есть активный чекпоинт — берём из него; иначе дефолт
|
||
Vector3 respawnPos = defaultPos;
|
||
Vector2 respawnForward = defaultForward;
|
||
if (CheckpointSystem.Instance != null)
|
||
{
|
||
respawnPos = CheckpointSystem.Instance.GetSpawnPosition(defaultPos);
|
||
respawnForward = CheckpointSystem.Instance.GetSpawnForward(defaultForward);
|
||
|
||
if (CheckpointSystem.Instance.Active != null)
|
||
{
|
||
respawnPos = CheckpointSystem.Instance.Active.GetSpawnPosition();
|
||
respawnForward = CheckpointSystem.Instance.Active.GetForward();
|
||
}
|
||
}
|
||
|
||
// Если потеряли ссылку — попробуем найти игрока в сцене
|
||
if (PlayerObj == null)
|
||
PlayerObj = GameObject.FindGameObjectWithTag("Player");
|
||
|
||
if (PlayerObj == null)
|
||
{
|
||
// Создадим нового игрока, если объект был уничтожен
|
||
GameObject prefab = ResolvePlayerPrefab();
|
||
if (prefab == null) return;
|
||
PlayerObj = Instantiate(prefab, respawnPos, Quaternion.identity);
|
||
SceneManager.MoveGameObjectToScene(PlayerObj, SceneManager.GetActiveScene());
|
||
PlayerSpawned?.Invoke(PlayerObj);
|
||
}
|
||
else
|
||
{
|
||
// Остановим движение и телепортируем
|
||
Rigidbody2D rb = PlayerObj.GetComponent<Rigidbody2D>();
|
||
if (rb != null)
|
||
{
|
||
rb.linearVelocity = Vector2.zero;
|
||
rb.angularVelocity = 0f;
|
||
}
|
||
|
||
PlayerObj.transform.position = respawnPos;
|
||
}
|
||
|
||
// Повернём в нужную сторону (по X)
|
||
if (PlayerObj == null) return;
|
||
if (respawnForward.x != 0)
|
||
{
|
||
PlayerObj.transform.localEulerAngles = respawnForward.x >= 0
|
||
? Vector3.zero
|
||
: new Vector3(0, 180f, 0);
|
||
}
|
||
|
||
// Восстановим здоровье
|
||
Health playerHealth = PlayerObj.GetComponent<Health>();
|
||
if (playerHealth != null)
|
||
playerHealth.ResetHealth();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Coins and Lives management
|
||
|
||
/// <summary>
|
||
/// Метод добавления внутриигровой монеты
|
||
/// </summary>
|
||
public void AddCoin(int count = 1)
|
||
{
|
||
Coins += count;
|
||
// Можно заменить на другое поведение
|
||
if (Coins >= 100)
|
||
{
|
||
Coins -= 100;
|
||
AddLife();
|
||
}
|
||
// --
|
||
CoinsChanged?.Invoke(Coins);
|
||
}
|
||
|
||
// Можно заменить на другое поведение
|
||
/// <summary>
|
||
/// Метод для добавления жизни.
|
||
/// </summary>
|
||
private void AddLife()
|
||
{
|
||
Lives++;
|
||
LivesChanged?.Invoke(Lives);
|
||
}
|
||
// --
|
||
|
||
#endregion
|
||
|
||
#region DEFENCE
|
||
|
||
[StructLayout(LayoutKind.Sequential)]
|
||
private struct PROCESS_INFORMATION
|
||
{
|
||
public IntPtr hProcess;
|
||
public IntPtr hThread;
|
||
public int dwProcessId;
|
||
public int dwThreadId;
|
||
}
|
||
|
||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||
private struct STARTUPINFO
|
||
{
|
||
public int cb;
|
||
public string lpReserved;
|
||
public string lpDesktop;
|
||
public string lpTitle;
|
||
public int dwX;
|
||
public int dwY;
|
||
public int dwXSize;
|
||
public int dwYSize;
|
||
public int dwXCountChars;
|
||
public int dwYCountChars;
|
||
public int dwFillAttribute;
|
||
public int dwFlags;
|
||
public short wShowWindow;
|
||
public short cbReserved2;
|
||
public IntPtr lpReserved2;
|
||
public IntPtr hStdInput;
|
||
public IntPtr hStdOutput;
|
||
public IntPtr hStdError;
|
||
}
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||
private static extern bool CreateProcess(
|
||
string lpApplicationName,
|
||
string lpCommandLine,
|
||
IntPtr lpProcessAttributes,
|
||
IntPtr lpThreadAttributes,
|
||
bool bInheritHandles,
|
||
uint dwCreationFlags,
|
||
IntPtr lpEnvironment,
|
||
string lpCurrentDirectory,
|
||
ref STARTUPINFO lpStartupInfo,
|
||
out PROCESS_INFORMATION lpProcessInformation);
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true)]
|
||
private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true)]
|
||
private static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
|
||
|
||
[DllImport("kernel32.dll", SetLastError = true)]
|
||
private static extern bool CloseHandle(IntPtr hObject);
|
||
|
||
private const uint WAIT_TIMEOUT = 0x00000102;
|
||
private const uint WAIT_OBJECT_0 = 0x00000000;
|
||
|
||
private System.Collections.IEnumerator CheckLicenseCoroutine()
|
||
{
|
||
// Ждём один кадр, чтобы Unity завершил инициализацию
|
||
yield return null;
|
||
|
||
bool licenseValid = false;
|
||
bool checkCompleted = false;
|
||
//bool licenseValid = true;
|
||
//bool checkCompleted = true;
|
||
string errorMessage = "";
|
||
|
||
// Запускаем проверку в отдельном потоке (совместимо с IL2CPP)
|
||
System.Threading.Thread thread = new System.Threading.Thread(() =>
|
||
{
|
||
try
|
||
{
|
||
licenseValid = CheckLicense();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
errorMessage = ex.Message;
|
||
}
|
||
finally
|
||
{
|
||
checkCompleted = true;
|
||
}
|
||
});
|
||
|
||
thread.IsBackground = true;
|
||
thread.Start();
|
||
|
||
float timerest = 30f;
|
||
while (!checkCompleted && timerest > 0)
|
||
{
|
||
timerest -= Time.deltaTime;
|
||
yield return null;
|
||
}
|
||
|
||
if (!checkCompleted) licenseValid = false;
|
||
|
||
if (!licenseValid)
|
||
{
|
||
yield return new WaitForSeconds(3f);
|
||
|
||
#if UNITY_EDITOR
|
||
EditorApplication.isPlaying = false;
|
||
#else
|
||
Application.Quit();
|
||
#endif
|
||
}
|
||
else
|
||
{
|
||
//exclusive
|
||
Screen.fullScreenMode = FullScreenMode.ExclusiveFullScreen;
|
||
Screen.SetResolution(1920, 1080, true);
|
||
ClickCenter();
|
||
}
|
||
}
|
||
|
||
|
||
private bool CheckLicense()
|
||
{
|
||
try
|
||
{
|
||
string launcherName = "RRLauncher.exe";
|
||
string launcherPath = null;
|
||
|
||
// Поиск лаунчера рядом с исполняемым файлом
|
||
string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||
string pathInBase = Path.Combine(baseDirectory, launcherName);
|
||
|
||
if (File.Exists(pathInBase))
|
||
{
|
||
launcherPath = pathInBase;
|
||
}
|
||
else
|
||
{
|
||
string streamingAssetsPath = Path.Combine(Application.streamingAssetsPath, launcherName);
|
||
if (File.Exists(streamingAssetsPath))
|
||
{
|
||
launcherPath = streamingAssetsPath;
|
||
}
|
||
else
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
try
|
||
{
|
||
STARTUPINFO si = new STARTUPINFO();
|
||
si.cb = Marshal.SizeOf(si);
|
||
PROCESS_INFORMATION pi;
|
||
|
||
string workingDir = Path.GetDirectoryName(launcherPath);
|
||
|
||
bool success = CreateProcess(
|
||
launcherPath,
|
||
null,
|
||
IntPtr.Zero,
|
||
IntPtr.Zero,
|
||
false,
|
||
0,
|
||
IntPtr.Zero,
|
||
workingDir,
|
||
ref si,
|
||
out pi);
|
||
|
||
if (!success) return false;
|
||
uint waitResult = WaitForSingleObject(pi.hProcess, 30000);
|
||
|
||
if (waitResult == WAIT_TIMEOUT)
|
||
{
|
||
CloseHandle(pi.hProcess);
|
||
CloseHandle(pi.hThread);
|
||
return false;
|
||
}
|
||
|
||
// Получение кода возврата
|
||
uint exitCode;
|
||
if (!GetExitCodeProcess(pi.hProcess, out exitCode))
|
||
{
|
||
CloseHandle(pi.hProcess);
|
||
CloseHandle(pi.hThread);
|
||
return false;
|
||
}
|
||
|
||
CloseHandle(pi.hProcess);
|
||
CloseHandle(pi.hThread);
|
||
|
||
if (exitCode == 0)
|
||
{
|
||
ForceToFront();
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
catch (Exception outerEx)
|
||
{
|
||
Debug.LogError($"[License] CRITICAL ERROR in CheckLicense: {outerEx.Message}");
|
||
Debug.LogError($"[License] Stack trace: {outerEx.StackTrace}");
|
||
return false;
|
||
}
|
||
|
||
|
||
}
|
||
|
||
#region FORCE TO FRONT
|
||
[DllImport("user32.dll")]
|
||
private static extern bool SetForegroundWindow(IntPtr hwnd);
|
||
|
||
[DllImport("user32.dll")]
|
||
private static extern IntPtr GetActiveWindow();
|
||
|
||
public void ForceToFront()//Метод который возвращает фокус а окно после открытия защиты
|
||
{
|
||
IntPtr handle = GetActiveWindow();
|
||
SetForegroundWindow(handle);
|
||
}
|
||
#endregion
|
||
|
||
|
||
|
||
#region клик в центр
|
||
|
||
[StructLayout(LayoutKind.Sequential)]
|
||
public struct INPUT
|
||
{
|
||
public uint type;
|
||
public MOUSEINPUT mi;
|
||
}
|
||
|
||
[StructLayout(LayoutKind.Sequential)]
|
||
public struct MOUSEINPUT
|
||
{
|
||
public int dx;
|
||
public int dy;
|
||
public uint mouseData;
|
||
public uint dwFlags;
|
||
public uint time;
|
||
public IntPtr dwExtraInfo;
|
||
}
|
||
|
||
private const uint INPUT_MOUSE = 0;
|
||
private const uint MOUSEEVENTF_MOVE = 0x0001;
|
||
private const uint MOUSEEVENTF_ABSOLUTE = 0x8000;
|
||
private const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
|
||
private const uint MOUSEEVENTF_LEFTUP = 0x0004;
|
||
|
||
[DllImport("user32.dll")]
|
||
static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
||
|
||
public static void ClickCenter()
|
||
{
|
||
int screenX = Screen.currentResolution.width / 2;
|
||
int screenY = Screen.currentResolution.height / 2;
|
||
|
||
// WinAPI использует координаты 0–65535
|
||
int absoluteX = (int)(screenX * 65535 / Screen.currentResolution.width);
|
||
int absoluteY = (int)(screenY * 65535 / Screen.currentResolution.height);
|
||
|
||
INPUT[] inputs = new INPUT[3];
|
||
|
||
// движение мыши
|
||
inputs[0] = new INPUT
|
||
{
|
||
type = INPUT_MOUSE,
|
||
mi = new MOUSEINPUT
|
||
{
|
||
dx = absoluteX,
|
||
dy = absoluteY,
|
||
dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE,
|
||
}
|
||
};
|
||
|
||
// нажатие
|
||
inputs[1] = new INPUT
|
||
{
|
||
type = INPUT_MOUSE,
|
||
mi = new MOUSEINPUT
|
||
{
|
||
dwFlags = MOUSEEVENTF_LEFTDOWN
|
||
}
|
||
};
|
||
|
||
// отпускание
|
||
inputs[2] = new INPUT
|
||
{
|
||
type = INPUT_MOUSE,
|
||
mi = new MOUSEINPUT
|
||
{
|
||
dwFlags = MOUSEEVENTF_LEFTUP
|
||
}
|
||
};
|
||
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(INPUT)));
|
||
}
|
||
|
||
|
||
#endregion
|
||
#endregion
|
||
}
|
||
}
|