Retail colors chat lines by the 34-value wire LogTextType (ACE's ChatMessageType), NOT by acdream's synthetic 9-value ChatKind. The old ChatWindowController.RetailChatColor(ChatKind) collapsed distinct retail colors onto one bucket per ChatKind — e.g. every Channel line rendered colorLightBlue (Magic's slot) when retail's actual palette spans five different colors across the Turbine rooms and legacy allegiance family. Ports ChatInterface::BuildChatColorLookupTable @0x004F31C0 verbatim (RetailChatColorTable, all 34 RGBA floats read from the PDB-paired binary's .data section) and threads a new ChatEntry.LogTextType field through every ingestion site to the correct retail wire value: HearSpeech/Tell pass the wire chatType through verbatim; Emote/SoulEmote hard-code 0x0C; the Tell self-echo hard-codes 0x04; legacy ChatChannel broadcasts derive their type from the channel bit via the new LegacyChannelChatType helper (ported from the decompiled Handle_Communication__ChannelBroadcast dispatch, hear vs. own-send); TurbineChat rooms map through TurbineChatDisplayNames.LogTextType; CombatChatTranslator's hit/miss/evade lines map to ACE's CombatSelf/ CombatEnemy per Player_Combat.cs; kill/death lines use retail's decompiled 0x00 Default (not a combat color). ChatWindowController's transcript now folds LogTextType through RetailChatColorTable with retail's exact "out-of-range keeps the previous line's color" carry rule; ChatPanel's combat highlighting sources the same table. Corrects HearSpeech.cs's doc-comment ChatType legend (4 of 6 entries were wrong). Adds register row AP-175 for the pre-existing (unchanged) Popup-renders-in-chat divergence and updates AP-39's stale per-ChatKind description. Narrows ISSUES #139 — its chat-colors half is done. Retail renders no chat timestamp prefix path exists in acdream today, so the "timestamp is always colorGrey 0x0C" rule has nothing to attach to; noted here per the research doc rather than left silent. Research: docs/research/2026-08-09-chat-retail-color-table.md Full Release suite: 11,833 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
185 lines
7 KiB
C#
185 lines
7 KiB
C#
using AcDream.Core.Chat;
|
|
using AcDream.Core.Combat;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.Chat;
|
|
|
|
/// <summary>
|
|
/// Phase I.7: <see cref="CombatChatTranslator"/> subscribes to
|
|
/// <see cref="CombatState"/>'s typed combat events and emits
|
|
/// retail-faithful chat lines into a <see cref="ChatLog"/>. Templates
|
|
/// ported VERBATIM from holtburger
|
|
/// <c>references/holtburger/.../panels/chat.rs</c> lines 221-308.
|
|
/// </summary>
|
|
public sealed class CombatChatTranslatorTests
|
|
{
|
|
private static (ChatLog, CombatState, CombatChatTranslator) Setup()
|
|
{
|
|
var chat = new ChatLog();
|
|
var combat = new CombatState();
|
|
var t = new CombatChatTranslator(combat, chat);
|
|
return (chat, combat, t);
|
|
}
|
|
|
|
[Fact]
|
|
public void DamageDealt_FormatsHolthurgerAttackerTemplate_AsInfo()
|
|
{
|
|
var (chat, combat, _) = Setup();
|
|
// damage_type 0x01 = SLASH → "slashing"
|
|
combat.OnAttackerNotification(
|
|
defenderName: "Mosswart Defiler", damageType: 0x01u,
|
|
damage: 12u, damagePercent: 0.54f);
|
|
|
|
var entry = Assert.Single(chat.Snapshot());
|
|
Assert.Equal(ChatKind.Combat, entry.Kind);
|
|
Assert.Equal(CombatLineKind.Info, entry.CombatKind);
|
|
Assert.Equal("You hit Mosswart Defiler for 12 slashing damage (54.0%).", entry.Text);
|
|
// Combat_Self (0x16) — retail's own-attack squelch type,
|
|
// references/ACE/.../Player_Combat.cs:162-163.
|
|
Assert.Equal(0x16u, entry.LogTextType);
|
|
}
|
|
|
|
[Fact]
|
|
public void DamageTaken_FormatsHolthurgerDefenderTemplate_AsWarning()
|
|
{
|
|
var (chat, combat, _) = Setup();
|
|
// hitQuadrant=1 → "chest"; damageType=0x10 → "fire"; critical=0
|
|
combat.OnVictimNotification(
|
|
attackerName: "Mosswart Stalker", attackerGuid: 0xA1u,
|
|
damageType: 0x10u, damage: 7u,
|
|
hitQuadrant: 1u, critical: 0u, attackType: 0u);
|
|
|
|
var entry = Assert.Single(chat.Snapshot());
|
|
Assert.Equal(ChatKind.Combat, entry.Kind);
|
|
Assert.Equal(CombatLineKind.Warning, entry.CombatKind);
|
|
Assert.Equal("Mosswart Stalker hit you for 7 fire damage to your chest.", entry.Text);
|
|
// Combat_Enemy (0x15) — retail's incoming-attack squelch type,
|
|
// references/ACE/.../Player_Combat.cs:541.
|
|
Assert.Equal(0x15u, entry.LogTextType);
|
|
}
|
|
|
|
[Fact]
|
|
public void DamageTaken_Critical_AppendsCriticalHitSuffix()
|
|
{
|
|
var (chat, combat, _) = Setup();
|
|
combat.OnVictimNotification(
|
|
attackerName: "Olthoi Soldier", attackerGuid: 0xB2u,
|
|
damageType: 0x02u /*PIERCE*/, damage: 99u,
|
|
hitQuadrant: 0u /*head*/, critical: 1u, attackType: 0u);
|
|
|
|
var entry = Assert.Single(chat.Snapshot());
|
|
Assert.Equal(
|
|
"Olthoi Soldier hit you for 99 piercing damage to your head. Critical hit.",
|
|
entry.Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissedOutgoing_FormatsEvasionAttackerTemplate_AsInfo()
|
|
{
|
|
var (chat, combat, _) = Setup();
|
|
combat.OnEvasionAttackerNotification("Mosswart Sniper");
|
|
|
|
var entry = Assert.Single(chat.Snapshot());
|
|
Assert.Equal(ChatKind.Combat, entry.Kind);
|
|
Assert.Equal(CombatLineKind.Info, entry.CombatKind);
|
|
Assert.Equal("Mosswart Sniper evaded your attack.", entry.Text);
|
|
// Combat_Self (0x16) — about the local player's OWN attack missing.
|
|
Assert.Equal(0x16u, entry.LogTextType);
|
|
}
|
|
|
|
[Fact]
|
|
public void EvadedIncoming_FormatsEvasionDefenderTemplate_AsInfo()
|
|
{
|
|
var (chat, combat, _) = Setup();
|
|
combat.OnEvasionDefenderNotification("Drudge Slinker");
|
|
|
|
var entry = Assert.Single(chat.Snapshot());
|
|
Assert.Equal(CombatLineKind.Info, entry.CombatKind);
|
|
Assert.Equal("You evaded Drudge Slinker's attack.", entry.Text);
|
|
// Combat_Enemy (0x15) — about an ENEMY'S attack.
|
|
Assert.Equal(0x15u, entry.LogTextType);
|
|
}
|
|
|
|
[Fact]
|
|
public void AttackDone_NonZeroControlStatus_EmitsNothing()
|
|
{
|
|
var (chat, combat, _) = Setup();
|
|
combat.OnAttackDone(attackSequence: 7, weenieError: 0x0036u);
|
|
|
|
// Retail HandleAttackDoneEvent consumes this only as control state.
|
|
// ACE deliberately uses ActionCancelled to reset the meter and sends
|
|
// a separate WeenieError event for any player-visible failure.
|
|
Assert.Empty(chat.Snapshot());
|
|
}
|
|
|
|
[Fact]
|
|
public void AttackDone_ZeroError_EmitsNothing()
|
|
{
|
|
var (chat, combat, _) = Setup();
|
|
combat.OnAttackDone(attackSequence: 7, weenieError: 0u);
|
|
|
|
Assert.Empty(chat.Snapshot());
|
|
}
|
|
|
|
[Fact]
|
|
public void KillLanded_EmitsInfoKillerLine()
|
|
{
|
|
var (chat, combat, _) = Setup();
|
|
combat.OnKillerNotification(victimName: "Phyntos Wasp", victimGuid: 0xCAFEu);
|
|
|
|
var entry = Assert.Single(chat.Snapshot());
|
|
Assert.Equal(ChatKind.Combat, entry.Kind);
|
|
Assert.Equal(CombatLineKind.Info, entry.CombatKind);
|
|
Assert.Equal("You killed Phyntos Wasp.", entry.Text);
|
|
// Default (0x00) — retail's decompiled kill/death color, not a
|
|
// combat color; see HandleKillerNotificationEvent @0x0056C410.
|
|
Assert.Equal(0x00u, entry.LogTextType);
|
|
}
|
|
|
|
[Fact]
|
|
public void FormatDamageType_PerHolthurgerBitNames_SingleBits()
|
|
{
|
|
// Spot-check three representative bits from the holtburger
|
|
// DamageType enum (combat.rs:55-95).
|
|
Assert.Equal("slashing", CombatChatTranslator.FormatDamageType(0x01u));
|
|
Assert.Equal("fire", CombatChatTranslator.FormatDamageType(0x10u));
|
|
Assert.Equal("nether", CombatChatTranslator.FormatDamageType(0x400u));
|
|
Assert.Equal("unknown", CombatChatTranslator.FormatDamageType(0u));
|
|
}
|
|
|
|
[Fact]
|
|
public void FormatDamageType_MultipleBits_JoinsWithSlash()
|
|
{
|
|
// Slash + Pierce → "slashing/piercing"
|
|
Assert.Equal("slashing/piercing",
|
|
CombatChatTranslator.FormatDamageType(0x01u | 0x02u));
|
|
}
|
|
|
|
[Fact]
|
|
public void FormatDamageLocation_PerHolthurgerEnum()
|
|
{
|
|
Assert.Equal("head", CombatChatTranslator.FormatDamageLocation(0));
|
|
Assert.Equal("chest", CombatChatTranslator.FormatDamageLocation(1));
|
|
Assert.Equal("upper arm", CombatChatTranslator.FormatDamageLocation(3));
|
|
Assert.Equal("foot", CombatChatTranslator.FormatDamageLocation(8));
|
|
// Out-of-range → graceful "body" fallback (defensive).
|
|
Assert.Equal("body", CombatChatTranslator.FormatDamageLocation(99));
|
|
}
|
|
|
|
[Fact]
|
|
public void Dispose_UnsubscribesFromAllEvents()
|
|
{
|
|
var (chat, combat, t) = Setup();
|
|
t.Dispose();
|
|
|
|
// Fire every event after disposal — none should produce chat lines.
|
|
combat.OnAttackerNotification("X", 0x01u, 1u, 1.0f);
|
|
combat.OnVictimNotification("X", 0u, 0x01u, 1u, 0u, 0u, 0u);
|
|
combat.OnEvasionAttackerNotification("X");
|
|
combat.OnEvasionDefenderNotification("X");
|
|
combat.OnAttackDone(0u, 0xFFu);
|
|
combat.OnKillerNotification("X", 0u);
|
|
|
|
Assert.Empty(chat.Snapshot());
|
|
}
|
|
}
|