/
Alexwww228
/
BridgeBattleSimulator
Обзор
Документация
Войти
/
Alexwww228
/
BridgeBattleSimulator
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
BattleSimulator.UI/ViewModels/ArmySetupViewModel.cs
287 строк
11 KB
Alexwww228
fix: Save system and Abstract Factory
24 май 2026, 01:45
24 май 2026, 01:45
7391992
Код
Авторство
О чём код?
using System.Collections.ObjectModel; using ReactiveUI; using BattleSimulator.Application.Builders; using BattleSimulator.Domain.Units; namespace BattleSimulator.UI.ViewModels; /// <summary>VM для строки «добавить юнита» (тип + имя).</summary> public sealed class AddUnitEntryViewModel : ReactiveObject { /// <summary>Выбранный тип юнита в ComboBox.</summary> public string SelectedType { get; set; } = "light"; /// <summary>Имя юнита (можно оставить пустым — сгенерируется авто).</summary> public string UnitName { get; set; } = ""; } /// <summary> /// ViewModel настройки армии. /// /// Паттерн MVVM: команды (ReactiveCommand) заменяют обработчики кликов. /// Паттерн Наблюдатель: коллекция Units уведомляет UI об изменениях. /// Бюджет проверяется в CanExecute AddUnitCommand — кнопка недоступна при нехватке золота. /// </summary> public sealed class ArmySetupViewModel : ReactiveObject { private readonly IUnitFactory _factory; private string _armyName; private decimal _budget = 100; /// <summary>Создать VM с фабрикой юнитов.</summary> public ArmySetupViewModel(string defaultName, IUnitFactory factory) { _factory = factory; _armyName = defaultName; // Заполнить каталог доступных типов foreach (var (key, (desc, cost)) in factory.AvailableTypes) Catalog.Add(new CatalogEntry(key, desc, cost)); UnitTypes = factory.AvailableTypes.Keys.ToList(); NewUnit = new AddUnitEntryViewModel { SelectedType = UnitTypes[0] }; _budget = 100; // Отслеживаем UnitCount через backing-поле для надёжных реактивных уведомлений _unitCount = Units.Count; Units.CollectionChanged += (_, _) => this.RaiseAndSetIfChanged(ref _unitCount, Units.Count); // Уведомить об начальном значении this.RaisePropertyChanged(nameof(UnitCount)); // Команда «Добавить» — недоступна при нехватке бюджета AddUnitCommand = ReactiveCommand.Create( AddUnit, this.WhenAnyValue( x => x.BudgetSpent, x => x.Budget, x => x.NewUnit.SelectedType, (spent, budget, type) => { if (!factory.AvailableTypes.TryGetValue(type ?? "", out var info)) return false; return (int)budget - spent >= info.Cost; }), outputScheduler: RxApp.MainThreadScheduler // <== важно ); RemoveUnitCommand = ReactiveCommand.Create<UnitListEntry>( RemoveUnit, outputScheduler: RxApp.MainThreadScheduler ); AutoFillCommand = ReactiveCommand.Create( AutoFill, outputScheduler: RxApp.MainThreadScheduler ); ClearCommand = ReactiveCommand.Create( Clear, outputScheduler: RxApp.MainThreadScheduler ); } // ── Bindable-свойства ──────────────────────────────────────────────── /// <summary>Название армии.</summary> public string ArmyName { get => _armyName; set => this.RaiseAndSetIfChanged(ref _armyName, value, nameof(ArmyName)); } /// <summary>Бюджет армии (decimal для NumericUpDown).</summary> public decimal Budget { get => _budget; set { this.RaiseAndSetIfChanged(ref _budget, value, nameof(Budget)); this.RaisePropertyChanged(nameof(BudgetRemaining)); this.RaisePropertyChanged(nameof(BudgetRemainingText)); } } /// <summary>Потраченный бюджет.</summary> public int BudgetSpent { get; private set; } /// <summary>Остаток бюджета (не уходит в минус при сбое ввода).</summary> public int BudgetRemaining => Math.Max(0, (int)Budget - BudgetSpent); /// <summary>Текст для полосы бюджета — без StringFormat, чтобы не ловить null.</summary> public string BudgetRemainingText => $"Остаток: {BudgetRemaining} золота"; /// <summary>Список добавленных юнитов.</summary> public ObservableCollection<UnitListEntry> Units { get; } = new(); /// <summary>Количество юнитов (для отслеживания в MainViewModel).</summary> private int _unitCount; public int UnitCount => _unitCount; /// <summary>Каталог доступных типов юнитов для отображения.</summary> public ObservableCollection<CatalogEntry> Catalog { get; } = new(); /// <summary>Список ключей типов для ComboBox.</summary> public List<string> UnitTypes { get; } /// <summary>Форма добавления нового юнита.</summary> public AddUnitEntryViewModel NewUnit { get; } // ── Команды (паттерн Command из MVVM) ─────────────────────────────── public ReactiveCommand<RxUnit, RxUnit> AddUnitCommand { get; } public ReactiveCommand<UnitListEntry, RxUnit> RemoveUnitCommand { get; } public ReactiveCommand<RxUnit, RxUnit> AutoFillCommand { get; } public ReactiveCommand<RxUnit, RxUnit> ClearCommand { get; } // ── Приватная логика ───────────────────────────────────────────────── private void AddUnit() { var type = NewUnit.SelectedType; var name = string.IsNullOrWhiteSpace(NewUnit.UnitName) ? $"{type}{Units.Count + 1}" : NewUnit.UnitName.Trim(); var cost = _factory.AvailableTypes[type].Cost; if (BudgetSpent + cost > (int)Budget) return; Units.Add(new UnitListEntry(type, name, cost)); BudgetSpent += cost; NewUnit.UnitName = ""; this.RaisePropertyChanged(nameof(BudgetRemaining)); this.RaisePropertyChanged(nameof(BudgetRemainingText)); } private void RemoveUnit(UnitListEntry entry) { if (Units.Remove(entry)) { BudgetSpent -= entry.Cost; this.RaisePropertyChanged(nameof(BudgetRemaining)); this.RaisePropertyChanged(nameof(BudgetRemainingText)); } } private void AutoFill() { Clear(); var rng = new Random(); var types = UnitTypes.ToArray(); var unitInfos = _factory.AvailableTypes; int counter = 1; while (true) { int remaining = (int)Budget - BudgetSpent; if (remaining <= 0) break; // Выбираем случайный тип var type = types[rng.Next(types.Length)]; var cost = unitInfos[type].Cost; if (cost <= remaining) { Units.Add(new UnitListEntry(type, $"{type}{counter++}", cost)); BudgetSpent += cost; } else { // Ищем самый дешёвый юнит, который помещается в остаток var cheapest = unitInfos .Where(x => x.Value.Cost <= remaining) .OrderBy(x => x.Value.Cost) .FirstOrDefault(); if (cheapest.Key == null) break; // Ничего больше добавить нельзя Units.Add(new UnitListEntry(cheapest.Key, $"{cheapest.Key}{counter++}", cheapest.Value.Cost)); BudgetSpent += cheapest.Value.Cost; } } this.RaisePropertyChanged(nameof(BudgetRemaining)); this.RaisePropertyChanged(nameof(BudgetRemainingText)); this.RaisePropertyChanged(nameof(UnitCount)); } public void Clear() { Units.Clear(); BudgetSpent = 0; this.RaisePropertyChanged(nameof(BudgetRemaining)); this.RaisePropertyChanged(nameof(BudgetRemainingText)); } /// <summary> /// Загрузить состояние армии из доменного объекта в VM (для UI). /// Попытка выбрать ключ фабрики по отображаемому имени типа. /// </summary> public void LoadFromArmy(Army army) { ArmyName = army.ArmyName; Budget = Math.Max(10, army.Budget); Units.Clear(); BudgetSpent = 0; foreach (var u in army.Units) { var key = ResolveFactoryKey(u.UnitType); var cost = u.IsArmyClone ? 0 : _factory.AvailableTypes[key].Cost; Units.Add(new UnitListEntry(key, u.Name, cost)); BudgetSpent += cost; } this.RaisePropertyChanged(nameof(BudgetRemaining)); this.RaisePropertyChanged(nameof(BudgetRemainingText)); this.RaisePropertyChanged(nameof(UnitCount)); } private string ResolveFactoryKey(string unitType) { var t = unitType.Trim(); var lower = t.ToLowerInvariant(); string? mapped = lower switch { "тяжёлая пехота" or "тяжелая пехота" or "heavy" => "heavy", "лёгкая пехота" or "легкая пехота" or "light" => "light", "лучник" or "archer" => "archer", "healer" or "целитель" => "healer", "wizard" or "маг" => "wizard", "стена" or "wall" or "walkingwalladapter" => "wall", _ => null }; if (mapped != null && _factory.AvailableTypes.ContainsKey(mapped)) return mapped; var fromDesc = UnitTypes.FirstOrDefault(k => _factory.AvailableTypes.TryGetValue(k, out var info) && (info.Description.StartsWith(t, StringComparison.OrdinalIgnoreCase) || info.Description.Contains(t, StringComparison.OrdinalIgnoreCase))); if (fromDesc != null) return fromDesc; return _factory.AvailableTypes.Keys.First(); } /// <summary> /// Создать объект Army доменного слоя на основе текущего состояния VM. /// Вызывается перед стартом боя. /// </summary> public Army BuildArmy() { var army = new Army(ArmyName, (int)Budget); foreach (var entry in Units) army.ForceAdd(_factory.Create(entry.Type, entry.Name)); return army; } } /// <summary>Запись в списке юнитов армии.</summary> public record UnitListEntry(string Type, string Name, int Cost); /// <summary>Запись каталога типов юнитов для отображения.</summary> public record CatalogEntry(string Key, string Description, int Cost);