feat(ui): port retail radar and compass
This commit is contained in:
parent
c4af181b92
commit
3cbe4b00a1
43 changed files with 2882 additions and 262 deletions
|
|
@ -178,6 +178,10 @@ public sealed class ClientObject
|
|||
public uint Priority { get; set; } // ClothingPriority / CoverageMask layer order
|
||||
public uint? Useability { get; set; } // ITEM_USEABLE from PublicWeenieDesc
|
||||
public uint? TargetType { get; set; } // ITEM_TYPE mask for targeted-use compatibility
|
||||
/// <summary>Retail PublicWeenieDesc <c>_blipColor</c>; null until supplied by CreateObject.</summary>
|
||||
public byte? RadarBlipColor { get; set; }
|
||||
/// <summary>Retail PublicWeenieDesc <c>_radar_enum</c>; null until supplied by CreateObject.</summary>
|
||||
public byte? RadarBehavior { get; set; }
|
||||
public int Structure { get; set; } // charges/uses remaining
|
||||
public int MaxStructure { get; set; }
|
||||
public float Workmanship { get; set; } // 0..10 (fractional on the wire)
|
||||
|
|
@ -217,7 +221,9 @@ public readonly record struct WeenieData(
|
|||
int? MaxStructure,
|
||||
float? Workmanship,
|
||||
uint? Useability = null,
|
||||
uint? TargetType = null);
|
||||
uint? TargetType = null,
|
||||
byte? RadarBlipColor = null,
|
||||
byte? RadarBehavior = null);
|
||||
|
||||
/// <summary>
|
||||
/// Retail ITEM_USEABLE helpers (acclient.h:6478, ItemUses::* at 0x004fccd0).
|
||||
|
|
|
|||
|
|
@ -386,6 +386,8 @@ public sealed class ClientObjectTable
|
|||
if (d.Priority is { } pr) obj.Priority = pr;
|
||||
if (d.Useability is { } use) obj.Useability = use;
|
||||
if (d.TargetType is { } targetType) obj.TargetType = targetType;
|
||||
if (d.RadarBlipColor is { } radarBlipColor) obj.RadarBlipColor = radarBlipColor;
|
||||
if (d.RadarBehavior is { } radarBehavior) obj.RadarBehavior = radarBehavior;
|
||||
if (d.ItemsCapacity is { } ic) obj.ItemsCapacity = ic;
|
||||
if (d.ContainersCapacity is { } cc) obj.ContainersCapacity = cc;
|
||||
if (d.Structure is { } st) obj.Structure = st;
|
||||
|
|
|
|||
|
|
@ -3,91 +3,190 @@ 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>
|
||||
/// Facts used by retail's radar color/shape classification. Relationship state is
|
||||
/// intentionally separate because fellowship and allegiance membership do not come
|
||||
/// from <c>PublicWeenieDesc</c>.
|
||||
/// </summary>
|
||||
public readonly record struct RadarObjectTraits(
|
||||
bool IsValid = true,
|
||||
byte BlipColorOverride = 0,
|
||||
bool IsPortal = false,
|
||||
bool IsVendor = false,
|
||||
bool IsAttackable = false,
|
||||
bool IsCreature = false,
|
||||
bool IsPlayer = false,
|
||||
bool IsAdmin = false,
|
||||
bool IsHiddenAdmin = false,
|
||||
bool IsPlayerKiller = false,
|
||||
bool IsPkLite = false,
|
||||
bool IsFreePk = false)
|
||||
{
|
||||
// PublicWeenieDesc::BitfieldIndex, acclient.h:6431.
|
||||
private const uint BfPlayer = 0x00000008u;
|
||||
private const uint BfAttackable = 0x00000010u;
|
||||
private const uint BfPlayerKiller = 0x00000020u;
|
||||
private const uint BfHiddenAdmin = 0x00000040u;
|
||||
private const uint BfVendor = 0x00000200u;
|
||||
private const uint BfPortal = 0x00040000u;
|
||||
private const uint BfAdmin = 0x00100000u;
|
||||
private const uint BfFreePk = 0x00200000u;
|
||||
private const uint BfPkLite = 0x02000000u;
|
||||
|
||||
/// <summary>
|
||||
/// Build the subset of retail traits present in a CreateObject PWD. Fellowship
|
||||
/// and allegiance state must still be supplied separately.
|
||||
/// </summary>
|
||||
public static RadarObjectTraits FromPublicWeenieDescription(
|
||||
uint itemType,
|
||||
uint bitfield,
|
||||
byte blipColorOverride = 0)
|
||||
=> new(
|
||||
IsValid: (bitfield & 0x80000000u) == 0,
|
||||
BlipColorOverride: blipColorOverride,
|
||||
IsPortal: (bitfield & BfPortal) != 0,
|
||||
IsVendor: (bitfield & BfVendor) != 0,
|
||||
IsAttackable: (bitfield & BfAttackable) != 0,
|
||||
IsCreature: (itemType & (uint)ItemType.Creature) != 0,
|
||||
IsPlayer: (bitfield & BfPlayer) != 0,
|
||||
IsAdmin: (bitfield & BfAdmin) != 0,
|
||||
IsHiddenAdmin: (bitfield & BfHiddenAdmin) != 0,
|
||||
IsPlayerKiller: (bitfield & BfPlayerKiller) != 0,
|
||||
IsPkLite: (bitfield & BfPkLite) != 0,
|
||||
IsFreePk: (bitfield & BfFreePk) != 0);
|
||||
}
|
||||
|
||||
/// <summary>Relationship facts owned by the client player/fellowship systems.</summary>
|
||||
public readonly record struct RadarRelationshipTraits(
|
||||
bool IsFellowshipMember = false,
|
||||
bool IsFellowshipLeader = false,
|
||||
bool IsAllegianceMember = false,
|
||||
bool PlayerIsPlayerKiller = false,
|
||||
bool PlayerIsPkLite = false);
|
||||
|
||||
/// <summary>
|
||||
/// Exact retail radar palette and <c>gmRadarUI::GetBlipColor</c> dispatch.
|
||||
/// Named retail anchors: <c>gmRadarUI::GetBlipColor @ 0x004D76F0</c> and
|
||||
/// <c>RGBAColor_Radar* @ 0x008190E8</c>.
|
||||
/// </summary>
|
||||
public static class RadarBlipColors
|
||||
{
|
||||
public readonly record struct Rgba(byte R, byte G, byte B, byte A);
|
||||
/// <summary>
|
||||
/// Retail stores radar colors as four floats. The full names expose those exact
|
||||
/// channels; R/G/B/A are compatibility RGBA8 projections for older UI callers.
|
||||
/// </summary>
|
||||
public readonly record struct Rgba(float Red, float Green, float Blue, float Alpha)
|
||||
{
|
||||
public byte R => ToByte(Red);
|
||||
public byte G => ToByte(Green);
|
||||
public byte B => ToByte(Blue);
|
||||
public byte A => ToByte(Alpha);
|
||||
|
||||
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)
|
||||
public Rgba DimRgb(float multiplier)
|
||||
=> new(Red * multiplier, Green * multiplier, Blue * multiplier, Alpha);
|
||||
|
||||
/// <summary>ImGui/OpenGL-style packed <c>0xAABBGGRR</c> projection.</summary>
|
||||
public uint ToAbgr32()
|
||||
=> ((uint)A << 24) | ((uint)B << 16) | ((uint)G << 8) | R;
|
||||
|
||||
private static byte ToByte(float value)
|
||||
=> (byte)Math.Clamp((int)MathF.Round(value * 255f), 0, 255);
|
||||
}
|
||||
|
||||
// Verbatim float constants at acclient.exe 0x008190E8..0x00819184.
|
||||
public static readonly Rgba Blue = new(0.25f, 0.660000026f, 1f, 1f);
|
||||
public static readonly Rgba Gold = new(1f, 0.670000017f, 0f, 1f);
|
||||
public static readonly Rgba Yellow = new(1f, 1f, 0.5f, 1f);
|
||||
public static readonly Rgba White = new(1f, 1f, 1f, 1f);
|
||||
public static readonly Rgba Red = new(1f, 0.25f, 0.389999986f, 1f);
|
||||
public static readonly Rgba Purple = new(0.75f, 0.389999986f, 1f, 1f);
|
||||
public static readonly Rgba Pink = new(1f, 0.660000026f, 0.75f, 1f);
|
||||
public static readonly Rgba Green = new(0f, 0.5f, 0.25f, 1f);
|
||||
public static readonly Rgba Cyan = new(0f, 1f, 1f, 1f);
|
||||
public static readonly Rgba BrightGreen = new(0f, 1f, 0f, 1f);
|
||||
|
||||
// Retail role aliases initialized at 0x006EC780..0x006F90E7.
|
||||
public static readonly Rgba Default = White;
|
||||
public static readonly Rgba Item = White;
|
||||
public static readonly Rgba Admin = Cyan;
|
||||
public static readonly Rgba Advocate = Pink;
|
||||
public static readonly Rgba Creature = Gold;
|
||||
public static readonly Rgba LifeStone = Blue;
|
||||
public static readonly Rgba NPC = Yellow;
|
||||
public static readonly Rgba PlayerKiller = Red;
|
||||
public static readonly Rgba Portal = Purple;
|
||||
public static readonly Rgba Sentinel = Cyan;
|
||||
public static readonly Rgba Vendor = Yellow;
|
||||
public static readonly Rgba Fellowship = BrightGreen;
|
||||
public static readonly Rgba FellowshipLeader = BrightGreen;
|
||||
public static readonly Rgba PKLite = Pink;
|
||||
|
||||
/// <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>
|
||||
/// Compatibility entry point for metadata already surfaced by CreateObject.
|
||||
/// Classification which requires fellowship state is available through the
|
||||
/// explicit-traits overload.
|
||||
/// </summary>
|
||||
public static Rgba For(uint itemType, uint pwdBitfield)
|
||||
=> For(RadarObjectTraits.FromPublicWeenieDescription(itemType, pwdBitfield));
|
||||
|
||||
/// <summary>
|
||||
/// Port of <c>gmRadarUI::GetBlipColor @ 0x004D76F0</c>. An explicit wire
|
||||
/// override wins; otherwise portal/vendor/creature/player classification runs
|
||||
/// in retail order, followed by the fellowship override.
|
||||
/// </summary>
|
||||
public static Rgba For(
|
||||
RadarObjectTraits traits,
|
||||
RadarRelationshipTraits relationship = default)
|
||||
{
|
||||
// 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;
|
||||
if (!traits.IsValid)
|
||||
return Default;
|
||||
|
||||
bool isCreature = (itemType & (uint)ItemType.Creature) != 0;
|
||||
bool isPlayer = (pwdBitfield & 0x8u) != 0;
|
||||
if (traits.BlipColorOverride != 0)
|
||||
return ForOverride(traits.BlipColorOverride);
|
||||
|
||||
// Creature that isn't a player → NPC / monster → yellow.
|
||||
if (isCreature && !isPlayer)
|
||||
if (traits.IsPortal)
|
||||
return Portal;
|
||||
|
||||
if (traits.IsVendor)
|
||||
return Vendor;
|
||||
|
||||
if (traits.IsAttackable && traits.IsCreature && !traits.IsPlayer)
|
||||
return Creature;
|
||||
|
||||
if (isPlayer)
|
||||
{
|
||||
bool isPK = (pwdBitfield & 0x20u) != 0;
|
||||
bool isPKLite = (pwdBitfield & 0x2000000u) != 0;
|
||||
if (isPK) return PlayerKiller;
|
||||
if (isPKLite) return PKLite;
|
||||
if (!traits.IsPlayer)
|
||||
return Default;
|
||||
}
|
||||
|
||||
// Not a special type, not a creature, not a player → an item /
|
||||
// object on the ground or in the world.
|
||||
return Item;
|
||||
Rgba color = Default;
|
||||
if (traits.IsAdmin && !traits.IsHiddenAdmin)
|
||||
color = Admin;
|
||||
else if (traits.IsPlayerKiller)
|
||||
color = PlayerKiller;
|
||||
else if (traits.IsPkLite)
|
||||
color = PKLite;
|
||||
else if (traits.IsFreePk)
|
||||
color = Creature;
|
||||
|
||||
if (relationship.IsFellowshipLeader)
|
||||
color = FellowshipLeader;
|
||||
else if (relationship.IsFellowshipMember)
|
||||
color = Fellowship;
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
/// <summary>Retail PWD <c>_blipColor</c> override table (values 1..10).</summary>
|
||||
public static Rgba ForOverride(byte overrideValue)
|
||||
=> overrideValue switch
|
||||
{
|
||||
1 => Blue,
|
||||
2 => Gold,
|
||||
3 => White,
|
||||
4 => Purple,
|
||||
5 => Red,
|
||||
6 => Pink,
|
||||
7 => Green,
|
||||
8 => Yellow,
|
||||
9 => Cyan,
|
||||
10 => BrightGreen,
|
||||
_ => Default,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
275
src/AcDream.Core/Ui/RetailRadar.cs
Normal file
275
src/AcDream.Core/Ui/RetailRadar.cs
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.Core.Ui;
|
||||
|
||||
/// <summary>Verbatim retail <c>RadarEnum</c> values from <c>acclient.h:6467</c>.</summary>
|
||||
public enum RadarBehavior : byte
|
||||
{
|
||||
Undef = 0,
|
||||
ShowNever = 1,
|
||||
ShowMovement = 2,
|
||||
ShowAttacking = 3,
|
||||
ShowAlways = 4,
|
||||
}
|
||||
|
||||
/// <summary>Verbatim retail <c>RadarBlipShape</c> values from <c>acclient.h:6575</c>.</summary>
|
||||
public enum RadarBlipShape
|
||||
{
|
||||
Undef = 0,
|
||||
Circle = 1,
|
||||
Box = 2,
|
||||
X = 3,
|
||||
Plus = 4,
|
||||
Triangle = 5,
|
||||
InvertedTriangle = 6,
|
||||
XBox = 7,
|
||||
|
||||
Default = Plus,
|
||||
AllegianceMember = Box,
|
||||
FellowshipLeader = Triangle,
|
||||
Fellowship = InvertedTriangle,
|
||||
Threat = X,
|
||||
ThreatAllegiance = XBox,
|
||||
}
|
||||
|
||||
public enum RadarCompassPoint
|
||||
{
|
||||
North,
|
||||
East,
|
||||
South,
|
||||
West,
|
||||
}
|
||||
|
||||
public readonly record struct RadarPixelOffset(int X, int Y);
|
||||
|
||||
public readonly record struct RadarProjection(Vector2 Pixel, float RgbMultiplier);
|
||||
|
||||
/// <summary>
|
||||
/// Pure retail radar behavior used by the retained UI. The caller supplies positions
|
||||
/// already converted to player space, matching retail's
|
||||
/// <c>SmartBox::convert_to_player_space</c> call.
|
||||
/// </summary>
|
||||
public static class RetailRadar
|
||||
{
|
||||
public const float OutdoorRangeMeters = 75f;
|
||||
public const float IndoorRangeMeters = 25f;
|
||||
public const float AltitudeDimThresholdMeters = 5f;
|
||||
public const float AltitudeDimMultiplier = 0.65f;
|
||||
public const float UpdateIntervalSeconds = 0.025f;
|
||||
|
||||
private static readonly RadarPixelOffset[] PointPixels = [new(0, 0)];
|
||||
private static readonly RadarPixelOffset[] EdgePixels = [new(0, -1), new(0, 1), new(-1, 0), new(1, 0)];
|
||||
private static readonly RadarPixelOffset[] CornerPixels = [new(1, 1), new(-1, -1), new(-1, 1), new(1, -1)];
|
||||
private static readonly RadarPixelOffset[] BoxPixels = [.. EdgePixels, .. CornerPixels];
|
||||
private static readonly RadarPixelOffset[] XPixels = [new(0, 0), .. CornerPixels];
|
||||
private static readonly RadarPixelOffset[] PlusPixels = [new(0, 0), .. EdgePixels];
|
||||
private static readonly RadarPixelOffset[] TrianglePixels = [new(0, 0), new(-1, 1), new(0, 1), new(1, 1)];
|
||||
private static readonly RadarPixelOffset[] InvertedTrianglePixels = [new(0, 0), new(-1, -1), new(0, -1), new(1, -1)];
|
||||
private static readonly RadarPixelOffset[] XBoxPixels =
|
||||
[
|
||||
.. EdgePixels,
|
||||
.. CornerPixels,
|
||||
new(-2, -2),
|
||||
new(2, -2),
|
||||
new(-2, 2),
|
||||
new(2, 2),
|
||||
];
|
||||
|
||||
private static readonly RadarPixelOffset[] PlayerMarkerPixelArray =
|
||||
[
|
||||
new(0, 0),
|
||||
.. EdgePixels,
|
||||
new(-2, 0),
|
||||
new(2, 0),
|
||||
new(0, -2),
|
||||
new(0, 2),
|
||||
];
|
||||
|
||||
private static readonly RadarPixelOffset[] SelectionPixelArray = BuildSelectionPixels();
|
||||
|
||||
/// <summary>
|
||||
/// <c>ACCWeenieObject::InqShowableOnRadar @ 0x0058C250</c>. The enum names do
|
||||
/// not imply a live motion/attack test here: retail accepts all three values.
|
||||
/// </summary>
|
||||
public static bool IsShowable(RadarBehavior behavior, bool hasPhysicsObject)
|
||||
=> hasPhysicsObject && behavior is
|
||||
RadarBehavior.ShowMovement or RadarBehavior.ShowAttacking or RadarBehavior.ShowAlways;
|
||||
|
||||
/// <summary><c>gmRadarUI::GetBlipShape @ 0x004D7B60</c>.</summary>
|
||||
public static RadarBlipShape GetBlipShape(
|
||||
RadarObjectTraits target,
|
||||
RadarRelationshipTraits relationship = default)
|
||||
{
|
||||
if (!target.IsValid)
|
||||
return RadarBlipShape.Undef;
|
||||
|
||||
if (relationship.IsFellowshipLeader)
|
||||
return RadarBlipShape.FellowshipLeader;
|
||||
|
||||
if (relationship.IsFellowshipMember)
|
||||
return RadarBlipShape.Fellowship;
|
||||
|
||||
if (relationship.IsAllegianceMember)
|
||||
return RadarBlipShape.AllegianceMember;
|
||||
|
||||
if ((target.IsPlayerKiller && relationship.PlayerIsPlayerKiller) ||
|
||||
(target.IsPkLite && relationship.PlayerIsPkLite))
|
||||
{
|
||||
return RadarBlipShape.Threat;
|
||||
}
|
||||
|
||||
return RadarBlipShape.Default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exact points filled by retail's DrawPoint/Edges/Corners/Cross/X/Triangle/
|
||||
/// InvertedTriangle/Hollow/XBox functions at <c>0x004D7C30..0x004D8531</c>.
|
||||
/// </summary>
|
||||
public static ReadOnlySpan<RadarPixelOffset> GetBlipPixels(RadarBlipShape shape)
|
||||
=> shape switch
|
||||
{
|
||||
RadarBlipShape.Circle => PointPixels,
|
||||
RadarBlipShape.Box => BoxPixels,
|
||||
RadarBlipShape.X => XPixels,
|
||||
RadarBlipShape.Plus => PlusPixels,
|
||||
RadarBlipShape.Triangle => TrianglePixels,
|
||||
RadarBlipShape.InvertedTriangle => InvertedTrianglePixels,
|
||||
RadarBlipShape.XBox => XBoxPixels,
|
||||
_ => [],
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Fixed bright-green player marker drawn by <c>gmRadarUI::DrawChildren
|
||||
/// @ 0x004D9720</c>: a plus with four extra points two pixels from center.
|
||||
/// </summary>
|
||||
public static ReadOnlySpan<RadarPixelOffset> PlayerMarkerPixels => PlayerMarkerPixelArray;
|
||||
|
||||
/// <summary>Selected-object outline from <c>gmRadarUI::DrawSelected @ 0x004D7FE0</c>.</summary>
|
||||
public static ReadOnlySpan<RadarPixelOffset> SelectionPixels => SelectionPixelArray;
|
||||
|
||||
public static float GetRangeMeters(bool isOutside)
|
||||
=> isOutside ? OutdoorRangeMeters : IndoorRangeMeters;
|
||||
|
||||
/// <summary>
|
||||
/// Project a player-local position to radar pixels exactly as
|
||||
/// <c>gmRadarUI::DrawObjects @ 0x004D9380</c>. Objects at or beyond
|
||||
/// <c>(range - 1 m)</c> are excluded. Positive player-space Y is screen-up.
|
||||
/// </summary>
|
||||
public static bool TryProject(
|
||||
Vector3 playerSpaceMeters,
|
||||
Vector2 centerPixels,
|
||||
int radarRadiusPixels,
|
||||
float radarRangeMeters,
|
||||
out RadarProjection projection)
|
||||
{
|
||||
float inclusionRadius = radarRangeMeters - 1f;
|
||||
float horizontalDistanceSquared =
|
||||
(playerSpaceMeters.X * playerSpaceMeters.X) +
|
||||
(playerSpaceMeters.Y * playerSpaceMeters.Y);
|
||||
|
||||
if (horizontalDistanceSquared >= inclusionRadius * inclusionRadius)
|
||||
{
|
||||
projection = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
float scale = radarRadiusPixels / radarRangeMeters;
|
||||
projection = new RadarProjection(
|
||||
new Vector2(
|
||||
(int)(centerPixels.X + (playerSpaceMeters.X * scale)),
|
||||
(int)(centerPixels.Y - (playerSpaceMeters.Y * scale))),
|
||||
GetAltitudeRgbMultiplier(playerSpaceMeters.Z));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>RGB is dimmed at exactly 5 m; alpha is left unchanged by the caller.</summary>
|
||||
public static float GetAltitudeRgbMultiplier(float playerSpaceZMeters)
|
||||
=> MathF.Abs(playerSpaceZMeters) < AltitudeDimThresholdMeters
|
||||
? 1f
|
||||
: AltitudeDimMultiplier;
|
||||
|
||||
/// <summary>
|
||||
/// Compute a compass token's top-left pixel from
|
||||
/// <c>gmRadarUI::UpdateCompassTokens @ 0x004D9060</c>. Heading is the physical
|
||||
/// player's heading in degrees, not camera yaw.
|
||||
/// </summary>
|
||||
public static Vector2 GetCompassTokenTopLeft(
|
||||
float playerHeadingDegrees,
|
||||
RadarCompassPoint point,
|
||||
Vector2 radarCenterPixels,
|
||||
float tokenMagnitudePixels,
|
||||
Vector2 tokenSizePixels)
|
||||
{
|
||||
// Retail stores headingRadians as float, then performs the trig on x87.
|
||||
// Preserve that float rounding while using double sin/cos like the x87 path.
|
||||
float headingRadians = playerHeadingDegrees * 0.0174532924f;
|
||||
double angle = headingRadians + (point switch
|
||||
{
|
||||
RadarCompassPoint.North => Math.PI,
|
||||
RadarCompassPoint.East => (double)1.57079637f,
|
||||
RadarCompassPoint.South => 0d,
|
||||
RadarCompassPoint.West => (double)4.71238899f,
|
||||
_ => 0d,
|
||||
});
|
||||
|
||||
float offsetX = (float)(Math.Sin(angle) * tokenMagnitudePixels);
|
||||
float tokenCenterX = (float)(offsetX + radarCenterPixels.X);
|
||||
float tokenCenterY = (float)((Math.Cos(angle) * tokenMagnitudePixels) + radarCenterPixels.Y);
|
||||
|
||||
return new Vector2(
|
||||
(int)(tokenCenterX - (tokenSizePixels.X * 0.5f)),
|
||||
(int)(tokenCenterY - (tokenSizePixels.Y * 0.5f)));
|
||||
}
|
||||
|
||||
private static RadarPixelOffset[] BuildSelectionPixels()
|
||||
{
|
||||
var pixels = new RadarPixelOffset[20];
|
||||
int index = 0;
|
||||
for (int x = -2; x <= 2; x++)
|
||||
{
|
||||
pixels[index++] = new RadarPixelOffset(x, 3);
|
||||
pixels[index++] = new RadarPixelOffset(x, -3);
|
||||
}
|
||||
|
||||
for (int y = -2; y <= 2; y++)
|
||||
{
|
||||
pixels[index++] = new RadarPixelOffset(3, y);
|
||||
pixels[index++] = new RadarPixelOffset(-3, y);
|
||||
}
|
||||
|
||||
return pixels;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Retail outdoor coordinate text derived from the player's landcell.</summary>
|
||||
public readonly record struct RadarCoordinates(double X, double Y)
|
||||
{
|
||||
public string XText => FormatAxis(X, "W", "E");
|
||||
public string YText => FormatAxis(Y, "S", "N");
|
||||
public string CombinedText => $"{YText},{XText}";
|
||||
|
||||
/// <summary>
|
||||
/// Port of <c>CPlayerSystem::InqPlayerCoords @ 0x00560090</c>. Indoor cells
|
||||
/// fail because retail obtains coordinates through <c>get_landscape_coord</c>.
|
||||
/// </summary>
|
||||
public static bool TryFromCell(uint cellId, out RadarCoordinates coordinates)
|
||||
{
|
||||
if (!LandDefs.GidToLcoord(cellId, out int lx, out int ly))
|
||||
{
|
||||
coordinates = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
coordinates = new RadarCoordinates(
|
||||
((lx - 1024) * 0.1) + 0.5,
|
||||
((ly - 1024) * 0.1) + 0.5);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string FormatAxis(double value, string negativeSuffix, string positiveSuffix)
|
||||
{
|
||||
string suffix = value < 0d ? negativeSuffix : value > 0d ? positiveSuffix : string.Empty;
|
||||
return $"{Math.Abs(value).ToString("F1", System.Globalization.CultureInfo.InvariantCulture)}{suffix}";
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue