From 04c98be154138089c9ee31f4a4725dbca6abaad5 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 18 Apr 2026 23:11:50 +0200 Subject: [PATCH] feat(overlay): surface Chat + Combat state in DebugOverlay + ship OpenAL-Soft native MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two visible wins that prove today's wire-layer work is actually doing something: 1. Chat panel (bottom-left): live tail of the last 10 ChatLog entries. Color-coded by kind — Tell cyan, Channel green, System yellow, Popup red, local/ranged white. Bound to WorldSession via the existing GameEventWiring path (H.1) so server ChannelBroadcast / Tell / TransientString / Popup + HearSpeech all render live. Includes a synthetic \"connecting / connected\" system message so the panel isn't blank before anyone talks. 2. Event panel (bottom-right): last 8 combat events from CombatState (E.4). Damage taken (red), damage dealt (yellow), evaded-incoming (green), missed-outgoing (grey). Each entry fades out over 5s. DebugOverlay.BindCombat wires the listeners. 3. Silk.NET.OpenAL.Soft.Native 1.23.1 added. Before this, OpenAL managed bindings loaded but the native runtime was absent so IsAvailable returned false and audio was silently disabled. Now soft_oal.dll ships to runtimes/win-x64/native/ and the engine reports \"OpenAL engine ready (16 voices, 3D positional)\" on launch. Footsteps + other motion-hook sounds now audible. GameWindow: after constructing DebugOverlay, assign .Chat + .Combat and call BindCombat to hook the event stream. Build green, 628 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- launch.log | 0 src/AcDream.App/AcDream.App.csproj | 1 + src/AcDream.App/Rendering/DebugOverlay.cs | 129 ++++++++++++++++++++++ src/AcDream.App/Rendering/GameWindow.cs | 6 + 4 files changed, 136 insertions(+) create mode 100644 launch.log diff --git a/launch.log b/launch.log new file mode 100644 index 0000000..e69de29 diff --git a/src/AcDream.App/AcDream.App.csproj b/src/AcDream.App/AcDream.App.csproj index d2c9ef2..277ae99 100644 --- a/src/AcDream.App/AcDream.App.csproj +++ b/src/AcDream.App/AcDream.App.csproj @@ -16,6 +16,7 @@ + diff --git a/src/AcDream.App/Rendering/DebugOverlay.cs b/src/AcDream.App/Rendering/DebugOverlay.cs index 7ac635a..57ab741 100644 --- a/src/AcDream.App/Rendering/DebugOverlay.cs +++ b/src/AcDream.App/Rendering/DebugOverlay.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Numerics; +using AcDream.Core.Chat; +using AcDream.Core.Combat; namespace AcDream.App.Rendering; @@ -21,6 +23,46 @@ public sealed class DebugOverlay public bool ShowStatsPanel { get; set; } = true; public bool ShowHelpPanel { get; set; } = false; public bool ShowCompass { get; set; } = true; + public bool ShowChatPanel { get; set; } = true; + public bool ShowEventPanel { get; set; } = true; + + /// + /// Live chat log to render in the bottom-left chat panel. Hook this + /// up to the session's ChatLog so server-sent ChannelBroadcast / Tell / + /// HearSpeech / system messages appear on screen. + /// + public ChatLog? Chat { get; set; } + + /// Live combat state for damage floaters + target HP display. + public CombatState? Combat { get; set; } + + // Tail of recent combat events, captured from CombatState callbacks so + // they stay on-screen long enough to read even when you're moving fast. + private readonly List<(string text, Vector4 color, float timeLeft)> _eventTail = new(); + private const int MaxEventTail = 8; + private const float EventHoldSec = 5f; + + /// + /// Bind to CombatState events so incoming damage / evasion events + /// surface as transient event-log lines. Call once after Combat is set. + /// + public void BindCombat(CombatState combat) + { + combat.DamageTaken += d => PushEvent( + $"<< {d.AttackerName} hits you for {d.Damage}{(d.Critical ? " CRIT!" : "")}", Red); + combat.DamageDealtAccepted += d => PushEvent( + $">> you hit {d.DefenderName} for {d.Damage}", Yellow); + combat.EvadedIncoming += a => PushEvent( + $"<< {a}'s attack misses you", Green); + combat.MissedOutgoing += a => PushEvent( + $">> your attack misses {a}", Grey); + } + + private void PushEvent(string text, Vector4 color) + { + _eventTail.Add((text, color, EventHoldSec)); + while (_eventTail.Count > MaxEventTail) _eventTail.RemoveAt(0); + } // Toast state for transient notifications (e.g. "wireframes off"). private string? _toastText; @@ -84,6 +126,15 @@ public sealed class DebugOverlay if (_toastTimeLeft <= 0f) _toastText = null; } + + // Age event-tail entries; drop the ones that expired this frame. + for (int i = _eventTail.Count - 1; i >= 0; i--) + { + var e = _eventTail[i]; + e.timeLeft -= dt; + if (e.timeLeft <= 0f) _eventTail.RemoveAt(i); + else _eventTail[i] = e; + } } public void Draw(Snapshot s, Vector2 screenSize) @@ -94,6 +145,8 @@ public sealed class DebugOverlay if (ShowStatsPanel) DrawStatsPanel(s, screenSize); if (ShowCompass) DrawCompass(s, screenSize); if (ShowHelpPanel) DrawHelpPanel(screenSize); + if (ShowChatPanel) DrawChatPanel(screenSize); + if (ShowEventPanel) DrawEventPanel(screenSize); DrawHintBar(screenSize); DrawToast(screenSize); @@ -253,6 +306,82 @@ public sealed class DebugOverlay DrawPanel(x, y, lines, panelW); } + // ────────────────────────────────────────────────────────────────────── + // Chat panel — bottom-left: live ChatLog tail (Phase H.1). + // Proves the F.1 GameEvent dispatcher → ChatLog pipeline is alive. + // ────────────────────────────────────────────────────────────────────── + + private void DrawChatPanel(Vector2 screenSize) + { + if (Chat is null) return; + const int TailSize = 10; + var snap = Chat.Snapshot(); + if (snap.Length == 0) return; + + var lines = new List<(string text, Vector4 color)>(); + int start = Math.Max(0, snap.Length - TailSize); + for (int i = start; i < snap.Length; i++) + { + var e = snap[i]; + string prefix = e.Kind switch + { + ChatKind.LocalSpeech => $"[say] {e.Sender}: ", + ChatKind.RangedSpeech => $"[shout] {e.Sender}: ", + ChatKind.Channel => $"[ch{e.ChannelId}] {e.Sender}: ", + ChatKind.Tell => $"[tell] {e.Sender}: ", + ChatKind.System => "* ", + ChatKind.Popup => "!! ", + _ => "", + }; + Vector4 color = e.Kind switch + { + ChatKind.Tell => Cyan, + ChatKind.Channel => Green, + ChatKind.System => Yellow, + ChatKind.Popup => Red, + _ => White, + }; + lines.Add(($"{prefix}{e.Text}", color)); + } + + // Render from bottom-up above the hint bar. + float panelW = Math.Max(520f, MeasureMax(lines) + 2 * InnerPad); + float panelH = lines.Count * _font.LineHeight + 2 * InnerPad; + float x = 10f; + float y = screenSize.Y - _font.LineHeight - 18f - panelH; + DrawPanel(x, y, lines, panelW); + } + + // ────────────────────────────────────────────────────────────────────── + // Event panel — bottom-right: combat damage + evasion log (Phase E.4). + // Each entry fades as its timer runs out. + // ────────────────────────────────────────────────────────────────────── + + private void DrawEventPanel(Vector2 screenSize) + { + if (_eventTail.Count == 0) return; + + float lineH = _font.LineHeight; + float panelW = 360f; + // Prepare fade-out per line. + float x = screenSize.X - panelW - 10f; + float panelH = _eventTail.Count * lineH + 2 * InnerPad; + float y = screenSize.Y - _font.LineHeight - 18f - panelH; + + _text.DrawRect(x, y, panelW, panelH, PanelBg); + _text.DrawRectOutline(x, y, panelW, panelH, PanelBorder); + + float cy = y + InnerPad; + for (int i = 0; i < _eventTail.Count; i++) + { + var e = _eventTail[i]; + var c = e.color; + c.W *= MathF.Min(1f, e.timeLeft / 1.5f); + _text.DrawString(_font, e.text, x + InnerPad, cy, c); + cy += lineH; + } + } + // ────────────────────────────────────────────────────────────────────── // Hint bar — bottom-left: always-visible "F1 for help" reminder. // ────────────────────────────────────────────────────────────────────── diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 31415dd..e2c8925 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -567,6 +567,10 @@ public sealed class GameWindow : IDisposable _debugFont = new BitmapFont(_gl, fontBytes, pixelHeight: 15f, atlasSize: 512); _textRenderer = new TextRenderer(_gl, shadersDir); _debugOverlay = new DebugOverlay(_textRenderer, _debugFont); + // Phase F.1/H.1/E.4 visibility: show chat + combat events on screen. + _debugOverlay.Chat = Chat; + _debugOverlay.Combat = Combat; + _debugOverlay.BindCombat(Combat); Console.WriteLine($"debug overlay: loaded {fontBytes.Length / 1024}KB font, " + $"atlas {_debugFont.AtlasWidth}x{_debugFont.AtlasHeight}, " + $"lineHeight={_debugFont.LineHeight:F1}px"); @@ -740,7 +744,9 @@ public sealed class GameWindow : IDisposable senderGuid: speech.SenderGuid, isRanged: speech.IsRanged); + Chat.OnSystemMessage($"connecting to {host}:{portStr} as {user}", chatType: 1); _liveSession.Connect(user, pass); + Chat.OnSystemMessage("connected — character list received", chatType: 1); if (_liveSession.Characters is null || _liveSession.Characters.Characters.Count == 0) {