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