MosswartMassacre/MosswartMassacre/KillTracker.cs
erik 366cca8cb6 Phase 2: Extract IPluginLogger and KillTracker
- Create IPluginLogger interface, PluginCore implements it
- CharacterStats.cs and WebSocket.cs now use IPluginLogger instead of PluginCore.WriteToChat
- Extract KillTracker.cs: owns kill detection (all 36 regex patterns), death tracking,
  rate calculation, and the 1-sec stats update timer
- Bridge properties on PluginCore maintain backward compat for WebSocket telemetry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 07:29:49 +00:00

176 lines
6.9 KiB
C#

using System;
using System.Text.RegularExpressions;
using System.Timers;
namespace MosswartMassacre
{
/// <summary>
/// Tracks kills, deaths, and kill rate calculations.
/// Owns the 1-second stats update timer.
/// </summary>
internal class KillTracker
{
private readonly IPluginLogger _logger;
private readonly Action<int, double, double> _onStatsUpdated;
private readonly Action<TimeSpan> _onElapsedUpdated;
private int _totalKills;
private int _sessionDeaths;
private int _totalDeaths;
private double _killsPer5Min;
private double _killsPerHour;
private DateTime _lastKillTime = DateTime.Now;
private DateTime _statsStartTime = DateTime.Now;
private Timer _updateTimer;
// Kill message patterns — all 35+ patterns preserved exactly
private static readonly string[] KillPatterns = new string[]
{
@"^You flatten (?<targetname>.+)'s body with the force of your assault!$",
@"^You bring (?<targetname>.+) to a fiery end!$",
@"^You beat (?<targetname>.+) to a lifeless pulp!$",
@"^You smite (?<targetname>.+) mightily!$",
@"^You obliterate (?<targetname>.+)!$",
@"^You run (?<targetname>.+) through!$",
@"^You reduce (?<targetname>.+) to a sizzling, oozing mass!$",
@"^You knock (?<targetname>.+) into next Morningthaw!$",
@"^You split (?<targetname>.+) apart!$",
@"^You cleave (?<targetname>.+) in twain!$",
@"^You slay (?<targetname>.+) viciously enough to impart death several times over!$",
@"^You reduce (?<targetname>.+) to a drained, twisted corpse!$",
@"^Your killing blow nearly turns (?<targetname>.+) inside-out!$",
@"^Your attack stops (?<targetname>.+) cold!$",
@"^Your lightning coruscates over (?<targetname>.+)'s mortal remains!$",
@"^Your assault sends (?<targetname>.+) to an icy death!$",
@"^You killed (?<targetname>.+)!$",
@"^The thunder of crushing (?<targetname>.+) is followed by the deafening silence of death!$",
@"^The deadly force of your attack is so strong that (?<targetname>.+)'s ancestors feel it!$",
@"^(?<targetname>.+)'s seared corpse smolders before you!$",
@"^(?<targetname>.+) is reduced to cinders!$",
@"^(?<targetname>.+) is shattered by your assault!$",
@"^(?<targetname>.+) catches your attack, with dire consequences!$",
@"^(?<targetname>.+) is utterly destroyed by your attack!$",
@"^(?<targetname>.+) suffers a frozen fate!$",
@"^(?<targetname>.+)'s perforated corpse falls before you!$",
@"^(?<targetname>.+) is fatally punctured!$",
@"^(?<targetname>.+)'s death is preceded by a sharp, stabbing pain!$",
@"^(?<targetname>.+) is torn to ribbons by your assault!$",
@"^(?<targetname>.+) is liquified by your attack!$",
@"^(?<targetname>.+)'s last strength dissolves before you!$",
@"^Electricity tears (?<targetname>.+) apart!$",
@"^Blistered by lightning, (?<targetname>.+) falls!$",
@"^(?<targetname>.+)'s last strength withers before you!$",
@"^(?<targetname>.+) is dessicated by your attack!$",
@"^(?<targetname>.+) is incinerated by your assault!$"
};
internal int TotalKills => _totalKills;
internal double KillsPerHour => _killsPerHour;
internal double KillsPer5Min => _killsPer5Min;
internal int SessionDeaths => _sessionDeaths;
internal int TotalDeaths => _totalDeaths;
internal DateTime StatsStartTime => _statsStartTime;
internal DateTime LastKillTime => _lastKillTime;
internal int RareCount { get; set; }
/// <param name="logger">Logger for chat output</param>
/// <param name="onStatsUpdated">Callback(totalKills, killsPer5Min, killsPerHour) for UI updates</param>
/// <param name="onElapsedUpdated">Callback(elapsed) for UI elapsed time updates</param>
internal KillTracker(IPluginLogger logger, Action<int, double, double> onStatsUpdated, Action<TimeSpan> onElapsedUpdated)
{
_logger = logger;
_onStatsUpdated = onStatsUpdated;
_onElapsedUpdated = onElapsedUpdated;
}
internal void Start()
{
_updateTimer = new Timer(Constants.StatsUpdateIntervalMs);
_updateTimer.Elapsed += UpdateStats;
_updateTimer.Start();
}
internal void Stop()
{
if (_updateTimer != null)
{
_updateTimer.Stop();
_updateTimer.Dispose();
_updateTimer = null;
}
}
internal bool CheckForKill(string text)
{
if (IsKilledByMeMessage(text))
{
_totalKills++;
_lastKillTime = DateTime.Now;
CalculateKillsPerInterval();
_onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
return true;
}
return false;
}
internal void OnDeath()
{
_sessionDeaths++;
}
internal void SetTotalDeaths(int totalDeaths)
{
_totalDeaths = totalDeaths;
}
internal void RestartStats()
{
_totalKills = 0;
RareCount = 0;
_sessionDeaths = 0;
_statsStartTime = DateTime.Now;
_killsPer5Min = 0;
_killsPerHour = 0;
_logger?.Log($"Stats have been reset. Session deaths: {_sessionDeaths}, Total deaths: {_totalDeaths}");
_onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
}
private void UpdateStats(object sender, ElapsedEventArgs e)
{
try
{
TimeSpan elapsed = DateTime.Now - _statsStartTime;
_onElapsedUpdated?.Invoke(elapsed);
CalculateKillsPerInterval();
_onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
}
catch (Exception ex)
{
_logger?.Log("Error updating stats: " + ex.Message);
}
}
private void CalculateKillsPerInterval()
{
double minutesElapsed = (DateTime.Now - _statsStartTime).TotalMinutes;
if (minutesElapsed > 0)
{
_killsPer5Min = (_totalKills / minutesElapsed) * 5;
_killsPerHour = (_totalKills / minutesElapsed) * 60;
}
}
private bool IsKilledByMeMessage(string text)
{
foreach (string pattern in KillPatterns)
{
if (Regex.IsMatch(text, pattern))
return true;
}
return false;
}
}
}