fix #212: preserve shortcut tint in physical combat

Port the retail m_bShortcutGhosted meaning instead of treating SetShortcutNum's boolean as peace versus war. Keep occupied shortcut numbers on the gold sheet in NonCombat, Melee, and Missile, reserve the gray sheet for Magic, and lock the four-mode matrix with conformance tests and corrected research.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-13 11:21:12 +02:00
parent ab98cda26b
commit 5090aa7217
9 changed files with 197 additions and 112 deletions

View file

@ -66,95 +66,95 @@ public class UiItemSlotTests
}
[Fact]
public void ShortcutPeace_defaultIsTrue()
public void ShortcutGhosted_defaultsFalse()
{
var s = new UiItemSlot();
Assert.True(s.ShortcutPeace);
Assert.False(s.ShortcutGhosted);
}
[Fact]
public void SetShortcutNum_setsIndexAndPeace()
public void SetShortcutNum_setsIndexAndGhostedState()
{
var s = new UiItemSlot();
s.SetShortcutNum(3, peace: false);
s.SetShortcutNum(3, ghosted: true);
Assert.Equal(3, s.ShortcutNum);
Assert.False(s.ShortcutPeace);
Assert.True(s.ShortcutGhosted);
}
[Fact]
public void SetShortcutNum_peaceTrue()
public void SetShortcutNum_regularState()
{
var s = new UiItemSlot();
s.SetShortcutNum(0, peace: true);
s.SetShortcutNum(0, ghosted: false);
Assert.Equal(0, s.ShortcutNum);
Assert.True(s.ShortcutPeace);
Assert.False(s.ShortcutGhosted);
}
[Fact]
public void ClearShortcutNum_setsMinusOne()
{
var s = new UiItemSlot();
s.SetShortcutNum(5, peace: true);
s.SetShortcutNum(5, ghosted: false);
s.ClearShortcutNum();
Assert.Equal(-1, s.ShortcutNum);
}
// ── ActiveDigitArray occupancy gating (decomp UIElement_UIItem::SetShortcutNum:229481) ──
private static readonly uint[] Peace = { 0x10u, 0x11u, 0x12u };
private static readonly uint[] War = { 0x20u, 0x21u, 0x22u };
private static readonly uint[] Regular = { 0x10u, 0x11u, 0x12u };
private static readonly uint[] Ghosted = { 0x20u, 0x21u, 0x22u };
private static readonly uint[] Empty = { 0x30u, 0x31u, 0x32u };
/// <summary>
/// When ItemId == 0 (empty slot), ActiveDigitArray returns EmptyDigits regardless
/// of ShortcutPeace. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) —
/// of ShortcutGhosted. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) —
/// else branch when m_elem_Icon->m_state == 0x1000001c (empty).
/// </summary>
[Fact]
public void ActiveDigitArray_emptySlot_returnsEmptyDigits()
{
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty };
s.SetShortcutNum(0, peace: true);
var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty };
s.SetShortcutNum(0, ghosted: false);
// ItemId == 0 → EmptyDigits
Assert.Same(Empty, s.ActiveDigitArray());
}
[Fact]
public void ActiveDigitArray_emptySlot_warStance_stillReturnsEmptyDigits()
public void ActiveDigitArray_emptySlot_ghosted_stillReturnsEmptyDigits()
{
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty };
s.SetShortcutNum(0, peace: false);
var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty };
s.SetShortcutNum(0, ghosted: true);
// ItemId == 0 → EmptyDigits regardless of stance
Assert.Same(Empty, s.ActiveDigitArray());
}
/// <summary>
/// When ItemId != 0 (occupied), ActiveDigitArray returns PeaceDigits or WarDigits
/// depending on ShortcutPeace. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481/229493).
/// When ItemId != 0 (occupied), ActiveDigitArray returns RegularDigits or GhostedDigits
/// depending on ShortcutGhosted. Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481/229493).
/// </summary>
[Fact]
public void ActiveDigitArray_occupiedSlot_peaceStance_returnsPeaceDigits()
public void ActiveDigitArray_occupiedSlot_regular_returnsRegularDigits()
{
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty };
var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty };
s.SetItem(0x5001u, 0x99u);
s.SetShortcutNum(0, peace: true);
Assert.Same(Peace, s.ActiveDigitArray());
s.SetShortcutNum(0, ghosted: false);
Assert.Same(Regular, s.ActiveDigitArray());
}
[Fact]
public void ActiveDigitArray_occupiedSlot_warStance_returnsWarDigits()
public void ActiveDigitArray_occupiedSlot_ghosted_returnsGhostedDigits()
{
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = Empty };
var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = Empty };
s.SetItem(0x5001u, 0x99u);
s.SetShortcutNum(0, peace: false);
Assert.Same(War, s.ActiveDigitArray());
s.SetShortcutNum(0, ghosted: true);
Assert.Same(Ghosted, s.ActiveDigitArray());
}
[Fact]
public void ActiveDigitArray_emptySlot_nullEmptyDigits_returnsNull()
{
var s = new UiItemSlot { PeaceDigits = Peace, WarDigits = War, EmptyDigits = null };
s.SetShortcutNum(0, peace: true);
var s = new UiItemSlot { RegularDigits = Regular, GhostedDigits = Ghosted, EmptyDigits = null };
s.SetShortcutNum(0, ghosted: false);
Assert.Null(s.ActiveDigitArray());
}