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) {