using System.Numerics; namespace AcDream.UI.Abstractions.Panels.Debug; /// /// The Phase I.2 debug panel — single ImGui window with collapsing-header /// sections that replace the old custom DebugOverlay's six floating /// panels (Info / Stats / Help / Compass / Chat / Event) plus the toast /// surface. Reads through so values are always live. /// /// /// Layout: Player Info, Performance, Compass, Help, Combat events, Recent /// toasts, Diagnostics. Each section is a CollapsingHeader; /// importance-ranked sections default open, niche ones default closed. /// /// /// /// Reuses the I.1 widget extensions only; never imports a backend /// namespace. Same constraints as VitalsPanel and ChatPanel. /// /// public sealed class DebugPanel : IPanel { private readonly DebugVM _vm; public DebugPanel(DebugVM vm) { _vm = vm ?? throw new ArgumentNullException(nameof(vm)); } /// public string Id => "acdream.debug"; /// public string Title => "Debug"; /// public bool IsVisible { get; set; } = true; /// /// Cheat-sheet of currently meaningful keybinds. Kept as a static /// table because the data is stable and the panel only renders /// labels — no behavior change to the bindings themselves. /// private static readonly (string Key, string Action)[] Keybinds = { ("Esc", "exit fly / player / close window"), ("Tab", "toggle player mode (when in-world)"), ("F", "toggle fly camera"), ("F1", "toggle this debug panel"), ("F2", "toggle collision wireframes"), ("F3", "console dump (pos + nearby objects)"), ("F7", "cycle time-of-day override"), ("F8 / F9", "mouse sensitivity slower / faster"), ("F10", "cycle weather"), ("W A S D", "move (player mode) / fly"), ("Mouse", "turn character / look (fly)"), ("Hold RMB", "free orbit camera around player"), ("Wheel", "zoom chase camera in / out"), ("Space", "jump"), ("Shift", "run"), }; /// public void Render(PanelContext ctx, IPanelRenderer renderer) { if (!renderer.Begin(Title)) { renderer.End(); return; } DrawPlayerInfo(renderer); DrawPerformance(renderer); DrawCompass(renderer); DrawHelp(renderer); DrawCombatEvents(renderer); DrawRecentToasts(renderer); DrawDiagnostics(renderer); renderer.End(); } // ── Sections ────────────────────────────────────────────────────── private void DrawPlayerInfo(IPanelRenderer r) { if (!r.CollapsingHeader("Player Info", defaultOpen: true)) return; string mode = _vm.InPlayerMode ? "PLAYER" : _vm.InFlyMode ? "FLY" : "ORBIT"; r.Text($"mode: {mode} cell: 0x{_vm.CellId:X8}"); var p = _vm.PlayerPosition; r.Text($"pos: ({p.X,7:F1}, {p.Y,7:F1}, {p.Z,7:F2})"); r.Text($"heading: {_vm.HeadingDeg,3:F0}°"); r.Text($"grounded: {(_vm.OnGround ? "yes" : "no ")} vZ: {_vm.VerticalVelocity,5:F2}"); string near = float.IsPositiveInfinity(_vm.NearestObjDist) ? "---" : $"{_vm.NearestObjDist,4:F1}m"; if (_vm.Colliding) { r.TextColored(new Vector4(1f, 0.4f, 0.35f, 1f), $"near: {near} {_vm.NearestObjLabel} [BLOCKED]"); } else { r.Text($"near: {near} {_vm.NearestObjLabel}"); } if (_vm.InPlayerMode) r.Text($"chase dist: {_vm.ChaseDistance,4:F1}m{(_vm.RmbOrbit ? " [RMB orbit]" : "")}"); r.Text($"sens: {_vm.MouseSensitivity:F3}x"); } private void DrawPerformance(IPanelRenderer r) { if (!r.CollapsingHeader("Performance", defaultOpen: true)) return; r.Text($"fps: {_vm.Fps,5:F0} frame: {_vm.FrameMs,5:F1} ms"); r.Text($"visible LB: {_vm.LandblocksVisible,3}/{_vm.LandblocksTotal,3} radius: {_vm.StreamingRadius}"); r.Text($"entities: {_vm.EntityCount,4} animated: {_vm.AnimatedCount,3} coll: {_vm.ShadowObjectCount}"); r.Text($"lights: {_vm.ActiveLights}/{_vm.RegisteredLights} particles: {_vm.ParticleCount}"); r.Text($"time: {_vm.DayFraction,5:F2} {_vm.HourName} weather: {_vm.Weather}"); } private void DrawCompass(IPanelRenderer r) { if (!r.CollapsingHeader("Compass", defaultOpen: false)) return; // Phase I.2 stub — the visual strip + cardinal markers from the // old DebugOverlay relied on raw 2D-rect primitives we don't (and // shouldn't) expose through IPanelRenderer. The fancy compass // strip lands in D.6 with proper world-HUD draw-list primitives. // For now show heading degrees + compass cardinal label. float h = NormalizeDeg(_vm.HeadingDeg); r.Text($"heading: {h,3:F0}° cardinal: {Cardinal(h)}"); } private void DrawHelp(IPanelRenderer r) { if (!r.CollapsingHeader("Help", defaultOpen: false)) return; r.BeginTable("debug.help", 2); foreach (var (key, action) in Keybinds) { r.TableNextColumn(); r.Text(key); r.TableNextColumn(); r.Text(action); } r.EndTable(); } private void DrawCombatEvents(IPanelRenderer r) { if (!r.CollapsingHeader("Combat events", defaultOpen: true)) return; if (_vm.CombatEvents.Count == 0) { r.Text("(no recent combat)"); return; } foreach (var line in _vm.CombatEvents) { r.TextColored(ColorForCombat(line.Kind), line.Text); } } private void DrawRecentToasts(IPanelRenderer r) { if (!r.CollapsingHeader("Recent toasts", defaultOpen: false)) return; if (_vm.RecentToasts.Count == 0) { r.Text("(none)"); return; } foreach (var t in _vm.RecentToasts) { string ts = t.Timestamp.ToLocalTime().ToString("HH:mm:ss"); r.TextColored(ColorForToast(t.Kind), $"[{ts}] {t.Text}"); } } private void DrawDiagnostics(IPanelRenderer r) { if (!r.CollapsingHeader("Diagnostics", defaultOpen: true)) return; bool dumpMotion = _vm.DumpMotion; bool dumpVitals = _vm.DumpVitals; bool dumpOpcodes = _vm.DumpOpcodes; bool dumpSky = _vm.DumpSky; if (r.Checkbox("Dump motion (ACDREAM_DUMP_MOTION)", ref dumpMotion)) _vm.DumpMotion = dumpMotion; if (r.Checkbox("Dump vitals (ACDREAM_DUMP_VITALS)", ref dumpVitals)) _vm.DumpVitals = dumpVitals; if (r.Checkbox("Dump opcodes (ACDREAM_DUMP_OPCODES)", ref dumpOpcodes)) _vm.DumpOpcodes = dumpOpcodes; if (r.Checkbox("Dump sky (ACDREAM_DUMP_SKY)", ref dumpSky)) _vm.DumpSky = dumpSky; r.Spacing(); // Cycle / toggle actions live on the VM as Action handles; the // host (GameWindow) populates them with the same lambdas the // old F7/F10/F2 keybinds used. if (r.Button("Cycle time of day")) _vm.CycleTimeOfDay?.Invoke(); r.SameLine(); if (r.Button("Cycle weather")) _vm.CycleWeather?.Invoke(); r.SameLine(); if (r.Button("Toggle collision wires")) _vm.ToggleCollisionWires?.Invoke(); // Phase K.2 — explicit free-fly toggle button. Mirrors the // legacy F-key alias but is discoverable to users who haven't // memorized the Ctrl+F* debug bindings. Action handle owned // by GameWindow; null-safe for tests / offline. if (r.Button("Toggle Free-Fly Mode")) _vm.ToggleFlyMode?.Invoke(); r.Text(_vm.DebugWireframes ? "collision wires: ON" : "collision wires: OFF"); } // ── Color helpers ───────────────────────────────────────────────── private static Vector4 ColorForCombat(CombatEventKind kind) => kind switch { CombatEventKind.Info => new Vector4(1.0f, 0.9f, 0.3f, 1f), // yellow CombatEventKind.Warn => new Vector4(1.0f, 0.5f, 0.5f, 1f), // light red CombatEventKind.Error => new Vector4(1.0f, 0.3f, 0.3f, 1f), // deep red _ => new Vector4(1f, 1f, 1f, 1f), }; private static Vector4 ColorForToast(ToastKind kind) => kind switch { ToastKind.Warn => new Vector4(1.0f, 0.8f, 0.4f, 1f), ToastKind.Error => new Vector4(1.0f, 0.4f, 0.4f, 1f), _ => new Vector4(0.85f, 0.95f, 1.0f, 1f), }; private static float NormalizeDeg(float deg) { deg %= 360f; if (deg < 0) deg += 360f; return deg; } private static string Cardinal(float deg) { // Heading 0 = +X (east) per the old overlay. Same eight cardinal // labels — N/E/S/W with NE/SE/SW/NW between. // 0=E, 90=N, 180=W, 270=S (acdream's coordinate convention). string[] dirs = { "E", "NE", "N", "NW", "W", "SW", "S", "SE" }; int idx = (int)MathF.Round(deg / 45f) & 7; return dirs[idx]; } }