RQ Overlay: оверлей заработка и статистики для Royal Quest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 12:46:52 +03:00
commit 7f89dba5f8
43 changed files with 4253 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using RQOverlay.Core;
using RQOverlay.Plugins;
namespace RQOverlay;
/// <summary>Состояние текущей сессии фарма: время, показатели включённых плагинов, итог.</summary>
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();
}
/// <summary>
/// Ручная корректировка заработка в золоте (например, минус потраченное на зелья).
/// Показывается в оверлее отдельной строкой, только когда не ноль.
/// </summary>
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();
}
}
/// <summary>Показатели всех включённых плагинов, по порядку.</summary>
public ObservableCollection<Metric> Metrics { get; } = new();
/// <summary>Золотые показатели — для полоски долей.</summary>
public ObservableCollection<Metric> GoldMetrics { get; } = new();
/// <summary>Дополнительные блоки плагинов в оверлее (список добычи и т.п.).</summary>
public ObservableCollection<FrameworkElement> 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}";
}
}
/// <summary>Оверлей пропускает клики мыши в игру.</summary>
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); }
/// <summary>Раз в секунду: таймер и темп.</summary>
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();
}
/// <param name="initial">Первый снимок — история до запуска оверлея.</param>
/// <param name="countHistory">Засчитывать ли историю в сессию (флаг --history).</param>
public void Apply(IEnumerable<LogEntry> 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<ItemSold>())
_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);
}
}
/// <summary>Набор включённых плагинов изменился — пересобираем оверлей.</summary>
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;
}
}