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:
Erik 2026-05-15 06:49:46 +02:00
parent 37177a418e
commit 8544a785d7
2 changed files with 176 additions and 0 deletions

View 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);
}
}