feat(B.7): RadarBlipColors — port of gmRadarUI::GetBlipColor
Static helper resolving a target indicator / radar blip colour from ItemType + the raw PublicWeenieDesc._bitfield acdream already parses onto EntitySpawn. Dispatch order matches retail decomp at 0x004d76f0: Portal (BF_PORTAL = 0x40000) → cyan Vendor (BF_VENDOR = 0x200) → green Creature && !IsPlayer → yellow Player + IsPK (BF_PLAYER_KILLER = 0x20) → red Player + IsPKLite (= 0x2000000) → pink Player (other) → white (Default) Otherwise (item / object) → light grey RGBA values are hand-tuned to visually match retail screenshots; the real RGBAColor_Radar* constants live in retail static data and can be swapped in later without breaking call sites. 8 unit tests cover the full type/flag matrix (item, NPC, friendly player, PK, PKLite, vendor, portal-priority-over-flags). Next: TargetIndicatorPanel (App, ImGui draw) that uses this lookup.
This commit is contained in:
parent
37177a418e
commit
8544a785d7
2 changed files with 176 additions and 0 deletions
93
src/AcDream.Core/Ui/RadarBlipColors.cs
Normal file
93
src/AcDream.Core/Ui/RadarBlipColors.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.Core.Ui;
|
||||
|
||||
/// <summary>
|
||||
/// B.7 (2026-05-15) — port of retail's <c>gmRadarUI::GetBlipColor</c>
|
||||
/// (named decomp <c>0x004d76f0</c>). Returns the radar / target-indicator
|
||||
/// colour for an entity based on its <see cref="ItemType"/> and the
|
||||
/// raw <c>PublicWeenieDesc._bitfield</c> we already parse out of
|
||||
/// <c>CreateObject</c>.
|
||||
///
|
||||
/// <para>
|
||||
/// Used by the Vivid Target Indicator (Phase B.7) to colour the four
|
||||
/// corner triangles around the selected entity. Same value retail
|
||||
/// would have shown on the radar blip — so the indicator + radar agree.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Dispatch order matches the retail decomp at lines 219913+ of
|
||||
/// <c>docs/research/named-retail/acclient_2013_pseudo_c.txt</c>. The
|
||||
/// PWD bit layout matches <c>acclient.h:6431-6463</c>:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>BF_PLAYER = 0x8</c></item>
|
||||
/// <item><c>BF_PLAYER_KILLER = 0x20</c></item>
|
||||
/// <item><c>BF_VENDOR = 0x200</c> (byte[1] & 0x02 in retail)</item>
|
||||
/// <item><c>BF_PORTAL = 0x40000</c></item>
|
||||
/// <item><c>BF_FREE_PKSTATUS = 0x200000</c> (hostile-flagged player)</item>
|
||||
/// <item><c>BF_PKLITE_PKSTATUS = 0x2000000</c></item>
|
||||
/// </list>
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>RGBA values</b> are hand-tuned to visually match retail screenshots
|
||||
/// (yellow creature, red PK, pink PKLite, green vendor, cyan portal,
|
||||
/// white default). Real <c>RGBAColor_Radar*</c> constants live in retail
|
||||
/// static data — if they're ever recovered the table can be tightened.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class RadarBlipColors
|
||||
{
|
||||
public readonly record struct Rgba(byte R, byte G, byte B, byte A);
|
||||
|
||||
public static readonly Rgba Item = new(220, 220, 220, 255); // light grey (default object)
|
||||
public static readonly Rgba Default = new(255, 255, 255, 255); // white (friendly player)
|
||||
public static readonly Rgba Creature = new(255, 220, 80, 255); // yellow (NPC / monster)
|
||||
public static readonly Rgba PlayerKiller = new(255, 64, 64, 255); // red (PK)
|
||||
public static readonly Rgba PKLite = new(255, 128, 192, 255); // pink (PKLite)
|
||||
public static readonly Rgba Vendor = new( 64, 192, 64, 255); // green (vendor NPC)
|
||||
public static readonly Rgba Portal = new( 64, 192, 255, 255); // cyan (portal)
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the radar-blip colour for an entity. Caller supplies the
|
||||
/// raw <see cref="ItemType"/> (from <c>CreateObject.ItemType</c>) and
|
||||
/// <paramref name="pwdBitfield"/> (from
|
||||
/// <c>CreateObject.ObjectDescriptionFlags</c>) — both are already
|
||||
/// parsed and stashed on <c>EntitySpawn</c> at spawn time.
|
||||
///
|
||||
/// <para>
|
||||
/// Returns <see cref="Default"/> for friendly players, <see cref="Creature"/>
|
||||
/// for NPCs/monsters, <see cref="Item"/> for everything else.
|
||||
/// Special types (PK, PKLite, Vendor, Portal) win over the base
|
||||
/// type when their flag is set.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static Rgba For(uint itemType, uint pwdBitfield)
|
||||
{
|
||||
// Special-type early returns. Order matches retail dispatch
|
||||
// (Portal first, then Vendor) — same target can't logically
|
||||
// be both, but in case of bit collision retail's order wins.
|
||||
if ((pwdBitfield & 0x40000u) != 0) return Portal;
|
||||
if ((pwdBitfield & 0x200u) != 0) return Vendor;
|
||||
|
||||
bool isCreature = (itemType & (uint)ItemType.Creature) != 0;
|
||||
bool isPlayer = (pwdBitfield & 0x8u) != 0;
|
||||
|
||||
// Creature that isn't a player → NPC / monster → yellow.
|
||||
if (isCreature && !isPlayer)
|
||||
return Creature;
|
||||
|
||||
if (isPlayer)
|
||||
{
|
||||
bool isPK = (pwdBitfield & 0x20u) != 0;
|
||||
bool isPKLite = (pwdBitfield & 0x2000000u) != 0;
|
||||
if (isPK) return PlayerKiller;
|
||||
if (isPKLite) return PKLite;
|
||||
return Default;
|
||||
}
|
||||
|
||||
// Not a special type, not a creature, not a player → an item /
|
||||
// object on the ground or in the world.
|
||||
return Item;
|
||||
}
|
||||
}
|
||||
83
tests/AcDream.Core.Tests/Ui/RadarBlipColorsTests.cs
Normal file
83
tests/AcDream.Core.Tests/Ui/RadarBlipColorsTests.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Ui;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Ui;
|
||||
|
||||
public sealed class RadarBlipColorsTests
|
||||
{
|
||||
// PWD bit constants per docs/research/named-retail/acclient.h:6431-6463
|
||||
private const uint BF_PLAYER = 0x8u;
|
||||
private const uint BF_PLAYER_KILLER = 0x20u;
|
||||
private const uint BF_VENDOR = 0x200u;
|
||||
private const uint BF_PORTAL = 0x40000u;
|
||||
private const uint BF_PKLITE_PKSTATUS = 0x2000000u;
|
||||
|
||||
[Fact]
|
||||
public void Item_NoFlags_ReturnsItemColor()
|
||||
{
|
||||
// SpellComponents is itemType=0x1000 (e.g. a Taper) — not a creature.
|
||||
var result = RadarBlipColors.For(itemType: (uint)ItemType.SpellComponents, pwdBitfield: 0);
|
||||
Assert.Equal(RadarBlipColors.Item, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Misc_NoFlags_ReturnsItemColor()
|
||||
{
|
||||
var result = RadarBlipColors.For((uint)ItemType.Misc, pwdBitfield: 0);
|
||||
Assert.Equal(RadarBlipColors.Item, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Creature_NotPlayer_ReturnsCreatureColor()
|
||||
{
|
||||
// NPC: itemType has Creature bit, no Player flag.
|
||||
var result = RadarBlipColors.For((uint)ItemType.Creature, pwdBitfield: 0);
|
||||
Assert.Equal(RadarBlipColors.Creature, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FriendlyPlayer_ReturnsDefaultColor()
|
||||
{
|
||||
// Friendly player: Creature itemType + Player flag, no PK bits.
|
||||
var result = RadarBlipColors.For((uint)ItemType.Creature, pwdBitfield: BF_PLAYER);
|
||||
Assert.Equal(RadarBlipColors.Default, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PK_Player_ReturnsPlayerKillerColor()
|
||||
{
|
||||
var result = RadarBlipColors.For((uint)ItemType.Creature,
|
||||
pwdBitfield: BF_PLAYER | BF_PLAYER_KILLER);
|
||||
Assert.Equal(RadarBlipColors.PlayerKiller, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PKLite_Player_ReturnsPKLiteColor()
|
||||
{
|
||||
var result = RadarBlipColors.For((uint)ItemType.Creature,
|
||||
pwdBitfield: BF_PLAYER | BF_PKLITE_PKSTATUS);
|
||||
Assert.Equal(RadarBlipColors.PKLite, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vendor_BeatsCreatureFlag()
|
||||
{
|
||||
// A vendor NPC: Creature itemType + Vendor flag. Vendor wins per
|
||||
// retail's dispatch order (vendor check happens before creature
|
||||
// check at 0x004d7946-004d7973).
|
||||
var result = RadarBlipColors.For((uint)ItemType.Creature,
|
||||
pwdBitfield: BF_VENDOR);
|
||||
Assert.Equal(RadarBlipColors.Vendor, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Portal_TopPriority()
|
||||
{
|
||||
// Portal flag wins over everything else (retail dispatch order
|
||||
// checks BF_PORTAL first).
|
||||
var result = RadarBlipColors.For((uint)ItemType.Creature,
|
||||
pwdBitfield: BF_PORTAL | BF_PLAYER | BF_PLAYER_KILLER);
|
||||
Assert.Equal(RadarBlipColors.Portal, result);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue