Adds the [indoor-bsp] probe + ProbeIndoorBspEnabled toggle for the Indoor walking Phase 1 BSP-cluster investigation. Mirrors the existing [resolve] / [cell-transit] / [indoor-*] pattern: one log line per BSPQuery.FindCollisions call from FindEnvCollisions' cell branch, capturing cell id, sphere local-pos, result TransitionState, and the hit poly's normal + side-type via the LastBspHitPoly side-channel (already wired for ProbeBuildingEnabled, now also fires for the indoor flag). Toggle via ACDREAM_PROBE_INDOOR_BSP=1 env var or DebugPanel checkbox. Zero-cost when off. Predecessor for the three fix commits that will close ISSUES.md #84/#85/#86 after the capture session. Spec: docs/superpowers/specs/2026-05-19-indoor-walking-phase1-bsp-cluster-design.md Plan: docs/superpowers/plans/2026-05-19-indoor-walking-phase1-bsp-cluster.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
479 lines
22 KiB
C#
479 lines
22 KiB
C#
using System.Numerics;
|
|
using AcDream.Core.Combat;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Rendering;
|
|
|
|
namespace AcDream.UI.Abstractions.Panels.Debug;
|
|
|
|
/// <summary>
|
|
/// Severity tag for a single combat-event line in the
|
|
/// <see cref="DebugVM.CombatEvents"/> ring. The panel reads this to pick
|
|
/// a <c>TextColored</c> rgba per row (yellow info / red warn / deep-red
|
|
/// error). Mirrors the same tri-tone the chat panel uses for combat
|
|
/// (Phase I.7).
|
|
/// </summary>
|
|
public enum CombatEventKind
|
|
{
|
|
/// <summary>You dealt damage / landed a hit. Yellow.</summary>
|
|
Info,
|
|
/// <summary>An incoming hit you evaded. Red.</summary>
|
|
Warn,
|
|
/// <summary>You took damage. Deep red.</summary>
|
|
Error,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Single typed entry in the combat-events ring. <see cref="Timestamp"/>
|
|
/// is captured at append time so a future panel revision can fade old
|
|
/// entries; for I.2 the panel just renders the text + rgba.
|
|
/// </summary>
|
|
public readonly record struct CombatEventLine(
|
|
DateTime Timestamp,
|
|
CombatEventKind Kind,
|
|
string Text);
|
|
|
|
/// <summary>
|
|
/// Severity tag for a transient toast message. Mirrors
|
|
/// <see cref="CombatEventKind"/> but lives in its own enum so the toast
|
|
/// surface can grow (e.g. an "OK" green) without dragging the combat
|
|
/// surface along.
|
|
/// </summary>
|
|
public enum ToastKind
|
|
{
|
|
Info,
|
|
Warn,
|
|
Error,
|
|
}
|
|
|
|
/// <summary>Single transient toast message kept in the recent-toasts ring.</summary>
|
|
public readonly record struct ToastMessage(
|
|
DateTime Timestamp,
|
|
ToastKind Kind,
|
|
string Text);
|
|
|
|
/// <summary>
|
|
/// ViewModel for the Phase I.2 <see cref="DebugPanel"/>. Read-through
|
|
/// (no caching): every property forwards to a <c>Func<T></c> that
|
|
/// the host (<c>GameWindow</c>) wires up at construction. Internal
|
|
/// state is limited to (a) the combat-event ring buffer, populated via a
|
|
/// self-subscription to <see cref="CombatState"/>'s typed events
|
|
/// (replacing the old <c>DebugOverlay.BindCombat</c>); (b) the toast
|
|
/// ring; (c) the diagnostic-flag bools the panel exposes as checkboxes.
|
|
///
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class DebugVM
|
|
{
|
|
/// <summary>Maximum number of combat-event lines kept in the ring.</summary>
|
|
public const int MaxCombatEvents = 25;
|
|
|
|
/// <summary>Maximum number of recent toast messages kept in the ring.</summary>
|
|
public const int MaxRecentToasts = 25;
|
|
|
|
private readonly Func<Vector3> _getPlayerPosition;
|
|
private readonly Func<float> _getPlayerHeadingDeg;
|
|
private readonly Func<uint> _getPlayerCellId;
|
|
private readonly Func<bool> _getPlayerOnGround;
|
|
private readonly Func<bool> _getInPlayerMode;
|
|
private readonly Func<bool> _getInFlyMode;
|
|
private readonly Func<float> _getVerticalVelocity;
|
|
private readonly Func<int> _getEntityCount;
|
|
private readonly Func<int> _getAnimatedCount;
|
|
private readonly Func<int> _getLandblocksVisible;
|
|
private readonly Func<int> _getLandblocksTotal;
|
|
private readonly Func<int> _getShadowObjectCount;
|
|
private readonly Func<float> _getNearestObjDist;
|
|
private readonly Func<string> _getNearestObjLabel;
|
|
private readonly Func<bool> _getColliding;
|
|
private readonly Func<bool> _getDebugWireframes;
|
|
private readonly Func<int> _getStreamingRadius;
|
|
private readonly Func<float> _getMouseSensitivity;
|
|
private readonly Func<float> _getChaseDistance;
|
|
private readonly Func<bool> _getRmbOrbit;
|
|
private readonly Func<string> _getHourName;
|
|
private readonly Func<float> _getDayFraction;
|
|
private readonly Func<string> _getWeather;
|
|
private readonly Func<int> _getActiveLights;
|
|
private readonly Func<int> _getRegisteredLights;
|
|
private readonly Func<int> _getParticleCount;
|
|
private readonly Func<float> _getFps;
|
|
private readonly Func<float> _getFrameMs;
|
|
|
|
private readonly Queue<CombatEventLine> _combatEvents = new();
|
|
private readonly Queue<ToastMessage> _toasts = new();
|
|
|
|
/// <summary>
|
|
/// Build a VM bound to live data sources. Every <c>Func</c> 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.
|
|
/// </summary>
|
|
public DebugVM(
|
|
Func<Vector3> getPlayerPosition,
|
|
Func<float> getPlayerHeadingDeg,
|
|
Func<uint> getPlayerCellId,
|
|
Func<bool> getPlayerOnGround,
|
|
Func<bool> getInPlayerMode,
|
|
Func<bool> getInFlyMode,
|
|
Func<float> getVerticalVelocity,
|
|
Func<int> getEntityCount,
|
|
Func<int> getAnimatedCount,
|
|
Func<int> getLandblocksVisible,
|
|
Func<int> getLandblocksTotal,
|
|
Func<int> getShadowObjectCount,
|
|
Func<float> getNearestObjDist,
|
|
Func<string> getNearestObjLabel,
|
|
Func<bool> getColliding,
|
|
Func<bool> getDebugWireframes,
|
|
Func<int> getStreamingRadius,
|
|
Func<float> getMouseSensitivity,
|
|
Func<float> getChaseDistance,
|
|
Func<bool> getRmbOrbit,
|
|
Func<string> getHourName,
|
|
Func<float> getDayFraction,
|
|
Func<string> getWeather,
|
|
Func<int> getActiveLights,
|
|
Func<int> getRegisteredLights,
|
|
Func<int> getParticleCount,
|
|
Func<float> getFps,
|
|
Func<float> 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) ───────────────
|
|
|
|
/// <summary>Mirror of <c>ACDREAM_DUMP_MOTION</c>; flipped at runtime via the panel.</summary>
|
|
public bool DumpMotion { get; set; }
|
|
/// <summary>Mirror of <c>ACDREAM_DUMP_VITALS</c>.</summary>
|
|
public bool DumpVitals { get; set; }
|
|
/// <summary>Mirror of <c>ACDREAM_DUMP_OPCODES</c>.</summary>
|
|
public bool DumpOpcodes { get; set; }
|
|
/// <summary>Mirror of <c>ACDREAM_DUMP_SKY</c>.</summary>
|
|
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.
|
|
/// <summary>
|
|
/// Runtime mirror of <c>PhysicsDiagnostics.ProbeResolveEnabled</c>
|
|
/// (env var <c>ACDREAM_PROBE_RESOLVE</c>). Toggling here flips the
|
|
/// resolver probe live — no relaunch required.
|
|
/// </summary>
|
|
public bool ProbeResolve
|
|
{
|
|
get => PhysicsDiagnostics.ProbeResolveEnabled;
|
|
set => PhysicsDiagnostics.ProbeResolveEnabled = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runtime mirror of <c>PhysicsDiagnostics.ProbeCellEnabled</c>
|
|
/// (env var <c>ACDREAM_PROBE_CELL</c>). Toggling here flips the
|
|
/// cell-transit probe live.
|
|
/// </summary>
|
|
public bool ProbeCell
|
|
{
|
|
get => PhysicsDiagnostics.ProbeCellEnabled;
|
|
set => PhysicsDiagnostics.ProbeCellEnabled = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// L.2d slice 1 (2026-05-13). Runtime mirror of
|
|
/// <c>PhysicsDiagnostics.ProbeBuildingEnabled</c> (env var
|
|
/// <c>ACDREAM_PROBE_BUILDING</c>). Toggling here flips the per-hit
|
|
/// <c>[resolve-bldg]</c> diagnostic + the registration-time
|
|
/// <c>[entity-source]</c> log lines. Heavy when enabled — emits one
|
|
/// multi-line entry per BSP hit per physics tick.
|
|
/// </summary>
|
|
public bool ProbeBuilding
|
|
{
|
|
get => PhysicsDiagnostics.ProbeBuildingEnabled;
|
|
set => PhysicsDiagnostics.ProbeBuildingEnabled = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// B.6 slice 1 (2026-05-14). Runtime mirror of
|
|
/// <c>PhysicsDiagnostics.ProbeAutoWalkEnabled</c> (env var
|
|
/// <c>ACDREAM_PROBE_AUTOWALK</c>). Toggling here flips the
|
|
/// <c>[autowalk-out]</c> / <c>[autowalk-mt]</c> / <c>[autowalk-up]</c>
|
|
/// trace used to characterize ACE's behavior during a server-
|
|
/// initiated auto-walk (issue #63). Low volume when off — only the
|
|
/// local player's events are filtered through the probe.
|
|
/// </summary>
|
|
public bool ProbeAutoWalk
|
|
{
|
|
get => PhysicsDiagnostics.ProbeAutoWalkEnabled;
|
|
set => PhysicsDiagnostics.ProbeAutoWalkEnabled = value;
|
|
}
|
|
|
|
// ── Indoor rendering diagnostics (2026-05-19) ───────────────────
|
|
// Mirror RenderingDiagnostics statics so DebugPanel checkbox toggles
|
|
// take effect on the next render frame without relaunching.
|
|
|
|
/// <summary>
|
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorWalkEnabled</c>
|
|
/// (env var <c>ACDREAM_PROBE_INDOOR_WALK</c>).
|
|
/// </summary>
|
|
public bool ProbeIndoorWalk
|
|
{
|
|
get => RenderingDiagnostics.ProbeIndoorWalkEnabled;
|
|
set => RenderingDiagnostics.ProbeIndoorWalkEnabled = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorLookupEnabled</c>
|
|
/// (env var <c>ACDREAM_PROBE_INDOOR_LOOKUP</c>).
|
|
/// </summary>
|
|
public bool ProbeIndoorLookup
|
|
{
|
|
get => RenderingDiagnostics.ProbeIndoorLookupEnabled;
|
|
set => RenderingDiagnostics.ProbeIndoorLookupEnabled = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorUploadEnabled</c>
|
|
/// (env var <c>ACDREAM_PROBE_INDOOR_UPLOAD</c>).
|
|
/// </summary>
|
|
public bool ProbeIndoorUpload
|
|
{
|
|
get => RenderingDiagnostics.ProbeIndoorUploadEnabled;
|
|
set => RenderingDiagnostics.ProbeIndoorUploadEnabled = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorXformEnabled</c>
|
|
/// (env var <c>ACDREAM_PROBE_INDOOR_XFORM</c>).
|
|
/// </summary>
|
|
public bool ProbeIndoorXform
|
|
{
|
|
get => RenderingDiagnostics.ProbeIndoorXformEnabled;
|
|
set => RenderingDiagnostics.ProbeIndoorXformEnabled = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runtime mirror of <c>RenderingDiagnostics.ProbeIndoorCullEnabled</c>
|
|
/// (env var <c>ACDREAM_PROBE_INDOOR_CULL</c>).
|
|
/// </summary>
|
|
public bool ProbeIndoorCull
|
|
{
|
|
get => RenderingDiagnostics.ProbeIndoorCullEnabled;
|
|
set => RenderingDiagnostics.ProbeIndoorCullEnabled = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Indoor walking Phase 1 (2026-05-19). Runtime mirror of
|
|
/// <c>PhysicsDiagnostics.ProbeIndoorBspEnabled</c> (env var
|
|
/// <c>ACDREAM_PROBE_INDOOR_BSP</c>). Toggling here flips the
|
|
/// <c>[indoor-bsp]</c> probe live — no relaunch required.
|
|
/// Physics-side companion to the five render-side
|
|
/// <c>ProbeIndoor*</c> mirrors directly above.
|
|
/// </summary>
|
|
public bool ProbeIndoorBsp
|
|
{
|
|
get => PhysicsDiagnostics.ProbeIndoorBspEnabled;
|
|
set => PhysicsDiagnostics.ProbeIndoorBspEnabled = value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runtime mirror of <c>RenderingDiagnostics.IndoorAll</c> — toggles all
|
|
/// five indoor probes together. No dedicated env var; set any individual
|
|
/// probe env var or use <c>ACDREAM_PROBE_INDOOR_ALL</c> to initialize
|
|
/// all five flags on at startup.
|
|
/// </summary>
|
|
public bool ProbeIndoorAll
|
|
{
|
|
get => RenderingDiagnostics.IndoorAll;
|
|
set => RenderingDiagnostics.IndoorAll = value;
|
|
}
|
|
|
|
// ── Chase camera tunables (forward to CameraDiagnostics) ──────────
|
|
|
|
/// <summary>Runtime mirror of <see cref="CameraDiagnostics.UseRetailChaseCamera"/>.</summary>
|
|
public bool UseRetailChaseCamera
|
|
{
|
|
get => CameraDiagnostics.UseRetailChaseCamera;
|
|
set => CameraDiagnostics.UseRetailChaseCamera = value;
|
|
}
|
|
|
|
/// <summary>Runtime mirror of <see cref="CameraDiagnostics.AlignToSlope"/>.</summary>
|
|
public bool CameraAlignToSlope
|
|
{
|
|
get => CameraDiagnostics.AlignToSlope;
|
|
set => CameraDiagnostics.AlignToSlope = value;
|
|
}
|
|
|
|
/// <summary>Runtime mirror of <see cref="CameraDiagnostics.TranslationStiffness"/>.</summary>
|
|
public float CameraTranslationStiffness
|
|
{
|
|
get => CameraDiagnostics.TranslationStiffness;
|
|
set => CameraDiagnostics.TranslationStiffness = value;
|
|
}
|
|
|
|
/// <summary>Runtime mirror of <see cref="CameraDiagnostics.RotationStiffness"/>.</summary>
|
|
public float CameraRotationStiffness
|
|
{
|
|
get => CameraDiagnostics.RotationStiffness;
|
|
set => CameraDiagnostics.RotationStiffness = value;
|
|
}
|
|
|
|
/// <summary>Runtime mirror of <see cref="CameraDiagnostics.MouseLowPassWindowSec"/>.</summary>
|
|
public float CameraMouseLowPassWindowSec
|
|
{
|
|
get => CameraDiagnostics.MouseLowPassWindowSec;
|
|
set => CameraDiagnostics.MouseLowPassWindowSec = value;
|
|
}
|
|
|
|
/// <summary>Runtime mirror of <see cref="CameraDiagnostics.CameraAdjustmentSpeed"/>.</summary>
|
|
public float CameraAdjustmentSpeed
|
|
{
|
|
get => CameraDiagnostics.CameraAdjustmentSpeed;
|
|
set => CameraDiagnostics.CameraAdjustmentSpeed = value;
|
|
}
|
|
|
|
// ── Action hooks invoked by panel buttons ──────────────────────────
|
|
|
|
/// <summary>
|
|
/// Cycle the time-of-day debug override (matches the old F7
|
|
/// behavior — none → midnight → dawn → noon → dusk → none). Wired
|
|
/// by <c>GameWindow</c>; null when no host is available (tests).
|
|
/// </summary>
|
|
public Action? CycleTimeOfDay { get; set; }
|
|
|
|
/// <summary>
|
|
/// Cycle the weather-kind debug override (matches the old F10
|
|
/// behavior — clear → overcast → rain → snow → storm).
|
|
/// </summary>
|
|
public Action? CycleWeather { get; set; }
|
|
|
|
/// <summary>
|
|
/// Toggle the collision-wires debug renderer. Same effect as the
|
|
/// old F2 keybind, which we keep as a hotkey alias.
|
|
/// </summary>
|
|
public Action? ToggleCollisionWires { get; set; }
|
|
|
|
/// <summary>
|
|
/// 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 <c>GameWindow</c> to the
|
|
/// same routine the legacy F-key fly toggle invokes.
|
|
/// </summary>
|
|
public Action? ToggleFlyMode { get; set; }
|
|
|
|
// ── Combat event ring + toast ring ─────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Snapshot view of the combat-event ring. Oldest-first; the panel
|
|
/// can iterate and render each line through <c>TextColored</c>
|
|
/// based on <see cref="CombatEventLine.Kind"/>.
|
|
/// </summary>
|
|
public IReadOnlyCollection<CombatEventLine> CombatEvents => _combatEvents;
|
|
|
|
/// <summary>Snapshot view of the recent-toasts ring (oldest-first).</summary>
|
|
public IReadOnlyCollection<ToastMessage> RecentToasts => _toasts;
|
|
|
|
/// <summary>
|
|
/// Append a toast message to the ring. Cap at
|
|
/// <see cref="MaxRecentToasts"/>; oldest entries drop. The panel's
|
|
/// "Recent toasts" section reads this; no on-screen flash for I.2.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|