/
Yaroslav4444-bauer
/
ArmyFight
Обзор
Документация
Войти
/
Yaroslav4444-bauer
/
ArmyFight
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
ViewModels/BattleViewModel.cs
500 строк
19 KB
Yaroslav4444-bauer
Добовлены комментарии summary
01 июн 2026, 12:38
01 июн 2026, 12:38
70196b7
Код
Авторство
О чём код?
using System.Collections.ObjectModel; using System.Windows; using System.Windows.Input; using System.Windows.Media; using ArmyFight.Services; using WpfCommand = System.Windows.Input.ICommand; namespace ArmyFight.ViewModels { /// <summary> /// Одна строка журнала боя с цветом для отображения в ListBox. /// </summary> public class BattleLogEntry { public string Text { get; } public Brush Brush { get; } /// <summary> /// Создаёт запись журнала, преобразуя цвет консоли в кисть WPF. /// </summary> /// <param name="text">Текст сообщения.</param> /// <param name="color">Цвет из консольного вывода.</param> public BattleLogEntry(string text, ConsoleColor color) { Text = text; Brush = color switch { ConsoleColor.Red => new SolidColorBrush(Color.FromRgb(255, 107, 107)), ConsoleColor.Green => new SolidColorBrush(Color.FromRgb(129, 199, 132)), ConsoleColor.Yellow => new SolidColorBrush(Color.FromRgb(255, 213, 79)), ConsoleColor.Cyan => new SolidColorBrush(Color.FromRgb(126, 200, 227)), ConsoleColor.Magenta => new SolidColorBrush(Color.FromRgb(186, 104, 200)), ConsoleColor.Blue => new SolidColorBrush(Color.FromRgb(144, 202, 249)), ConsoleColor.DarkGray => new SolidColorBrush(Color.FromRgb(120, 128, 150)), _ => new SolidColorBrush(Color.FromRgb(220, 226, 240)) }; } } /// <summary> /// Одна строка раскладки «бойцы напротив»: юнит армии 1 и юнит армии 2. /// </summary> public class FightPairViewModel { public UnitCardViewModel? Left { get; } public UnitCardViewModel? Right { get; } /// <summary> /// Создаёт пару карточек для режимов 3v3 и linetoline. /// </summary> /// <param name="left">Боец армии 1 или null.</param> /// <param name="right">Боец армии 2 или null.</param> public FightPairViewModel(UnitCardViewModel? left, UnitCardViewModel? right) { Left = left; Right = right; } public bool HasLeft => Left != null; public bool HasRight => Right != null; } /// <summary> /// Модель экрана боя: армии, журнал, ходы, отмена/повтор, сохранение и смена стратегии. /// </summary> public class BattleViewModel : BaseViewModel { private readonly GameService _svc; private readonly MainViewModel _nav; // Horizontal mode (1v1) public ObservableCollection<UnitCardViewModel> Army1 { get; } = new(); public ObservableCollection<UnitCardViewModel> Army2 { get; } = new(); // Row mode (3v3, linetoline): active pairs and reserve public ObservableCollection<FightPairViewModel> FightPairs { get; } = new(); public ObservableCollection<UnitCardViewModel> Army1Reserve { get; } = new(); public ObservableCollection<UnitCardViewModel> Army2Reserve { get; } = new(); public ObservableCollection<BattleLogEntry> Log { get; } = new(); // Layout mode — follows current battle strategy private bool _isHorizontalLayout; public bool IsHorizontalLayout { get => _isHorizontalLayout; private set => Set(ref _isHorizontalLayout, value); } private bool _isRowLayout; public bool IsRowLayout { get => _isRowLayout; private set => Set(ref _isRowLayout, value); } private bool _isLineToLine; public bool IsLineToLine { get => _isLineToLine; private set => Set(ref _isLineToLine, value); } public bool HasReserve => Army1Reserve.Count > 0 || Army2Reserve.Count > 0; private bool _isBusy; public bool IsBusy { get => _isBusy; private set { Set(ref _isBusy, value); Application.Current.Dispatcher.BeginInvoke(CommandManager.InvalidateRequerySuggested); } } private bool _isGameOver; public bool IsGameOver { get => _isGameOver; private set => Set(ref _isGameOver, value); } private string _winnerText = string.Empty; public string WinnerText { get => _winnerText; private set => Set(ref _winnerText, value); } private string _statusText = "Подготовка к бою..."; public string StatusText { get => _statusText; private set => Set(ref _statusText, value); } public int TurnNumber => _svc.TurnNumber; public bool CanUndo => _svc.CanUndo && !IsBusy && !IsGameOver; public bool CanRedo => _svc.CanRedo && !IsBusy; // ── Skip input ────────────────────────────────────────── private bool _isSkipInputVisible; public bool IsSkipInputVisible { get => _isSkipInputVisible; private set => Set(ref _isSkipInputVisible, value); } private string _skipCountText = "5"; public string SkipCountText { get => _skipCountText; set => Set(ref _skipCountText, value); } // ── Save dialog ────────────────────────────────────────── private bool _isSaveDialogVisible; public bool IsSaveDialogVisible { get => _isSaveDialogVisible; private set => Set(ref _isSaveDialogVisible, value); } private string _saveName = "Сохранение"; public string SaveName { get => _saveName; set => Set(ref _saveName, value); } // ── Strategy change dialog ─────────────────────────────── private bool _isStrategyDialogVisible; public bool IsStrategyDialogVisible { get => _isStrategyDialogVisible; private set => Set(ref _isStrategyDialogVisible, value); } private bool _strategyPick1to1 = true; public bool StrategyPick1to1 { get => _strategyPick1to1; set => Set(ref _strategyPick1to1, value); } private bool _strategyPick3to3; public bool StrategyPick3to3 { get => _strategyPick3to3; set => Set(ref _strategyPick3to3, value); } private bool _strategyPickLineToLine; public bool StrategyPickLineToLine { get => _strategyPickLineToLine; set => Set(ref _strategyPickLineToLine, value); } // ── Settings dialog ────────────────────────────────────── private bool _isSettingsDialogVisible; public bool IsSettingsDialogVisible { get => _isSettingsDialogVisible; private set => Set(ref _isSettingsDialogVisible, value); } private bool _isSoundEnabled = GameSettings.SoundEnabled; public bool IsSoundEnabled { get => _isSoundEnabled; set { if (Set(ref _isSoundEnabled, value)) GameSettings.SoundEnabled = value; } } private bool _isLoggingEnabled = GameSettings.LoggingEnabled; public bool IsLoggingEnabled { get => _isLoggingEnabled; set { if (Set(ref _isLoggingEnabled, value)) GameSettings.LoggingEnabled = value; } } // ── Commands ───────────────────────────────────────────── public WpfCommand NextTurnCommand { get; } public WpfCommand ShowSkipCommand { get; } public WpfCommand CancelSkipCommand { get; } public WpfCommand ConfirmSkipCommand { get; } public WpfCommand PlayToEndCommand { get; } public WpfCommand UndoCommand { get; } public WpfCommand RedoCommand { get; } public WpfCommand ShowSaveCommand { get; } public WpfCommand ConfirmSaveCommand { get; } public WpfCommand CancelSaveCommand { get; } public WpfCommand ChangeStrategy { get; } public WpfCommand CancelStrategyCommand { get; } public WpfCommand ConfirmStrategyCommand { get; } public WpfCommand ShowSettingsCommand { get; } public WpfCommand CloseSettingsCommand { get; } public WpfCommand ExitToMenuCommand { get; } /// <summary> /// Подписывается на события сервиса, настраивает команды и выполняет первичное обновление UI. /// </summary> /// <param name="svc">Сервис игры.</param> /// <param name="nav">Навигация для выхода в меню.</param> public BattleViewModel(GameService svc, MainViewModel nav) { _svc = svc; _nav = nav; UpdateLayoutFromStrategy(); svc.OnStateChanged += RefreshAll; svc.OnLog += AddLogEntry; NextTurnCommand = new AsyncRelayCommand( async () => await ExecuteTurnsAsync(1), () => !IsBusy && !IsGameOver); ShowSkipCommand = new RelayCommand( () => { IsSkipInputVisible = true; }, () => !IsBusy && !IsGameOver); CancelSkipCommand = new RelayCommand(() => IsSkipInputVisible = false); ConfirmSkipCommand = new AsyncRelayCommand(async () => { IsSkipInputVisible = false; int n = int.TryParse(SkipCountText, out var v) ? Math.Max(1, v) : 1; await ExecuteTurnsAsync(n); }, () => !IsBusy && !IsGameOver); PlayToEndCommand = new AsyncRelayCommand( async () => await ExecuteTurnsAsync(int.MaxValue), () => !IsBusy && !IsGameOver); UndoCommand = new RelayCommand(() => { _svc.Undo(); IsGameOver = false; }, () => CanUndo); RedoCommand = new RelayCommand( () => _svc.Redo(), () => CanRedo); ShowSaveCommand = new RelayCommand(() => IsSaveDialogVisible = true, () => !IsBusy); CancelSaveCommand = new RelayCommand(() => IsSaveDialogVisible = false); ConfirmSaveCommand = new RelayCommand(() => { IsSaveDialogVisible = false; string name = string.IsNullOrWhiteSpace(SaveName) ? "Сохранение" : SaveName; _svc.Save(name); }); ChangeStrategy = new RelayCommand( () => { SyncStrategyPicksFromCurrent(); IsStrategyDialogVisible = true; }, () => !IsBusy); CancelStrategyCommand = new RelayCommand(() => IsStrategyDialogVisible = false); ConfirmStrategyCommand = new RelayCommand(() => { string strategy = StrategyPick3to3 ? "3to3" : StrategyPickLineToLine ? "linetoline" : "1to1"; IsStrategyDialogVisible = false; _svc.ChangeStrategy(strategy); }); ShowSettingsCommand = new RelayCommand(() => { SyncSettingsFromCurrent(); IsSettingsDialogVisible = true; }); CloseSettingsCommand = new RelayCommand(() => IsSettingsDialogVisible = false); ExitToMenuCommand = new RelayCommand(() => { svc.OnStateChanged -= RefreshAll; svc.OnLog -= AddLogEntry; nav.ShowMenu(); }); RefreshAll(); } /// <summary> /// Устанавливает флаги раскладки UI по текущей стратегии боя. /// </summary> private void UpdateLayoutFromStrategy() { IsHorizontalLayout = GameStatements.BATTLE_STRATEGY_NAME == "1to1"; IsRowLayout = !IsHorizontalLayout; IsLineToLine = GameStatements.BATTLE_STRATEGY_NAME == "linetoline"; } /// <summary> /// Синхронизирует переключатели диалога смены стратегии с активным режимом. /// </summary> private void SyncStrategyPicksFromCurrent() => Application.Current.Dispatcher.Invoke(() => { string name = GameStatements.BATTLE_STRATEGY_NAME; StrategyPick1to1 = name == "1to1"; StrategyPick3to3 = name == "3to3"; StrategyPickLineToLine = name == "linetoline"; }); /// <summary> /// Подтягивает звук и логирование из глобальных настроек в диалог настроек. /// </summary> private void SyncSettingsFromCurrent() { IsSoundEnabled = GameSettings.SoundEnabled; IsLoggingEnabled = GameSettings.LoggingEnabled; } /// <summary> /// Полностью обновляет армии, пары боя, статус и доступность команд на UI-потоке. /// </summary> private void RefreshAll() => Application.Current.Dispatcher.Invoke(() => { UpdateLayoutFromStrategy(); RefreshArmy(Army1, _svc.Army1, _svc.ActiveFrontCount, false); RefreshArmy(Army2, _svc.Army2, _svc.ActiveFrontCount, true); RefreshFightPairs(); bool over = _svc.IsGameOver || _svc.IsDrawn; IsGameOver = over; WinnerText = _svc.IsDrawn ? "Ничья — слишком долгое сражение!" : !string.IsNullOrEmpty(_svc.Winner) ? $"Победа: {_svc.Winner}!" : string.Empty; StatusText = over ? WinnerText : $"Ход {_svc.TurnNumber} | Армия 1: {_svc.Army1?.Infantrymen.Count ?? 0} vs Армия 2: {_svc.Army2?.Infantrymen.Count ?? 0}"; Notify(nameof(TurnNumber)); Notify(nameof(CanUndo)); Notify(nameof(CanRedo)); // Ensure Undo/Redo buttons re-evaluate CanExecute after state changes Application.Current.Dispatcher.BeginInvoke(CommandManager.InvalidateRequerySuggested); }); /// <summary> /// Пересобирает коллекцию карточек одной армии с отметкой активного фронта и разделителя. /// </summary> /// <param name="col">Целевая коллекция UI.</param> /// <param name="army">Игровая армия.</param> /// <param name="frontCount">Число юнитов на фронте.</param> /// <param name="faceLeft">Зеркалить карточки (армия 2).</param> private static void RefreshArmy( ObservableCollection<UnitCardViewModel> col, IArmy? army, int frontCount, bool faceLeft) { col.Clear(); if (army == null) return; int count = army.Infantrymen.Count; for (int i = 0; i < count; i++) { bool isActive = i < frontCount; bool showSep = frontCount < count && i == frontCount - 1; col.Add(UnitCardViewModel.FromUnit(army.Infantrymen[i], isActive, faceLeft, showSep)); } } /// <summary> /// Обновляет пары на фронте и коллекции резерва для режимов 3v3 и linetoline. /// </summary> private void RefreshFightPairs() { FightPairs.Clear(); Army1Reserve.Clear(); Army2Reserve.Clear(); var a1 = _svc.Army1; var a2 = _svc.Army2; if (a1 == null || a2 == null) return; int frontCount = _svc.ActiveFrontCount; int a1Count = a1.Infantrymen.Count; int a2Count = a2.Infantrymen.Count; // Number of paired rows to show (all pairs for linetoline, up to frontCount for 3v3) int activeCount = frontCount == int.MaxValue ? Math.Max(a1Count, a2Count) : Math.Min(frontCount, Math.Max(a1Count, a2Count)); for (int i = 0; i < activeCount; i++) { UnitCardViewModel? left = i < a1Count ? UnitCardViewModel.FromUnit(a1.Infantrymen[i], true, false) : null; UnitCardViewModel? right = i < a2Count ? UnitCardViewModel.FromUnit(a2.Infantrymen[i], true, true) : null; FightPairs.Add(new FightPairViewModel(left, right)); } // Reserve (3v3 only — linetoline has no reserve) if (frontCount != int.MaxValue) { for (int i = frontCount; i < a1Count; i++) Army1Reserve.Add(UnitCardViewModel.FromUnit(a1.Infantrymen[i], false, false)); for (int i = frontCount; i < a2Count; i++) Army2Reserve.Add(UnitCardViewModel.FromUnit(a2.Infantrymen[i], false, true)); } Notify(nameof(HasReserve)); } /// <summary> /// Добавляет строку в журнал боя на UI-потоке. /// </summary> /// <param name="text">Текст.</param> /// <param name="color">Цвет консоли.</param> private void AddLogEntry(string text, ConsoleColor color) => Application.Current.Dispatcher.Invoke(() => { if (!string.IsNullOrWhiteSpace(text)) Log.Add(new BattleLogEntry(text, color)); }); /// <summary> /// Выполняет заданное число ходов подряд с короткой паузой между ними при пакетном пропуске. /// </summary> /// <param name="count">Число ходов; int.MaxValue — до конца боя.</param> private async Task ExecuteTurnsAsync(int count) { IsBusy = true; try { for (int i = 0; i < count; i++) { if (_svc.IsGameOver || _svc.IsDrawn) break; await _svc.ExecuteTurnAsync(); if (count > 1) await Task.Delay(30); } } finally { IsBusy = false; } } } }