using System.Collections.ObjectModel; using System.ComponentModel; using System.Globalization; using System.Windows; using RQOverlay.Core; using RQOverlay.Plugins; namespace RQOverlay; /// Состояние текущей сессии фарма: время, показатели включённых плагинов, итог. public sealed class SessionViewModel : Observable { readonly PriceBook _prices; readonly PluginManager _plugins; DateTime _start = DateTime.Now; public SessionViewModel(PriceBook prices, PluginManager plugins) { _prices = prices; _plugins = plugins; _plugins.Changed += Rebuild; Rebuild(); } /// /// Ручная корректировка заработка в золоте (например, минус потраченное на зелья). /// Показывается в оверлее отдельной строкой, только когда не ноль. /// public Metric Adjustment { get; } = new("Корректировка", "#9AA3B5", isGold: true); public string AdjustmentText { get => Adjustment.Value == 0 ? "" : Adjustment.Value.ToString("0.##", CultureInfo.CurrentCulture); set { string s = (value ?? "").Trim().Replace(" ", "").Replace(" ", "").Replace(',', '.'); decimal v; if (s.Length == 0 || s == "-") v = 0; else if (!decimal.TryParse(s, NumberStyles.Number, CultureInfo.InvariantCulture, out v)) return; bool wasShown = Adjustment.Value != 0; Adjustment.Value = v; Raise(); if (wasShown != (v != 0)) Rebuild(); else RaiseTotals(); } } /// Показатели всех включённых плагинов, по порядку. public ObservableCollection Metrics { get; } = new(); /// Золотые показатели — для полоски долей. public ObservableCollection GoldMetrics { get; } = new(); /// Дополнительные блоки плагинов в оверлее (список добычи и т.п.). public ObservableCollection OverlayViews { get; } = new(); decimal TotalGold => GoldMetrics.Sum(m => m.Value); double Hours => (DateTime.Now - _start).TotalHours; public bool HasGold => GoldMetrics.Count > 0; public bool HasIncome => TotalGold > 0; public string Total => Fmt.Gold(TotalGold); public string PerHour => // Первую минуту темп скачет слишком сильно, чтобы что-то значить. Hours < 1.0 / 60 ? "— / ч" : Fmt.Gold(TotalGold / (decimal)Hours) + " / ч"; public string Elapsed { get { var t = DateTime.Now - _start; return $"{(int)t.TotalHours}:{t.Minutes:00}:{t.Seconds:00}"; } } /// Оверлей пропускает клики мыши в игру. bool _clickThrough; public bool ClickThrough { get => _clickThrough; set => Set(ref _clickThrough, value); } bool _listExpanded = true; public bool ListExpanded { get => _listExpanded; set => Set(ref _listExpanded, value); } bool _connected; public bool Connected { get => _connected; set => Set(ref _connected, value); } string _status = "Запуск…"; public string Status { get => _status; set => Set(ref _status, value); } /// Раз в секунду: таймер и темп. public void Tick() { double hours = Hours; foreach (var m in Metrics) m.UpdateRate(hours); Raise(nameof(Elapsed)); Raise(nameof(PerHour)); } public void Reset() { _start = DateTime.Now; _plugins.ResetSession(); AdjustmentText = ""; Tick(); } /// Первый снимок — история до запуска оверлея. /// Засчитывать ли историю в сессию (флаг --history). public void Apply(IEnumerable entries, bool initial, bool countHistory) { foreach (var entry in entries) { var events = GameEvents.Parse(entry); if (events.Count == 0) continue; // Цены учим из любых продаж, в том числе из истории лога. foreach (var s in events.OfType()) _prices.LearnFromSale(s.Name, s.Qty, s.Total); if (!initial) _plugins.Dispatch(entry, events, isLive: true); else if (countHistory) _plugins.Dispatch(entry, events, isLive: false); } } /// Набор включённых плагинов изменился — пересобираем оверлей. void Rebuild() { foreach (var m in Metrics) m.PropertyChanged -= OnMetricChanged; Metrics.Clear(); GoldMetrics.Clear(); OverlayViews.Clear(); foreach (var plugin in _plugins.Enabled) { foreach (var m in plugin.Metrics) { Metrics.Add(m); if (m.IsGold) GoldMetrics.Add(m); m.PropertyChanged += OnMetricChanged; } try { if (plugin.CreateOverlayView() is { } view) OverlayViews.Add(view); } catch { /* сломанный блок плагина просто не показываем */ } } if (Adjustment.Value != 0) { Metrics.Add(Adjustment); GoldMetrics.Add(Adjustment); Adjustment.PropertyChanged += OnMetricChanged; } Tick(); RaiseTotals(); Raise(nameof(HasGold)); } void OnMetricChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(Metric.Value) && sender is Metric { IsGold: true }) RaiseTotals(); } void RaiseTotals() { Raise(nameof(Total)); Raise(nameof(PerHour)); Raise(nameof(HasIncome)); } } public static class Fmt { static readonly NumberFormatInfo Nfi = new() { NumberGroupSeparator = " ", NumberDecimalSeparator = "," }; public static string Gold(decimal v) => Math.Round(v).ToString("#,0", Nfi); public static string Gold(long v) => v.ToString("#,0", Nfi); public static string Plural(int n, string one, string few, string many) { int m10 = n % 10, m100 = n % 100; if (m10 == 1 && m100 != 11) return one; if (m10 is >= 2 and <= 4 && m100 is < 12 or > 14) return few; return many; } }