From e1d7d095993ae5f7e25c38e80d73527966821376 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 16 Aug 2026 15:33:19 +0200 Subject: [PATCH] =?UTF-8?q?fix(chargen):=20Campaign=20CC=20gate=20round=20?= =?UTF-8?q?1=20closeout=20=E2=80=94=20Group=201:=20real=20color=20wheel=20?= =?UTF-8?q?wiring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lands Batch G's two STOPPED items, making the real palette-color swatch wheel visually live instead of inert: - UiButton and UiDatElement gain a per-instance Tint property threaded into every existing DrawSprite call (defaults to Vector4.One, so every pre-existing button/element is byte-identical unless a caller sets a non-identity tint). - CharacterCreationAppearancePage now sets Tint directly on each color swatch button and the GradCircle element, replacing the Batch G flat-fill ChargenSwatchColorTile overlay outright — an opaque rectangle drawn on top can never reproduce retail's actual SurfaceWindow::BlitAndColor(..., Blit_Multiply, color) multiply blend, only a genuine per-instance sprite tint can, so the overlay approach is deleted rather than layered under the new mechanism. - CharacterCreationUiController and RetailUiRuntime grow pass-through properties (AppearancePalSetSource/AppearanceClothingTableSource/ AppearancePaletteColorSource) mirroring the existing PreviewControl seam, so LivePresentationComposition can wire a DAT-backed ChargenAppearanceCatalog into the Appearance page (wiring itself lands with the Group 3 commit, since it shares a file with an unrelated F16 fix). Register: AP-216/AP-217 RETIRED (161 -> now further reduced in later commits) — both rows' remaining gaps are closed, not merely narrowed. CharacterCreationAppearancePageSwatchColorTests updated for the new Tint-based assertions (two pre-existing assertions were carried over incorrectly from the old overlay-visibility model and are corrected). Co-Authored-By: Claude Fable 5 --- .../Layout/CharacterCreationAppearancePage.cs | 125 ++++++++---------- .../Layout/CharacterCreationUiController.cs | 23 ++++ .../UI/Layout/ChargenSwatchColorTile.cs | 69 ---------- src/AcDream.App/UI/Layout/UiDatElement.cs | 13 +- src/AcDream.App/UI/RetailUiRuntime.cs | 35 +++++ src/AcDream.App/UI/UiButton.cs | 19 ++- ...rCreationAppearancePageSwatchColorTests.cs | 101 +++++++------- 7 files changed, 193 insertions(+), 192 deletions(-) delete mode 100644 src/AcDream.App/UI/Layout/ChargenSwatchColorTile.cs diff --git a/src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs b/src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs index 813f242f..cadde2ae 100644 --- a/src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs +++ b/src/AcDream.App/UI/Layout/CharacterCreationAppearancePage.cs @@ -73,20 +73,29 @@ namespace AcDream.App.UI.Layout; /// /// /// The real color wheel (Campaign CC gate round 1 Batch G, R2-5, -/// register AP-216/AP-217): retail's DoColorSpots @0x0047d850 / -/// DoGradDisk @0x0047da90 paint each swatch and the gradient disc -/// with an ACTUAL representative color sampled from the real DAT palette -/// data (AcDream.Core.CharGen.ChargenSwatchColorResolver ports the +/// register AP-216/AP-217 — CLOSEOUT (Group 1) makes it visually live): +/// retail's DoColorSpots @0x0047d850 / DoGradDisk @0x0047da90 +/// paint each swatch and the gradient disc with an ACTUAL representative +/// color sampled from the real DAT palette data +/// (AcDream.Core.CharGen.ChargenSwatchColorResolver ports the /// computation — see its own doc for the two color-source shapes and the -/// clothing PalSet lookup). / -/// / are -/// late-bound composition seams (same pattern as ) -/// a DAT-backed catalog wires in after construction; the -/// children painted over each swatch/the gradient disc are this batch's -/// rendering primitive — see that class's own doc for why it is a flat -/// color fill (a documented approximation of retail's actual recolored- -/// sprite blit) and the STOPPED shared-file edit that would upgrade it to -/// a genuine texture tint. +/// clothing PalSet lookup) via a genuine multiplicative sprite tint — +/// retail's own SurfaceWindow::BlitAndColor(..., Blit_Multiply, +/// color). // +/// are late-bound composition seams (same +/// pattern as ) a DAT-backed catalog wires in +/// after construction (CharacterCreationUiController.AppearancePalSetSource +/// etc., assigned once by LivePresentationComposition alongside the +/// existing AppearancePreviewControl wiring). Each swatch +/// () and the gradient disc (, +/// resolved via ) now set their OWN +/// / directly — +/// the earlier flat-fill ChargenSwatchColorTile overlay (Batch G's +/// documented approximation, since neither widget exposed a tint hook yet) +/// is retired: a flat opaque rectangle drawn ON TOP of a sprite can never +/// reproduce a multiply blend, only a genuine per-instance sprite tint can, +/// so this closeout replaces the overlay outright rather than layering a +/// tint UNDER it. /// /// internal sealed class CharacterCreationAppearancePage : IDisposable @@ -193,17 +202,13 @@ internal sealed class CharacterCreationAppearancePage : IDisposable private readonly UiButton? _rotateCounterClockwise; private readonly UiButton? _zoomIn; private readonly UiButton? _zoomOut; - private readonly UiElement? _gradCircle; - /// R2-5: one flat-color tile per swatch, added as an EXTRA - /// child of the swatch it decorates (see 's - /// own doc) — null wherever the matching entry - /// itself is null (nothing to attach to). - private readonly ChargenSwatchColorTile?[] _swatchColorTiles = new ChargenSwatchColorTile?[SwatchIds.Length]; - - /// R2-5: the gradient disc's own tint tile, an extra child of - /// . - private readonly ChargenSwatchColorTile? _gradCircleTile; + /// The gradient disc (0x1000030e) — Type 3 in the + /// authored dat, so (not the base + /// ) is what resolves; + /// typed concretely (post-closeout Group 1) so + /// can set directly. + private readonly UiDatElement? _gradCircle; private Choice _currentChoice = Choice.Face; private Part _currentPart = Part.Hair; @@ -219,15 +224,13 @@ internal sealed class CharacterCreationAppearancePage : IDisposable /// R2-5 late-bound seams (same pattern as /// above) for the real color-wheel mechanism — null (the default) /// leaves every swatch/the gradient disc showing ONLY its authored - /// static art, i.e. this page's pre-Batch-G behavior, until a - /// composition root supplies a DAT-backed - /// AcDream.Content.CharGen.ChargenAppearanceCatalog (which - /// already implements all three interfaces) for these three - /// properties, mirroring how itself gets - /// wired in from outside this class. STOPPED (Batch G): that - /// assignment is a 3-line addition to - /// CharacterCreationUiController.cs, outside this batch's file - /// contract — see the batch's handoff notes. + /// static art, i.e. this page's pre-Batch-G behavior. Closeout Group 1 + /// wires a DAT-backed AcDream.Content.CharGen.ChargenAppearanceCatalog + /// (which already implements all three interfaces) into these three + /// properties via CharacterCreationUiController.AppearancePalSetSource/ + /// AppearanceClothingTableSource/AppearancePaletteColorSource, + /// mirroring how itself gets wired in from + /// outside this class. /// internal IChargenPalSetSource? PalSetSource { get; set; } internal IChargenClothingTableSource? ClothingTableSource { get; set; } @@ -280,18 +283,6 @@ internal sealed class CharacterCreationAppearancePage : IDisposable int index = i; swatch.OnClick = () => SelectColor(index); _swatches[i] = swatch; - - // R2-5: an extra CHILD tile, sized to exactly cover the - // swatch's own face — see ChargenSwatchColorTile's own doc for - // why this is a flat fill rather than a recolored sprite, and - // for why ClickThrough there keeps this from ever swallowing - // the swatch's own click. - var tile = new ChargenSwatchColorTile - { - Left = 0f, Top = 0f, Width = swatch.Width, Height = swatch.Height, - }; - swatch.AddChild(tile); - _swatchColorTiles[i] = tile; } for (int i = 0; i < SwatchOverlayIds.Length; i++) @@ -301,15 +292,7 @@ internal sealed class CharacterCreationAppearancePage : IDisposable if (_shadeScroll is not null) _shadeScroll.ScalarChanged = SetShadeFromScalar; - _gradCircle = Find(pageRoot, GradCircleId); - if (_gradCircle is not null) - { - _gradCircleTile = new ChargenSwatchColorTile - { - Left = 0f, Top = 0f, Width = _gradCircle.Width, Height = _gradCircle.Height, - }; - _gradCircle.AddChild(_gradCircleTile); - } + _gradCircle = Find(pageRoot, GradCircleId); Viewport = Find(pageRoot, ViewportId); @@ -777,12 +760,13 @@ internal sealed class CharacterCreationAppearancePage : IDisposable continue; bool visible = i < displayCount; swatch.Visible = visible; - if (_swatchColorTiles[i] is { } tile) - { - ChargenSwatchRgb? rgb = visible ? swatchColors[i] : null; - tile.Color = rgb is { } c ? ToTintColor(c) : null; - tile.Visible = rgb is not null; - } + // Closeout Group 1: a genuine multiplicative sprite tint on the + // swatch's own authored spot art (UiButton.Tint), replacing the + // Batch G flat-fill overlay. Vector4.One (identity) reproduces + // the swatch's bare authored art untouched — both "no color data + // yet" (sources unwired) and "beyond this part's color count". + ChargenSwatchRgb? rgb = visible ? swatchColors[i] : null; + swatch.Tint = rgb is { } c ? ToTintColor(c) : Vector4.One; } // AP-217 (Batch C PARTIAL -> Batch G, R2-5, FULL): @@ -798,18 +782,17 @@ internal sealed class CharacterCreationAppearancePage : IDisposable { bool isEyes = _currentPart == Part.Eyes; _gradCircle.Visible = !isEyes; - if (_gradCircleTile is { } gradTile) - { - int gradIndex = isEyes - ? -1 - : colorSlot is null - ? 0 - : (int)ColorCurrent(_currentPart, snapshot.Appearance); - ChargenSwatchRgb? gradColor = - gradIndex >= 0 && gradIndex < swatchColors.Length ? swatchColors[gradIndex] : null; - gradTile.Color = gradColor is { } gc ? ToTintColor(gc) : null; - gradTile.Visible = !isEyes && gradColor is not null; - } + // Closeout Group 1: same Tint mechanism as the swatches above — + // the gradient disc's own authored art is multiplied by the + // currently-selected swatch's color instead of an overlay child. + int gradIndex = isEyes + ? -1 + : colorSlot is null + ? 0 + : (int)ColorCurrent(_currentPart, snapshot.Appearance); + ChargenSwatchRgb? gradColor = + gradIndex >= 0 && gradIndex < swatchColors.Length ? swatchColors[gradIndex] : null; + _gradCircle.Tint = gradColor is { } gc ? ToTintColor(gc) : Vector4.One; } ChargenShadeSlot? shadeSlot = ShadeSlotFor(_currentPart); diff --git a/src/AcDream.App/UI/Layout/CharacterCreationUiController.cs b/src/AcDream.App/UI/Layout/CharacterCreationUiController.cs index d736bf57..b728e6ee 100644 --- a/src/AcDream.App/UI/Layout/CharacterCreationUiController.cs +++ b/src/AcDream.App/UI/Layout/CharacterCreationUiController.cs @@ -363,6 +363,29 @@ internal sealed class CharacterCreationUiController : IDisposable set => _appearancePage.PreviewControl = value; } + /// Campaign CC gate round 1 closeout (Group 1, R2-5): the same + /// late-bound pattern as above, + /// for the real color-wheel/swatch-color mechanism's three DAT-backed + /// seams — see 's + /// own doc comment. + internal IChargenPalSetSource? AppearancePalSetSource + { + get => _appearancePage.PalSetSource; + set => _appearancePage.PalSetSource = value; + } + + internal IChargenClothingTableSource? AppearanceClothingTableSource + { + get => _appearancePage.ClothingTableSource; + set => _appearancePage.ClothingTableSource = value; + } + + internal IChargenPaletteColorSource? AppearancePaletteColorSource + { + get => _appearancePage.PaletteColorSource; + set => _appearancePage.PaletteColorSource = value; + } + /// Gates the Appearance preview's per-frame work on whether /// that specific page — AND the whole chargen screen — is the one /// currently showing. Close() only ever hides , diff --git a/src/AcDream.App/UI/Layout/ChargenSwatchColorTile.cs b/src/AcDream.App/UI/Layout/ChargenSwatchColorTile.cs deleted file mode 100644 index 069ad825..00000000 --- a/src/AcDream.App/UI/Layout/ChargenSwatchColorTile.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Numerics; - -namespace AcDream.App.UI.Layout; - -/// -/// Campaign CC gate round 1 Batch G (R2-5, register AP-216/AP-217): paints -/// one flat-fill patch of a computed -/// on top of whatever element it is attached to as a child — the -/// Appearance page's real-color rendering primitive for the nine color -/// swatches and the gradient disc. -/// -/// -/// Why a flat fill, not a recolored sprite (documented approximation): -/// retail's own mechanism (gmCGAppearancePage::DoColorSpots @0x0047d850 -/// / DoGradDisk @0x0047da90) blits an authored "spot"/gradient -/// graphic and RECOLORS it in place -/// (SurfaceWindow::ReplaceColor / BlitAndColor(..., -/// Blit_Multiply, color)) — a genuine multiplicative texture tint. The -/// retained-UI sprite pipeline this codebase already has -/// () DOES carry a per-draw -/// Vector4 tint parameter that could reproduce that exact multiply -/// blend, but neither (the nine swatches' own type, -/// sealed) nor (the gradient disc's own type) -/// exposes a per-instance tint hook on their EXISTING sprite draw calls — -/// adding one is a small, precisely-scoped, additive change to those two -/// shared widget files, outside this batch's file contract (reported as a -/// STOPPED item; see the batch's own commit message / handoff notes for the -/// exact diff). Rather than leave the swatches/wheel colorless pending that -/// follow-up, this class achieves the same OBSERVABLE result — "this -/// swatch/wheel visibly reflects the real computed color" — the cheapest -/// way the CURRENT public primitives allow: -/// is a plain solid-color quad, so the tile reads as a flat color patch -/// rather than a recolored dot/gradient graphic. It is added as an extra -/// CHILD of the swatch/disc it decorates (never replacing or subclassing -/// either sealed/shared type), so it draws strictly ON TOP -/// (: children paint after their -/// parent's own OnDraw) without disturbing the underlying element's -/// own state machine, media, or click handling at all. -/// -/// -/// -/// defaults to false on the -/// base class, so this MUST be set true by the constructor here (not left -/// to a caller to remember) — walks -/// children BEFORE testing the parent, and an opaque, click-absorbing tile -/// sitting on top of a swatch button would silently eat every click meant -/// for it. -/// -/// -internal sealed class ChargenSwatchColorTile : UiElement -{ - public ChargenSwatchColorTile() - { - ClickThrough = true; - Visible = false; - } - - /// The color to paint, or null to draw nothing this frame - /// ( is the authoritative on/off switch — callers - /// should set both together, matching every other swatch-visibility - /// site in CharacterCreationAppearancePage). - public Vector4? Color { get; set; } - - protected override void OnDraw(UiRenderContext ctx) - { - if (Color is { } c && Width > 0f && Height > 0f) - ctx.DrawFill(0f, 0f, Width, Height, c); - } -} diff --git a/src/AcDream.App/UI/Layout/UiDatElement.cs b/src/AcDream.App/UI/Layout/UiDatElement.cs index a132e6b1..7e70a5d6 100644 --- a/src/AcDream.App/UI/Layout/UiDatElement.cs +++ b/src/AcDream.App/UI/Layout/UiDatElement.cs @@ -179,6 +179,15 @@ public class UiDatElement : UiElement, IUiDatStateful /// Label color (default white). public Vector4 LabelColor { get; set; } = Vector4.One; + /// + /// Campaign CC gate round 1 closeout (Group 1, R2-5): per-instance + /// multiplicative sprite tint, threaded into both + /// calls this class makes (the runtime-image path and the ordinary + /// authored-media path) — same shape and same default-identity + /// no-op-for-existing-callers guarantee as . + /// + public Vector4 Tint { get; set; } = Vector4.One; + /// Retail LayoutDesc property 0x21 (two-pass glyph outline, /// UIElement_Text::SetOutline @0x0046a81c). Seeded in the ctor from the /// element's effective-default state, same as @@ -271,7 +280,7 @@ public class UiDatElement : UiElement, IUiDatStateful 0f, 1f, 1f, - Vector4.One); + Tint); } DrawLabel(ctx); return; @@ -290,7 +299,7 @@ public class UiDatElement : UiElement, IUiDatStateful // doc). Overlay/Alphablend use the same blit (the sprite shader // already alpha-blends). No Stretch mode exists in DrawModeType; // whole-canvas stretching happens at UiRoot.FixedCanvasSize. - ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One); + ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Tint); } } diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index a62fb663..a6f56b47 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -680,6 +680,41 @@ public sealed class RetailUiRuntime : IDisposable } } + /// Campaign CC gate round 1 closeout (Group 1, R2-5): the same + /// late-bound pattern as above, for + /// the real color-wheel/swatch-color mechanism's three DAT-backed seams + /// — see 's + /// own doc comment. + internal AcDream.Core.CharGen.IChargenPalSetSource? ChargenPalSetSource + { + get => CharacterCreationController?.AppearancePalSetSource; + set + { + if (CharacterCreationController is { } controller) + controller.AppearancePalSetSource = value; + } + } + + internal AcDream.Core.CharGen.IChargenClothingTableSource? ChargenClothingTableSource + { + get => CharacterCreationController?.AppearanceClothingTableSource; + set + { + if (CharacterCreationController is { } controller) + controller.AppearanceClothingTableSource = value; + } + } + + internal AcDream.Core.CharGen.IChargenPaletteColorSource? ChargenPaletteColorSource + { + get => CharacterCreationController?.AppearancePaletteColorSource; + set + { + if (CharacterCreationController is { } controller) + controller.AppearancePaletteColorSource = value; + } + } + /// CC6b-MOUNT: whether the Appearance page (specifically) is /// the one currently showing — false, safely, before the screen mounts. /// diff --git a/src/AcDream.App/UI/UiButton.cs b/src/AcDream.App/UI/UiButton.cs index e266710d..bf574e4b 100644 --- a/src/AcDream.App/UI/UiButton.cs +++ b/src/AcDream.App/UI/UiButton.cs @@ -153,6 +153,19 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful /// public uint? FaceFileOverride { get; set; } + /// + /// Campaign CC gate round 1 closeout (Group 1, R2-5): per-instance + /// multiplicative sprite tint, threaded into every + /// call this class makes (main face, face-segment, drag-acceptance + /// overlay) — retail's own SurfaceWindow::BlitAndColor(..., + /// Blit_Multiply, color). Default (white, + /// full alpha) leaves every DrawSprite call byte-identical to before + /// this property existed; only a caller that explicitly sets a + /// non-identity tint (e.g. 's + /// color-wheel swatches) changes what draws. + /// + public Vector4 Tint { get; set; } = Vector4.One; + /// Additional left inset for left-aligned labels. public float LabelOffsetX { get; set; } = 3f; @@ -469,7 +482,7 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful float faceWidth = FaceWidth > 0f ? FaceWidth : Width; float faceHeight = FaceHeight > 0f ? FaceHeight : Height; ctx.DrawSprite(tex, FaceLeft, FaceTop, faceWidth, faceHeight, - 0, 0, faceWidth / tw, faceHeight / th, Vector4.One); + 0, 0, faceWidth / tw, faceHeight / th, Tint); } } } @@ -525,7 +538,7 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful { var (tex, _, _) = _resolve(dragSprite); if (tex != 0) - ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One); + ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Tint); } } @@ -647,7 +660,7 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful // first reflowed by its own four-edge retail layout policy. ctx.DrawSprite(texture, rect.X0, rect.Y0, rect.Width, rect.Height, 0f, 0f, (float)rect.Width / textureWidth, (float)rect.Height / textureHeight, - Vector4.One); + Tint); } private void AddAvailableStates(ElementInfo mediaInfo) diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterCreationAppearancePageSwatchColorTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterCreationAppearancePageSwatchColorTests.cs index b3392e80..fc81db24 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterCreationAppearancePageSwatchColorTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterCreationAppearancePageSwatchColorTests.cs @@ -234,12 +234,9 @@ public sealed class CharacterCreationAppearancePageSwatchColorTests Assert.IsType(UiElement.FindDescendant( pageRoot, CharacterCreationAppearancePage.SwatchIds[index])); - private static ChargenSwatchColorTile SwatchTile(UiElement pageRoot, int index) => - Assert.IsType(Assert.Single(Swatch(pageRoot, index).Children)); - - private static ChargenSwatchColorTile GradTile(UiElement pageRoot) => - Assert.IsType(Assert.Single( - UiElement.FindDescendant(pageRoot, CharacterCreationAppearancePage.GradCircleId)!.Children)); + private static UiDatElement GradCircle(UiElement pageRoot) => + Assert.IsType(UiElement.FindDescendant( + pageRoot, CharacterCreationAppearancePage.GradCircleId)); private static Vector4 ToVector4(ChargenSwatchRgb rgb) => new(rgb.R / 255f, rgb.G / 255f, rgb.B / 255f, 1f); @@ -251,13 +248,13 @@ public sealed class CharacterCreationAppearancePageSwatchColorTests page.Refresh(view, view.Snapshot); - Assert.Equal(ToVector4(HairColorA), SwatchTile(root, 0).Color); - Assert.True(SwatchTile(root, 0).Visible); - Assert.Equal(ToVector4(HairColorB), SwatchTile(root, 1).Color); - Assert.True(SwatchTile(root, 1).Visible); - // Only two hair colors exist — swatch 2 must be blank. - Assert.Null(SwatchTile(root, 2).Color); - Assert.False(SwatchTile(root, 2).Visible); + Assert.Equal(ToVector4(HairColorA), Swatch(root, 0).Tint); + Assert.True(Swatch(root, 0).Visible); + Assert.Equal(ToVector4(HairColorB), Swatch(root, 1).Tint); + Assert.True(Swatch(root, 1).Visible); + // Only two hair colors exist — swatch 2 must be hidden and untinted. + Assert.Equal(Vector4.One, Swatch(root, 2).Tint); + Assert.False(Swatch(root, 2).Visible); } /// Part change (Hair -> Eyes via the spin's own select-zone @@ -269,14 +266,14 @@ public sealed class CharacterCreationAppearancePageSwatchColorTests var (pal, clothing, colors) = MakeSources(); (CharacterCreationAppearancePage page, FakeView view, UiElement root) = BuildPage(pal, clothing, colors); page.Refresh(view, view.Snapshot); - Assert.Equal(ToVector4(HairColorA), SwatchTile(root, 0).Color); + Assert.Equal(ToVector4(HairColorA), Swatch(root, 0).Tint); UiButton eyesSpin = Assert.IsType( UiElement.FindDescendant(root, CharacterCreationAppearancePage.EyesSpinId)); eyesSpin.OnClickAt!(180, 10); // select zone — switches _currentPart, no index change. - Assert.Equal(ToVector4(EyeColorA), SwatchTile(root, 0).Color); - Assert.Equal(ToVector4(EyeColorB), SwatchTile(root, 1).Color); + Assert.Equal(ToVector4(EyeColorA), Swatch(root, 0).Tint); + Assert.Equal(ToVector4(EyeColorB), Swatch(root, 1).Tint); } /// Color change (clicking a different swatch) must retint the @@ -287,9 +284,12 @@ public sealed class CharacterCreationAppearancePageSwatchColorTests var (pal, clothing, colors) = MakeSources(); (CharacterCreationAppearancePage page, FakeView view, UiElement root) = BuildPage(pal, clothing, colors); page.Refresh(view, view.Snapshot); - // No color selected yet (Unset) — no tint. - Assert.Null(GradTile(root).Color); - Assert.False(GradTile(root).Visible); + // No color selected yet (Unset) — no tint, but the disc's own base + // art is still shown (Visible tracks !isEyes only, independent of + // whether a real color has been resolved — the disc is never + // hidden pending a tint, only Eyes hides it at all). + Assert.Equal(Vector4.One, GradCircle(root).Tint); + Assert.True(GradCircle(root).Visible); Swatch(root, 0).OnClick!(); view.Snapshot = view.Snapshot with @@ -297,8 +297,8 @@ public sealed class CharacterCreationAppearancePageSwatchColorTests Appearance = view.Snapshot.Appearance with { HairColor = 0u }, }; page.Refresh(view, view.Snapshot); - Assert.Equal(ToVector4(HairColorA), GradTile(root).Color); - Assert.True(GradTile(root).Visible); + Assert.Equal(ToVector4(HairColorA), GradCircle(root).Tint); + Assert.True(GradCircle(root).Visible); Swatch(root, 1).OnClick!(); view.Snapshot = view.Snapshot with @@ -306,13 +306,13 @@ public sealed class CharacterCreationAppearancePageSwatchColorTests Appearance = view.Snapshot.Appearance with { HairColor = 1u }, }; page.Refresh(view, view.Snapshot); - Assert.Equal(ToVector4(HairColorB), GradTile(root).Color); + Assert.Equal(ToVector4(HairColorB), GradCircle(root).Tint); } - /// AP-217: Eyes always blanks the gradient disc's tile — no - /// tint is ever shown for Eyes, regardless of the selected eye color. + /// AP-217: Eyes always hides the gradient disc — no tint is + /// ever shown for Eyes, regardless of the selected eye color. [Fact] - public void EyesPart_GradientDiscTileStaysBlank() + public void EyesPart_GradientDiscStaysHiddenAndUntinted() { var (pal, clothing, colors) = MakeSources(); (CharacterCreationAppearancePage page, FakeView view, UiElement root) = BuildPage(pal, clothing, colors); @@ -326,11 +326,11 @@ public sealed class CharacterCreationAppearancePageSwatchColorTests UiElement.FindDescendant(root, CharacterCreationAppearancePage.EyesSpinId)); eyesSpin.OnClickAt!(180, 10); - Assert.False(GradTile(root).Visible); - Assert.Null(GradTile(root).Color); + Assert.False(GradCircle(root).Visible); + Assert.Equal(Vector4.One, GradCircle(root).Tint); // The swatches themselves still show real eye colors — only the - // disc blanks. - Assert.Equal(ToVector4(EyeColorA), SwatchTile(root, 0).Color); + // disc hides. + Assert.Equal(ToVector4(EyeColorA), Swatch(root, 0).Tint); } /// Nose/Mouth/Skin (colorSlot == null): retail still shows @@ -348,19 +348,20 @@ public sealed class CharacterCreationAppearancePageSwatchColorTests UiElement.FindDescendant(root, CharacterCreationAppearancePage.SkinSpinId)); skinSpin.OnClickAt!(10, 10); // skin has no arrow zones — every click selects it. - Assert.Equal(ToVector4(SkinColor), SwatchTile(root, 0).Color); - Assert.True(SwatchTile(root, 0).Visible); - Assert.Null(SwatchTile(root, 1).Color); - Assert.False(SwatchTile(root, 1).Visible); - Assert.Equal(ToVector4(SkinColor), GradTile(root).Color); - Assert.True(GradTile(root).Visible); + Assert.Equal(ToVector4(SkinColor), Swatch(root, 0).Tint); + Assert.True(Swatch(root, 0).Visible); + Assert.Equal(Vector4.One, Swatch(root, 1).Tint); + Assert.False(Swatch(root, 1).Visible); + Assert.Equal(ToVector4(SkinColor), GradCircle(root).Tint); + Assert.True(GradCircle(root).Visible); } /// Headgear's swatch resolves through the CURRENTLY EQUIPPED /// garment's own ClothingTable — an Unset headgear style (no garment) - /// shows no swatches at all. + /// shows no color (the swatch stays visible with its bare authored art, + /// untinted). [Fact] - public void HeadgearPart_ResolvesThroughTheEquippedGarment_UnsetShowsNoSwatches() + public void HeadgearPart_ResolvesThroughTheEquippedGarment_UnsetShowsNoColor() { var (pal, clothing, colors) = MakeSources(); (CharacterCreationAppearancePage page, FakeView view, UiElement root) = BuildPage(pal, clothing, colors); @@ -374,16 +375,21 @@ public sealed class CharacterCreationAppearancePageSwatchColorTests UiElement.FindDescendant(root, CharacterCreationAppearancePage.HeadgearSpinId)); headgearSpin.OnClickAt!(180, 10); - Assert.Equal(ToVector4(HeadgearColorA), SwatchTile(root, 0).Color); + Assert.Equal(ToVector4(HeadgearColorA), Swatch(root, 0).Tint); // Now un-equip (Unset) and refresh again — no garment, no colors. + // The swatch's own Visible stays true (the "beyond count" gate is + // the gender's fixed ClothingColors list length, independent of + // which garment is equipped — Batch C's already-shipped half); only + // the TINT reverts to identity, showing the swatch's bare authored + // art with no color. view.Snapshot = view.Snapshot with { Appearance = view.Snapshot.Appearance with { HeadgearStyle = RuntimeCharacterCreationAppearance.Unset }, }; page.Refresh(view, view.Snapshot); - Assert.Null(SwatchTile(root, 0).Color); - Assert.False(SwatchTile(root, 0).Visible); + Assert.Equal(Vector4.One, Swatch(root, 0).Tint); + Assert.True(Swatch(root, 0).Visible); } /// Heritage/gender change must re-source the color computation @@ -395,7 +401,7 @@ public sealed class CharacterCreationAppearancePageSwatchColorTests var (pal, clothing, colors) = MakeSources(); (CharacterCreationAppearancePage page, FakeView view, UiElement root) = BuildPage(pal, clothing, colors); page.Refresh(view, view.Snapshot); - Assert.Equal(ToVector4(HairColorA), SwatchTile(root, 0).Color); + Assert.Equal(ToVector4(HairColorA), Swatch(root, 0).Tint); // A second heritage with a DIFFERENT hair-color list. const uint otherHairPalSetId = 0x0F00_00AAu; @@ -409,16 +415,17 @@ public sealed class CharacterCreationAppearancePageSwatchColorTests page.Refresh(otherView, otherView.Snapshot); - Assert.Equal(ToVector4(otherHairColor), SwatchTile(root, 0).Color); + Assert.Equal(ToVector4(otherHairColor), Swatch(root, 0).Tint); } /// Before / /// / /// are - /// wired (composition-root STOPPED item), every tile MUST stay - /// invisible — the mechanism is fully inert, not a half-broken draw. + /// wired, every swatch/the gradient disc MUST show its bare authored + /// art (identity tint) — the mechanism is fully inert, not a + /// half-broken draw. [Fact] - public void UnwiredSources_LeaveEveryTileInvisible() + public void UnwiredSources_LeaveEverySwatchUntinted() { var view = new FakeView(MakeOptions(HeritageId, MakeGender()), HeritageId); var bindings = new CharacterCreationRuntimeBindings( @@ -440,8 +447,8 @@ public sealed class CharacterCreationAppearancePageSwatchColorTests page.Refresh(view, view.Snapshot); - Assert.False(GradTile(root).Visible); + Assert.Equal(Vector4.One, GradCircle(root).Tint); for (int i = 0; i < CharacterCreationAppearancePage.SwatchIds.Length; i++) - Assert.False(SwatchTile(root, i).Visible); + Assert.Equal(Vector4.One, Swatch(root, i).Tint); } }