diff --git a/src/AcDream.Core/Ui/RadarBlipColors.cs b/src/AcDream.Core/Ui/RadarBlipColors.cs
new file mode 100644
index 0000000..685f156
--- /dev/null
+++ b/src/AcDream.Core/Ui/RadarBlipColors.cs
@@ -0,0 +1,93 @@
+using AcDream.Core.Items;
+
+namespace AcDream.Core.Ui;
+
+///
+/// B.7 (2026-05-15) — port of retail's gmRadarUI::GetBlipColor
+/// (named decomp 0x004d76f0). Returns the radar / target-indicator
+/// colour for an entity based on its and the
+/// raw PublicWeenieDesc._bitfield we already parse out of
+/// CreateObject.
+///
+///
+/// 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.
+///
+///
+///
+/// Dispatch order matches the retail decomp at lines 219913+ of
+/// docs/research/named-retail/acclient_2013_pseudo_c.txt. The
+/// PWD bit layout matches acclient.h:6431-6463:
+///
+/// - BF_PLAYER = 0x8
+/// - BF_PLAYER_KILLER = 0x20
+/// - BF_VENDOR = 0x200 (byte[1] & 0x02 in retail)
+/// - BF_PORTAL = 0x40000
+/// - BF_FREE_PKSTATUS = 0x200000 (hostile-flagged player)
+/// - BF_PKLITE_PKSTATUS = 0x2000000
+///
+///
+///
+///
+/// RGBA values are hand-tuned to visually match retail screenshots
+/// (yellow creature, red PK, pink PKLite, green vendor, cyan portal,
+/// white default). Real RGBAColor_Radar* constants live in retail
+/// static data — if they're ever recovered the table can be tightened.
+///
+///
+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)
+
+ ///
+ /// Resolve the radar-blip colour for an entity. Caller supplies the
+ /// raw (from CreateObject.ItemType) and
+ /// (from
+ /// CreateObject.ObjectDescriptionFlags) — both are already
+ /// parsed and stashed on EntitySpawn at spawn time.
+ ///
+ ///
+ /// Returns for friendly players,
+ /// for NPCs/monsters, for everything else.
+ /// Special types (PK, PKLite, Vendor, Portal) win over the base
+ /// type when their flag is set.
+ ///
+ ///
+ 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;
+ }
+}
diff --git a/tests/AcDream.Core.Tests/Ui/RadarBlipColorsTests.cs b/tests/AcDream.Core.Tests/Ui/RadarBlipColorsTests.cs
new file mode 100644
index 0000000..482ea3e
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Ui/RadarBlipColorsTests.cs
@@ -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);
+ }
+}