/
Alexwww228
/
BridgeBattleSimulator
Обзор
Документация
Войти
/
Alexwww228
/
BridgeBattleSimulator
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
BattleSimulator.UI/ViewModels/MainViewModel.cs
387 строк
18 KB
Alexwww228
fix: Save system and Abstract Factory
24 май 2026, 01:45
24 май 2026, 01:45
7391992
Код
Авторство
О чём код?
using ReactiveUI; using System; using System.Reactive.Linq; using BattleSimulator.Domain.Units; using BattleSimulator.Application.Builders; using BattleSimulator.Application.Config; using BattleSimulator.Application.Factories; using BattleSimulator.Domain.Combat; using BattleSimulator.UI.Services; using BattleSimulator.UI.Services.Commands; namespace BattleSimulator.UI.ViewModels; /// <summary> /// Корневой ViewModel приложения — управляет навигацией между экранами. /// /// Экраны: Меню → ВыборСохранения / Настройка армий → Бой | Настройки /// /// Использует IArmyFactory (паттерн Абстрактная Фабрика) для создания /// всех связанных игровых объектов: калькулятора урона, фабрики юнитов /// и строителя армий. Это гарантирует их согласованность. /// </summary> public sealed class MainViewModel : ReactiveObject { // ══════════════════════════════════════════════════════════════════ // Абстрактная фабрика (паттерн Abstract Factory) // Все игровые объекты создаются только через неё. // ══════════════════════════════════════════════════════════════════ private readonly IArmyFactory _armyFactory; private readonly IUnitFactory _unitFactory; private readonly GameSaveService _saveService; // Номер слота, в который ведётся автосохранение (выбирается при загрузке) private int _activeSlot = 1; public MainViewModel() { // Создаём все объекты через фабрику — ни одного new DefaultDamageCalculator() напрямую _armyFactory = new StandardArmyFactory(); _unitFactory = _armyFactory.CreateUnitFactory(); _saveService = new GameSaveService(); Army1Setup = new ArmySetupViewModel("Красная армия", _unitFactory); Army2Setup = new ArmySetupViewModel("Синяя армия", _unitFactory); SharedBudget = 100; var canStart = Observable.CombineLatest( Army1Setup.WhenAnyValue(a => a.UnitCount).StartWith(Army1Setup.UnitCount), Army2Setup.WhenAnyValue(a => a.UnitCount).StartWith(Army2Setup.UnitCount), (c1, c2) => c1 > 0 && c2 > 0); _canStart = false; canStart.Subscribe(ok => this.RaiseAndSetIfChanged(ref _canStart, ok, nameof(CanStart))); StartBattleCommand = ReactiveCommand.Create(() => ExecuteNonUndoable("Начать бой", StartBattle), canStart); BackToSetupCommand = ReactiveCommand.Create(() => ExecuteNonUndoable("Назад к настройке", BackToSetup)); BackToMenuCommand = ReactiveCommand.Create(() => ExecuteNonUndoable("Назад в меню", GoToMenu)); SaveArmiesCommand = ReactiveCommand.Create(() => ExecuteNonUndoable("Сохранить армии", SaveArmies)); LoadLatestSaveCommand = ReactiveCommand.Create(() => ExecuteNonUndoable("Загрузить сохранение", LoadLatestSave)); SetBothBudgetsCommand = ReactiveCommand.Create<object?>(SetBothBudgetsFromParameter); // Кнопка "Продолжить" активна только если есть хотя бы одно сохранение // WhenAnyValue на CurrentScreen — перепроверяем наличие сохранений при каждом возврате в меню var canContinue = this.WhenAnyValue(x => x.CurrentScreen) .Select(_ => _saveService.HasAnySave()); NewGameCommand = ReactiveCommand.Create(() => ExecuteNonUndoable("Новая игра", NewGame)); ContinueCommand = ReactiveCommand.Create(() => ExecuteNonUndoable("Продолжить игру", OpenSaveSlotScreen), canContinue); OpenSettingsCommand = ReactiveCommand.Create(() => ExecuteNonUndoable("Открыть настройки", OpenSettings)); ExitCommand = ReactiveCommand.Create(() => ExecuteNonUndoable("Выход", ExitApp)); BackFromSettingsCommand = ReactiveCommand.Create(() => ExecuteNonUndoable("Назад из настроек", GoToMenu)); // Загрузить настройки игры var s = GameSettings.Instance; _enableDamageLogging = s.EnableDamageLogging; _enableDeathSound = s.EnableDeathSound; _enableAutoSave = s.EnableAutoSave; CurrentScreen = Screen.Меню; } // ══════════════════════════════════════════════════════════════════ // Текущий экран // ══════════════════════════════════════════════════════════════════ private Screen _currentScreen; public Screen CurrentScreen { get => _currentScreen; private set => this.RaiseAndSetIfChanged(ref _currentScreen, value, nameof(CurrentScreen)); } // ══════════════════════════════════════════════════════════════════ // Армии и бой // ══════════════════════════════════════════════════════════════════ private bool _canStart; public bool CanStart => _canStart; public ArmySetupViewModel Army1Setup { get; } public ArmySetupViewModel Army2Setup { get; } private decimal _sharedBudget = 100; public decimal SharedBudget { get => _sharedBudget; set { this.RaiseAndSetIfChanged(ref _sharedBudget, value); Army1Setup.Budget = value; Army2Setup.Budget = value; } } private BattleViewModel? _battle; public BattleViewModel? Battle { get => _battle; private set => this.RaiseAndSetIfChanged(ref _battle, value, nameof(Battle)); } // ViewModel экрана выбора слота (создаётся при переходе на этот экран) private SaveSlotViewModel? _saveSlot; public SaveSlotViewModel? SaveSlot { get => _saveSlot; private set => this.RaiseAndSetIfChanged(ref _saveSlot, value, nameof(SaveSlot)); } // ══════════════════════════════════════════════════════════════════ // Команды боя // ══════════════════════════════════════════════════════════════════ public ReactiveCommand<RxUnit, RxUnit> StartBattleCommand { get; } public ReactiveCommand<RxUnit, RxUnit> BackToSetupCommand { get; } public ReactiveCommand<RxUnit, RxUnit> BackToMenuCommand { get; } public ReactiveCommand<RxUnit, RxUnit> SaveArmiesCommand { get; } public ReactiveCommand<RxUnit, RxUnit> LoadLatestSaveCommand { get; } public ReactiveCommand<object?, RxUnit> SetBothBudgetsCommand { get; } // ══════════════════════════════════════════════════════════════════ // Команды главного меню // ══════════════════════════════════════════════════════════════════ public ReactiveCommand<RxUnit, RxUnit> NewGameCommand { get; } public ReactiveCommand<RxUnit, RxUnit> ContinueCommand { get; } public ReactiveCommand<RxUnit, RxUnit> OpenSettingsCommand { get; } public ReactiveCommand<RxUnit, RxUnit> ExitCommand { get; } public ReactiveCommand<RxUnit, RxUnit> BackFromSettingsCommand { get; } // ══════════════════════════════════════════════════════════════════ // Игровые настройки // ══════════════════════════════════════════════════════════════════ private bool _enableDamageLogging; public bool EnableDamageLogging { get => _enableDamageLogging; set { this.RaiseAndSetIfChanged(ref _enableDamageLogging, value); SaveGameSettings(); } } private bool _enableDeathSound; public bool EnableDeathSound { get => _enableDeathSound; set { this.RaiseAndSetIfChanged(ref _enableDeathSound, value); SaveGameSettings(); } } private bool _enableAutoSave; public bool EnableAutoSave { get => _enableAutoSave; set { this.RaiseAndSetIfChanged(ref _enableAutoSave, value); SaveGameSettings(); } } private void SaveGameSettings() { var s = GameSettings.Instance; s.EnableDamageLogging = _enableDamageLogging; s.EnableDeathSound = _enableDeathSound; s.EnableAutoSave = _enableAutoSave; s.Save(); } // ══════════════════════════════════════════════════════════════════ // Навигация // ══════════════════════════════════════════════════════════════════ private void GoToMenu() => CurrentScreen = Screen.Меню; private void OpenSettings() => CurrentScreen = Screen.Настройки; private void OpenSaveSlotScreen() { var vm = new SaveSlotViewModel(_saveService, isSaveMode: false); // Коллбэк: пользователь выбрал слот → загружаем из него vm.OnSlotSelected = slot => ContinueFromSlot(slot); // Коллбэк: пользователь нажал «Назад» → возврат в меню vm.OnBack = GoToMenu; SaveSlot = vm; CurrentScreen = Screen.ВыборСохранения; } /// <summary>Открыть экран выбора слота в режиме сохранения (из боя).</summary> private void OpenSaveSlotForSaving() { var vm = new SaveSlotViewModel(_saveService, isSaveMode: true); // Коллбэк: пользователь выбрал слот → сохраняем в него и возвращаемся в бой vm.OnSlotSelected = slot => { _activeSlot = slot; Battle?.SaveToSlot(slot); CurrentScreen = Screen.Бой; }; // Коллбэк: пользователь нажал «Назад» → возврат в бой без сохранения vm.OnBack = () => CurrentScreen = Screen.Бой; SaveSlot = vm; CurrentScreen = Screen.ВыборСохранения; } private void NewGame() { Army1Setup.Clear(); Army2Setup.Clear(); Battle?.Dispose(); Battle = null; CurrentScreen = Screen.Настройка; } /// <summary>Загрузить игру из конкретного слота и перейти в бой.</summary> private void ContinueFromSlot(int slot) { try { var saveData = _saveService.LoadBattle(slot); if (saveData == null) { CurrentScreen = Screen.Настройка; return; } var armies = _saveService.BuildArmies(saveData); if (armies == null) { CurrentScreen = Screen.Настройка; return; } Battle?.Dispose(); Army1Setup.LoadFromArmy(armies.Value.Army1); Army2Setup.LoadFromArmy(armies.Value.Army2); Battle = new BattleViewModel(armies.Value.Army1, armies.Value.Army2, _activeSlot); // Запомнить активный слот для автосохранения _activeSlot = slot; Battle.TurnCompleted += (a1, a2) => { if (EnableAutoSave) _saveService.SaveBattle(_activeSlot, Battle.Battlefield, a1, a2, Battle.TurnNumber); }; Battle.ExitToMenuRequested += ExitBattleToMenu; // Кнопка «Сохранить» в бою открывает экран выбора слота Battle.SaveSlotRequested += OpenSaveSlotForSaving; // Восстановить HP и состояние юнитов из сохранения Battle.LoadFromSave(); CurrentScreen = Screen.Бой; } catch (Exception ex) { Console.WriteLine($"Ошибка загрузки слота {slot}: {ex.Message}"); CurrentScreen = Screen.Настройка; } } private void SaveArmies() { if (Battle != null) { _saveService.SaveBattle(_activeSlot, Battle.Battlefield, Battle.Army1, Battle.Army2, Battle.TurnNumber); return; } _saveService.SaveBattle(_activeSlot, null, Army1Setup.BuildArmy(), Army2Setup.BuildArmy(), 0); } private void StartBattle() { var army1 = Army1Setup.BuildArmy(); var army2 = Army2Setup.BuildArmy(); Battle?.Dispose(); Battle = new BattleViewModel(army1, army2, _activeSlot); Battle.TurnCompleted += (a1, a2) => { if (EnableAutoSave) _saveService.SaveBattle(_activeSlot, Battle.Battlefield, a1, a2, Battle.TurnNumber); }; Battle.ExitToMenuRequested += ExitBattleToMenu; // Кнопка «Сохранить» в бою открывает экран выбора слота Battle.SaveSlotRequested += OpenSaveSlotForSaving; CurrentScreen = Screen.Бой; } private void BackToSetup() { Battle?.Dispose(); Battle = null; CurrentScreen = Screen.Настройка; } private void ExitBattleToMenu() { Battle?.Dispose(); Battle = null; CurrentScreen = Screen.Меню; } // ══════════════════════════════════════════════════════════════════ // Загрузка последнего сохранения (используется из экрана настройки) // ══════════════════════════════════════════════════════════════════ public void LoadLatestSave() { // Ищем первый непустой слот for (int slot = 1; slot <= GameSaveService.SlotCount; slot++) { if (!_saveService.HasSave(slot)) continue; ContinueFromSlot(slot); return; } CurrentScreen = Screen.Настройка; } // ══════════════════════════════════════════════════════════════════ // Вспомогательные методы // ══════════════════════════════════════════════════════════════════ private void SetBothBudgetsFromParameter(object? param) { if (param == null) return; if (decimal.TryParse(param.ToString(), out var budget)) SetBothBudgets(budget); } private void SetBothBudgets(decimal budget) { Army1Setup.Budget = budget; Army2Setup.Budget = budget; } private static void ExitApp() { if (Avalonia.Application.Current is Avalonia.Application app) { if (app.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop) { desktop.Shutdown(); return; } } Environment.Exit(0); } private static void ExecuteNonUndoable(string name, Action action) => new ActionGameCommand(name, action).Execute(); } /// <summary>Перечисление экранов приложения.</summary> public enum Screen { Меню, Настройка, Бой, Настройки, ВыборСохранения }