/
romashov.z
/
PPAGame
Обзор
Документация
Войти
/
romashov.z
/
PPAGame
Код
Запросы
1
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
gameUI-clean
PPAGameUI/ViewModels/BattleViewModel.cs
419 строк
14 KB
Artemiy Ivanov
fix: bugs
02 июн 2026, 14:59
02 июн 2026, 14:59
787ca82
Код
Авторство
О чём код?
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using System.Windows.Input; using ArmyBattleGame; using PPAGameUI; namespace PPAGameUI.ViewModels; public class BattleViewModel : INotifyPropertyChanged { private readonly BattleField _battle; private readonly GameSaveService _saveService; private readonly CommandHistory _history; private readonly BattleObservers _observers; private string? _currentSavePath; private readonly double _budget; public ArmyViewModel Army1 { get; } public ArmyViewModel Army2 { get; } public ObservableCollection<string> BattleLog { get; } = new(); public ObservableCollection<MeleePairViewModel> MeleePairs { get; } = new(); private string _statusText = ""; public string StatusText { get => _statusText; private set { _statusText = value; OnPropertyChanged(); } } private bool _battleEnded = false; public bool BattleEnded { get => _battleEnded; private set { _battleEnded = value; OnPropertyChanged(); } } private bool _isFightingToEnd = false; public bool IsFightingToEnd { get => _isFightingToEnd; private set { _isFightingToEnd = value; OnPropertyChanged(); OnPropertyChanged(nameof(CanAct)); } } // Кнопки управления активны только когда не идёт автобой public bool CanAct => !BattleEnded && !IsFightingToEnd; private string _currentStrategy = ""; public string CurrentStrategy { get => _currentStrategy; private set { _currentStrategy = value; OnPropertyChanged(); } } private string _lastActionText = ""; public string LastActionText { get => _lastActionText; private set { _lastActionText = value; OnPropertyChanged(); } } private string _winnerText = ""; public string WinnerText { get => _winnerText; private set { _winnerText = value; OnPropertyChanged(); } } private string _winnerEmoji = ""; public string WinnerEmoji { get => _winnerEmoji; private set { _winnerEmoji = value; OnPropertyChanged(); } } public bool CanUndo => _history.CanUndo; public bool CanRedo => _history.CanRedo; public ICommand MakeMoveCommand { get; } public ICommand FightToEndCommand { get; } public ICommand UndoCommand { get; } public ICommand RedoCommand { get; } public ICommand ResetCommand { get; } public ICommand ChangeStrategyCommand { get; } public ICommand SaveAndExitCommand { get; } public ICommand CopyLogCommand { get; } public event Action? ExitRequested; public event Action<string>? CopyToClipboardRequested; private readonly string _army1Name; private readonly string _army2Name; public BattleViewModel(BattleField battle, GameSaveService saveService, double budget, BattleObservers observers, string army1Name = "Армия 1", string army2Name = "Армия 2", string? savePath = null, List<string>? savedLog = null) { _battle = battle; _saveService = saveService; _budget = budget; _observers = observers; _currentSavePath = savePath; _army1Name = army1Name; _army2Name = army2Name; _history = new CommandHistory(); var initialMemento = _battle.CreateMemento(_saveService); _history.SaveInitialState(initialMemento); // При загрузке сохранения кладём текущее состояние в undo-стек — // чтобы после первого хода можно было вернуться к моменту загрузки. if (savePath != null) _history.RecordBeforeCommand(initialMemento); Army1 = new ArmyViewModel(_battle.Army1, army1Name); Army2 = new ArmyViewModel(_battle.Army2, army2Name); if (savedLog != null) foreach (var entry in savedLog) BattleLog.Add(entry); MakeMoveCommand = new RelayCommand(ExecuteMove); FightToEndCommand = new RelayCommand(ExecuteFightToEnd); UndoCommand = new RelayCommand(ExecuteUndo); RedoCommand = new RelayCommand(ExecuteRedo); ResetCommand = new RelayCommand(ExecuteReset); ChangeStrategyCommand = new RelayCommand<string>(ExecuteChangeStrategy); SaveAndExitCommand = new RelayCommand(ExecuteSaveAndExit); CopyLogCommand = new RelayCommand(ExecuteCopyLog); UpdateStatus(); UpdateMeleePairs(); } private void ExecuteMove() { var cmd = new MakeMoveCommand(_battle, _history, _saveService); cmd.Execute(); AddLog($"=== Ход {_battle.TurnCounter - 1} завершён ==="); RefreshAll(); if (!cmd.BattleContinues) { BattleEnded = true; ShowWinner(); DeleteCurrentSave(); } else { Autosave(); } } private async void ExecuteFightToEnd() { if (IsFightingToEnd) return; IsFightingToEnd = true; const int maxTurns = 5000; // защита от бесконечного боя (Гуляй-город vs Гуляй-город и т.п.) const int batchSize = 30; // ходов за один «тик» UI, потом отдаём управление int processed = 0; while (processed < maxTurns) { // Пакет ходов — всё на UI-потоке, но после каждых batchSize отдаём управление for (int i = 0; i < batchSize && processed < maxTurns; i++, processed++) { var cmd = new MakeMoveCommand(_battle, _history, _saveService); cmd.Execute(); AddLog($"=== Ход {_battle.TurnCounter - 1} завершён ==="); if (!cmd.BattleContinues) { BattleEnded = true; IsFightingToEnd = false; RefreshAll(); ShowWinner(); DeleteCurrentSave(); return; } } // Даём UI обновить лог и армии, потом продолжаем RefreshAll(); await System.Threading.Tasks.Task.Yield(); } // Сюда доходим только если за 5000 ходов победитель не определён AddLog($"⚠️ Бой остановлен: превышен лимит {maxTurns} ходов. Возможно, ни одна армия не наносит урон."); IsFightingToEnd = false; RefreshAll(); } private void ExecuteUndo() { var current = _battle.CreateMemento(_saveService); var target = _history.Undo(current); if (target == null) return; RestoreTo(target); AddLog("↩ Ход отменён"); Autosave(); } private void ExecuteRedo() { var current = _battle.CreateMemento(_saveService); var target = _history.Redo(current); if (target == null) return; RestoreTo(target); AddLog("↪ Ход повторён"); Autosave(); } private void ExecuteReset() { var target = _history.Reset(); if (target == null) return; RestoreTo(target); BattleEnded = false; AddLog("⟳ Бой сброшен в начало"); Autosave(); } public void ExecuteChangeStrategy(string? strategyName) { if (strategyName == null) return; var strategy = ArmyBattleGame.Game.CreateStrategy(strategyName); var cmd = new ChangeFormationCommand(_battle, _history, _saveService, strategy); cmd.Execute(); AddLog($"📋 Построение изменено: {strategy.DisplayName}"); RefreshAll(); Autosave(); } private void RestoreTo(BattleFieldMemento memento) { _observers.Detach(_battle.Army1); _observers.Detach(_battle.Army2); _battle.RestoreFromMemento(memento, _saveService); _observers.Attach(_battle.Army1); _observers.Attach(_battle.Army2); RefreshAll(); } private void RefreshAll() { Army1.RebuildGrid(); Army2.RebuildGrid(); UpdateStatus(); UpdateMeleePairs(); OnPropertyChanged(nameof(CanUndo)); OnPropertyChanged(nameof(CanRedo)); } private void UpdateStatus() { CurrentStrategy = _battle.CurrentStrategy.DisplayName; int alive1 = Army1.AliveCount; int alive2 = Army2.AliveCount; StatusText = $"Ход {_battle.TurnCounter} | " + $"Армия 1: {alive1} живых | " + $"Армия 2: {alive2} живых"; } private void UpdateMeleePairs() { MeleePairs.Clear(); int cols1 = _battle.Army1.ColumnCount; int cols2 = _battle.Army2.ColumnCount; int cols = Math.Max(cols1, cols2); for (int c = 0; c < cols; c++) { // GetFront не проверяет границы, поэтому проверяем сами IUnit? u1 = c < cols1 ? _battle.Army1.GetFront(c) : null; IUnit? u2 = c < cols2 ? _battle.Army2.GetFront(c) : null; if (u1 == null && u2 == null) continue; MeleePairs.Add(new MeleePairViewModel(u1, u2, cols > 1 ? c + 1 : 0)); } } private void ShowWinner() { bool a1 = _battle.Army1.HasAliveUnits; bool a2 = _battle.Army2.HasAliveUnits; (WinnerEmoji, WinnerText) = (a1, a2) switch { (true, false) => ("🏆", $"Победила {_army1Name}!"), (false, true) => ("🏆", $"Победила {_army2Name}!"), _ => ("🤝", "Ничья!\nВсе погибли") }; string logMsg = $"{WinnerEmoji} {WinnerText}"; StatusText = logMsg; AddLog(logMsg); } public void ConnectLogger(GuiBattleLogger logger) { logger.Logged += AddLog; } private void AddLog(string message) { BattleLog.Insert(0, message); if (message.StartsWith("⚔️") || message.StartsWith("🏹") || message.StartsWith("💀") || message.StartsWith("💚") || message.StartsWith("🔮") || message.StartsWith("🗡️")) { LastActionText = message; } if (BattleLog.Count > 1000) BattleLog.RemoveAt(BattleLog.Count - 1); } private void ExecuteCopyLog() { // BattleLog хранит записи newest-first, для копирования разворачиваем var text = string.Join("\n", BattleLog.Reverse()); CopyToClipboardRequested?.Invoke(text); } private void Autosave() { var saveData = new GameSaveData { Budget = _budget, TurnCounter = _battle.TurnCounter, IsArmy1Turn = _battle.IsArmy1Turn, FormationStrategy = _battle.CurrentStrategy.Name, Army1 = _saveService.SerializeArmy(_battle.Army1), Army2 = _saveService.SerializeArmy(_battle.Army2), Army1Name = _army1Name, Army2Name = _army2Name, BattleLog = new List<string>(BattleLog), }; if (_currentSavePath != null && System.IO.File.Exists(_currentSavePath)) _saveService.Overwrite(_currentSavePath, saveData); else _currentSavePath = _saveService.Save(saveData); } private void DeleteCurrentSave() { if (_currentSavePath != null) _saveService.DeleteSave(_currentSavePath); } private void ExecuteSaveAndExit() { Autosave(); ExitRequested?.Invoke(); } public event PropertyChangedEventHandler? PropertyChanged; private void OnPropertyChanged([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } // Пара юнитов в рукопашном бою (один слот по колонке) public class MeleePairViewModel { public string Army1Text { get; } public string Army2Text { get; } public string ColLabel { get; } public MeleePairViewModel(IUnit? u1, IUnit? u2, int colNumber) { ColLabel = colNumber > 1 ? $"Колонна {colNumber}" : ""; Army1Text = u1 != null ? $"{UnitViewModel.GetIcon(u1)} {u1.DisplayName} HP {u1.Health:F0}" : "— нет юнита —"; Army2Text = u2 != null ? $"{UnitViewModel.GetIcon(u2)} {u2.DisplayName} HP {u2.Health:F0}" : "— нет юнита —"; } } // Простая реализация ICommand public class RelayCommand : ICommand { private readonly Action _execute; private readonly Func<bool>? _canExecute; public RelayCommand(Action execute, Func<bool>? canExecute = null) { _execute = execute; _canExecute = canExecute; } public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true; public void Execute(object? parameter) => _execute(); public event EventHandler? CanExecuteChanged; public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); } public class RelayCommand<T> : ICommand { private readonly Action<T?> _execute; public RelayCommand(Action<T?> execute) => _execute = execute; public bool CanExecute(object? parameter) => true; public void Execute(object? parameter) => _execute(parameter is T t ? t : default); public event EventHandler? CanExecuteChanged; }