using System.Numerics;
using AcDream.Core.Combat;
using AcDream.Core.Physics;
namespace AcDream.UI.Abstractions.Panels.Debug;
///
/// Severity tag for a single combat-event line in the
/// ring. The panel reads this to pick
/// a TextColored rgba per row (yellow info / red warn / deep-red
/// error). Mirrors the same tri-tone the chat panel uses for combat
/// (Phase I.7).
///
public enum CombatEventKind
{
/// You dealt damage / landed a hit. Yellow.
Info,
/// An incoming hit you evaded. Red.
Warn,
/// You took damage. Deep red.
Error,
}
///
/// Single typed entry in the combat-events ring.
/// is captured at append time so a future panel revision can fade old
/// entries; for I.2 the panel just renders the text + rgba.
///
public readonly record struct CombatEventLine(
DateTime Timestamp,
CombatEventKind Kind,
string Text);
///
/// Severity tag for a transient toast message. Mirrors
/// but lives in its own enum so the toast
/// surface can grow (e.g. an "OK" green) without dragging the combat
/// surface along.
///
public enum ToastKind
{
Info,
Warn,
Error,
}
/// Single transient toast message kept in the recent-toasts ring.
public readonly record struct ToastMessage(
DateTime Timestamp,
ToastKind Kind,
string Text);
///
/// ViewModel for the Phase I.2 . Read-through
/// (no caching): every property forwards to a Func<T> that
/// the host (GameWindow) wires up at construction. Internal
/// state is limited to (a) the combat-event ring buffer, populated via a
/// self-subscription to 's typed events
/// (replacing the old DebugOverlay.BindCombat); (b) the toast
/// ring; (c) the diagnostic-flag bools the panel exposes as checkboxes.
///
///
/// Constructor explosion is intentional and acceptable here — the VM
/// lives entirely inside the AcDream.App composition root, not in any
/// plugin-facing surface. A nicer abstraction can come later if more
/// debug panels appear.
///
///
public sealed class DebugVM
{
/// Maximum number of combat-event lines kept in the ring.
public const int MaxCombatEvents = 25;
/// Maximum number of recent toast messages kept in the ring.
public const int MaxRecentToasts = 25;
private readonly Func _getPlayerPosition;
private readonly Func _getPlayerHeadingDeg;
private readonly Func _getPlayerCellId;
private readonly Func _getPlayerOnGround;
private readonly Func _getInPlayerMode;
private readonly Func _getInFlyMode;
private readonly Func _getVerticalVelocity;
private readonly Func _getEntityCount;
private readonly Func _getAnimatedCount;
private readonly Func _getLandblocksVisible;
private readonly Func _getLandblocksTotal;
private readonly Func _getShadowObjectCount;
private readonly Func _getNearestObjDist;
private readonly Func _getNearestObjLabel;
private readonly Func _getColliding;
private readonly Func _getDebugWireframes;
private readonly Func _getStreamingRadius;
private readonly Func _getMouseSensitivity;
private readonly Func _getChaseDistance;
private readonly Func _getRmbOrbit;
private readonly Func _getHourName;
private readonly Func _getDayFraction;
private readonly Func _getWeather;
private readonly Func _getActiveLights;
private readonly Func _getRegisteredLights;
private readonly Func _getParticleCount;
private readonly Func _getFps;
private readonly Func _getFrameMs;
private readonly Queue _combatEvents = new();
private readonly Queue _toasts = new();
///
/// Build a VM bound to live data sources. Every Func is read
/// per-frame by the panel — pass closures that resolve to the
/// authoritative source on each call so the panel always sees fresh
/// state.
///
public DebugVM(
Func getPlayerPosition,
Func getPlayerHeadingDeg,
Func getPlayerCellId,
Func getPlayerOnGround,
Func getInPlayerMode,
Func getInFlyMode,
Func getVerticalVelocity,
Func getEntityCount,
Func getAnimatedCount,
Func getLandblocksVisible,
Func getLandblocksTotal,
Func getShadowObjectCount,
Func getNearestObjDist,
Func getNearestObjLabel,
Func getColliding,
Func getDebugWireframes,
Func getStreamingRadius,
Func getMouseSensitivity,
Func getChaseDistance,
Func getRmbOrbit,
Func getHourName,
Func getDayFraction,
Func getWeather,
Func getActiveLights,
Func getRegisteredLights,
Func getParticleCount,
Func getFps,
Func getFrameMs,
CombatState combat)
{
if (combat is null) throw new ArgumentNullException(nameof(combat));
_getPlayerPosition = getPlayerPosition ?? throw new ArgumentNullException(nameof(getPlayerPosition));
_getPlayerHeadingDeg = getPlayerHeadingDeg ?? throw new ArgumentNullException(nameof(getPlayerHeadingDeg));
_getPlayerCellId = getPlayerCellId ?? throw new ArgumentNullException(nameof(getPlayerCellId));
_getPlayerOnGround = getPlayerOnGround ?? throw new ArgumentNullException(nameof(getPlayerOnGround));
_getInPlayerMode = getInPlayerMode ?? throw new ArgumentNullException(nameof(getInPlayerMode));
_getInFlyMode = getInFlyMode ?? throw new ArgumentNullException(nameof(getInFlyMode));
_getVerticalVelocity = getVerticalVelocity ?? throw new ArgumentNullException(nameof(getVerticalVelocity));
_getEntityCount = getEntityCount ?? throw new ArgumentNullException(nameof(getEntityCount));
_getAnimatedCount = getAnimatedCount ?? throw new ArgumentNullException(nameof(getAnimatedCount));
_getLandblocksVisible = getLandblocksVisible ?? throw new ArgumentNullException(nameof(getLandblocksVisible));
_getLandblocksTotal = getLandblocksTotal ?? throw new ArgumentNullException(nameof(getLandblocksTotal));
_getShadowObjectCount = getShadowObjectCount ?? throw new ArgumentNullException(nameof(getShadowObjectCount));
_getNearestObjDist = getNearestObjDist ?? throw new ArgumentNullException(nameof(getNearestObjDist));
_getNearestObjLabel = getNearestObjLabel ?? throw new ArgumentNullException(nameof(getNearestObjLabel));
_getColliding = getColliding ?? throw new ArgumentNullException(nameof(getColliding));
_getDebugWireframes = getDebugWireframes ?? throw new ArgumentNullException(nameof(getDebugWireframes));
_getStreamingRadius = getStreamingRadius ?? throw new ArgumentNullException(nameof(getStreamingRadius));
_getMouseSensitivity = getMouseSensitivity ?? throw new ArgumentNullException(nameof(getMouseSensitivity));
_getChaseDistance = getChaseDistance ?? throw new ArgumentNullException(nameof(getChaseDistance));
_getRmbOrbit = getRmbOrbit ?? throw new ArgumentNullException(nameof(getRmbOrbit));
_getHourName = getHourName ?? throw new ArgumentNullException(nameof(getHourName));
_getDayFraction = getDayFraction ?? throw new ArgumentNullException(nameof(getDayFraction));
_getWeather = getWeather ?? throw new ArgumentNullException(nameof(getWeather));
_getActiveLights = getActiveLights ?? throw new ArgumentNullException(nameof(getActiveLights));
_getRegisteredLights = getRegisteredLights ?? throw new ArgumentNullException(nameof(getRegisteredLights));
_getParticleCount = getParticleCount ?? throw new ArgumentNullException(nameof(getParticleCount));
_getFps = getFps ?? throw new ArgumentNullException(nameof(getFps));
_getFrameMs = getFrameMs ?? throw new ArgumentNullException(nameof(getFrameMs));
// Self-subscribe to combat events. Each one becomes a typed entry
// in the ring; the panel renders them in TextColored. Replaces
// the old DebugOverlay.BindCombat side-channel.
combat.DamageTaken += d => Push(CombatEventKind.Error,
$"<< {d.AttackerName} hit you for {d.Damage}{(d.Critical ? " CRIT!" : "")}");
combat.DamageDealtAccepted += d => Push(CombatEventKind.Info,
$">> you hit {d.DefenderName} for {d.Damage}");
combat.EvadedIncoming += attacker => Push(CombatEventKind.Warn,
$"<< {attacker}'s attack missed you");
combat.MissedOutgoing += defender => Push(CombatEventKind.Info,
$">> your attack missed {defender}");
combat.AttackDone += (_, weenieError) =>
{
if (weenieError != 0)
Push(CombatEventKind.Error, $"!! attack failed (error 0x{weenieError:X})");
};
combat.KillLanded += (victim, _) =>
Push(CombatEventKind.Info, $"** you killed {victim}");
}
// ── Read-through value surfaces ───────────────────────────────────
public Vector3 PlayerPosition => _getPlayerPosition();
public float HeadingDeg => _getPlayerHeadingDeg();
public uint CellId => _getPlayerCellId();
public bool OnGround => _getPlayerOnGround();
public bool InPlayerMode => _getInPlayerMode();
public bool InFlyMode => _getInFlyMode();
public float VerticalVelocity => _getVerticalVelocity();
public int EntityCount => _getEntityCount();
public int AnimatedCount => _getAnimatedCount();
public int LandblocksVisible => _getLandblocksVisible();
public int LandblocksTotal => _getLandblocksTotal();
public int ShadowObjectCount => _getShadowObjectCount();
public float NearestObjDist => _getNearestObjDist();
public string NearestObjLabel => _getNearestObjLabel();
public bool Colliding => _getColliding();
public bool DebugWireframes => _getDebugWireframes();
public int StreamingRadius => _getStreamingRadius();
public float MouseSensitivity => _getMouseSensitivity();
public float ChaseDistance => _getChaseDistance();
public bool RmbOrbit => _getRmbOrbit();
public string HourName => _getHourName();
public float DayFraction => _getDayFraction();
public string Weather => _getWeather();
public int ActiveLights => _getActiveLights();
public int RegisteredLights => _getRegisteredLights();
public int ParticleCount => _getParticleCount();
public float Fps => _getFps();
public float FrameMs => _getFrameMs();
// ── Diagnostic toggles (env-var-style runtime flags) ───────────────
/// Mirror of ACDREAM_DUMP_MOTION; flipped at runtime via the panel.
public bool DumpMotion { get; set; }
/// Mirror of ACDREAM_DUMP_VITALS.
public bool DumpVitals { get; set; }
/// Mirror of ACDREAM_DUMP_OPCODES.
public bool DumpOpcodes { get; set; }
/// Mirror of ACDREAM_DUMP_SKY.
public bool DumpSky { get; set; }
// L.2a slice 1 (2026-05-12): unlike DumpMotion/Vitals/Opcodes/Sky
// above (which are display-only mirrors of sticky-at-startup env
// vars), these forward directly to the PhysicsDiagnostics statics,
// so checkbox toggles take effect on the next physics resolve.
///
/// Runtime mirror of PhysicsDiagnostics.ProbeResolveEnabled
/// (env var ACDREAM_PROBE_RESOLVE). Toggling here flips the
/// resolver probe live — no relaunch required.
///
public bool ProbeResolve
{
get => PhysicsDiagnostics.ProbeResolveEnabled;
set => PhysicsDiagnostics.ProbeResolveEnabled = value;
}
///
/// Runtime mirror of PhysicsDiagnostics.ProbeCellEnabled
/// (env var ACDREAM_PROBE_CELL). Toggling here flips the
/// cell-transit probe live.
///
public bool ProbeCell
{
get => PhysicsDiagnostics.ProbeCellEnabled;
set => PhysicsDiagnostics.ProbeCellEnabled = value;
}
///
/// L.2d slice 1 (2026-05-13). Runtime mirror of
/// PhysicsDiagnostics.ProbeBuildingEnabled (env var
/// ACDREAM_PROBE_BUILDING). Toggling here flips the per-hit
/// [resolve-bldg] diagnostic + the registration-time
/// [entity-source] log lines. Heavy when enabled — emits one
/// multi-line entry per BSP hit per physics tick.
///
public bool ProbeBuilding
{
get => PhysicsDiagnostics.ProbeBuildingEnabled;
set => PhysicsDiagnostics.ProbeBuildingEnabled = value;
}
// ── Action hooks invoked by panel buttons ──────────────────────────
///
/// Cycle the time-of-day debug override (matches the old F7
/// behavior — none → midnight → dawn → noon → dusk → none). Wired
/// by GameWindow; null when no host is available (tests).
///
public Action? CycleTimeOfDay { get; set; }
///
/// Cycle the weather-kind debug override (matches the old F10
/// behavior — clear → overcast → rain → snow → storm).
///
public Action? CycleWeather { get; set; }
///
/// Toggle the collision-wires debug renderer. Same effect as the
/// old F2 keybind, which we keep as a hotkey alias.
///
public Action? ToggleCollisionWires { get; set; }
///
/// Phase K.2 — toggle the free-fly camera. Lets a user opt out of
/// the auto-entered chase camera (e.g. to inspect a remote part of
/// the world without the player following) without needing to find
/// the Ctrl+F* debug binding. Wired by GameWindow to the
/// same routine the legacy F-key fly toggle invokes.
///
public Action? ToggleFlyMode { get; set; }
// ── Combat event ring + toast ring ─────────────────────────────────
///
/// Snapshot view of the combat-event ring. Oldest-first; the panel
/// can iterate and render each line through TextColored
/// based on .
///
public IReadOnlyCollection CombatEvents => _combatEvents;
/// Snapshot view of the recent-toasts ring (oldest-first).
public IReadOnlyCollection RecentToasts => _toasts;
///
/// Append a toast message to the ring. Cap at
/// ; oldest entries drop. The panel's
/// "Recent toasts" section reads this; no on-screen flash for I.2.
///
public void AddToast(string text, ToastKind kind = ToastKind.Info)
{
if (string.IsNullOrEmpty(text)) return;
_toasts.Enqueue(new ToastMessage(DateTime.UtcNow, kind, text));
while (_toasts.Count > MaxRecentToasts)
_toasts.Dequeue();
}
private void Push(CombatEventKind kind, string text)
{
_combatEvents.Enqueue(new CombatEventLine(DateTime.UtcNow, kind, text));
while (_combatEvents.Count > MaxCombatEvents)
_combatEvents.Dequeue();
}
}