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;
}
}