diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 30f23372..dfc23777 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -37,7 +37,7 @@ accepted-divergence entries (#96, #49, #50). --- -## 1. Intentional architecture (IA) — 17 rows +## 1. Intentional architecture (IA) — 18 rows | # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle | |---|---|---|---|---|---| @@ -58,6 +58,7 @@ accepted-divergence entries (#96, #49, #50). | IA-17 | Toolbar chrome is toolkit-supplied through the central `RetailWindowFrame` mount (`UiCollapsibleFrame` 8-piece bevel) because LayoutDesc `0x21000016` carries no baked frame. It also supports a toolkit-defined collapse-to-one-row (bottom-edge resize snapping between a row-1-only and a two-row height, row-2 visibility tied to the stop) — retail's real collapse is keystone.dll (no decomp) and the DAT stacks both rows always. | `src/AcDream.App/UI/Layout/RetailWindowFrame.cs`; `src/AcDream.App/UI/UiCollapsibleFrame.cs`; toolbar policy in `GameWindow.cs`; spec: `docs/superpowers/specs/2026-06-20-d2b-toolbar-collapse-design.md` | The central mount now owns wrapper geometry/registration uniformly; border-over-content prevents the row-2 right cap from poking through | The collapse stops remain a toolkit reconstruction rather than a byte-port of Keystone behavior | gmToolbarUI WM chrome (keystone.dll, no PDB); no bevel ids in LayoutDesc 0x21000016 (toolbar dump) | | IA-18 | Effect overlay tile (enum 0x10000005) is a `ReplaceColor` SURFACE SOURCE — pure-white pixels in the composited drag icon are replaced PER-PIXEL with the same (x,y) pixel of the effect tile (the SURFACE overload `SurfaceWindow::ReplaceColor` 0x004415b0), preserving the tile's texture/gradient; the tile itself is NOT blitted as an additional layer. This IS faithful retail behavior. **Anti-regression: do NOT re-implement this as a blit layer NOR as a flat-color replace (it is a per-pixel surface copy).** | `src/AcDream.App/UI/IconComposer.cs` (`ReplaceWhiteFromSurface`) | Faithful port of `IconData::RenderIcons` @407614 → the SURFACE overload `ReplaceColor` 0x004415b0 (`dst[x,y]=src[x,y]` where `dst==white`); confirmed via clean Ghidra decompile + named decomp + visual (the Energy Crystal's blue is a gradient, 2026-06-17). | A blit-layer or flat-color re-implementation would show the wrong effect look (no gradient) — the visual-verification regression that retired the mean-color approximation | `IconData::RenderIcons` acclient_2013_pseudo_c.txt:407524; `ReplaceColor` SURFACE overload 0x004415b0:71656; `docs/research/2026-06-17-stateful-icon-RESOLVED.md` | | IA-19 | Automatic combat acquisition is narrowed to attackable non-player monsters. Retail `AutoTarget` falls back to `SelectNext(SELECTION_TYPE_COMPASS_ITEM)`, whose combat filter can also admit attackable enemy players in compatible PK states. | `src/AcDream.Core/Combat/CombatTargetPolicy.cs`; consumer `src/AcDream.App/Rendering/GameWindow.cs` | Explicit product direction: Auto Target must never select NPCs, players, pets, or other objects; manual player-selection commands remain available | In PK play, Auto Target will not acquire an otherwise valid hostile player as retail would; the player must be selected manually | `ClientCombatSystem::AutoTarget @ 0x0056BC80`; `CPlayerSystem::SelectNext @ 0x0055F9A0`; `ClientCombatSystem::ObjectIsAttackable @ 0x0056A600` | +| IA-20 | The basic combat bar keeps dark-red media `0x0600715E` visible as the middle baseline between Speed and Power. Retail owns that media as field `0x100005EF` and toggles it with trained Recklessness during the scalar-message path. | `src/AcDream.App/UI/UiScrollbar.cs`; binding geometry in `src/AcDream.App/UI/Layout/CombatUiController.cs` | Explicit connected visual direction: the dark middle track must remain present behind the live bright-red charge fill; the exact skill-gated presentation remains tracked by AP-112 | Untrained characters see the dark-red baseline where retail may leave only the gray track; trained/untrained Recklessness presentation is not distinguishable | `gmCombatUI::PostInit @ 0x004CC1A0`; `gmCombatUI::ListenToElementMessage @ 0x004CC430`; LayoutDesc `0x21000073` child `0x100005EF` | --- @@ -213,7 +214,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | `src/AcDream.App/UI/Layout/CharacterStatController.cs`; `CharacterSheetProvider.cs` | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | `gmCharacterTitleUI @ 0x0049A610`; `gmStatManagementUI::UpdateExperience @ 0x004F0A70` | | AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, spellbook/effects/favorite spell bars, residual social/examine/floating chat, quests/map/vitae/options/smartbox, vendor/trade/salvage/tinkering, and D.6 nameplates/floaters | D.5/D.6 roadmap; retained layout registration set | Basic `gmCombatUI` now covers the active M2 melee/missile loop; Wave 10 lands each remaining surface against authoritative state | Large portions of retail gameplay still have no production UI | Named `gm*UI::PostInit` methods and LayoutDesc catalog | | ~~AP-111~~ | **RETIRED 2026-07-11 (M2 held-object parenting)** — equipped hand items are no longer omitted from the render world. CreateObject now preserves Placement/Parent/position timestamp bootstrap; live `0xF749` ParentEvent is parsed with retail sequence freshness; a focused render controller resolves `Setup.HoldingLocations`, applies the child's placement frame, and recomposes the separate child entity after every parent animation tick. Pickup retains the weenie's visual metadata for a later wield. | `src/AcDream.Core.Net/Messages/{CreateObject,ParentEvent}.cs`; `src/AcDream.Core/Meshing/EquippedChildAttachment.cs`; `src/AcDream.App/Rendering/EquippedChildRenderController.cs` | — | — | `ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310`; `SmartBox::HandleParentEvent @ 0x004535D0`; `CPhysicsObj::set_parent @ 0x00515A90`; `CPhysicsObj::UpdateChild @ 0x00512D50` | -| AP-112 | The basic combat bar ports visibility, height selection, desired-power slider, exact 1.0/0.8-second charge, trained-Recklessness range, ready-stance gating, request/release, server-response queueing, and auto-repeat, but omits `StartAttackRequest`'s `FinishJump`/`MaybeStopCompletely` command-interpreter calls | `src/AcDream.App/Combat/CombatAttackController.cs` | The M2 attack contract and authored basic panel are live; the remaining seam requires the jump/movement command owner rather than a UI-local guess | Starting an attack while charging a jump or deliberately moving may not stop/cancel exactly when retail does | `ClientCombatSystem::StartAttackRequest @ 0x0056C040` | +| AP-112 | The basic combat bar ports visibility, height selection, desired-power slider, exact 1.0/0.8-second charge, ready-stance gating, request/release, server-response queueing, and auto-repeat, but omits `StartAttackRequest`'s `FinishJump`/`MaybeStopCompletely` command-interpreter calls and exact trained-Recklessness visibility semantics (IA-20 keeps the dark range as the accepted baseline) | `src/AcDream.App/Combat/CombatAttackController.cs`; `src/AcDream.App/UI/Layout/CombatUiController.cs` | The M2 attack contract and authored basic panel are live; the remaining seams require the jump/movement command owner and a distinct Recklessness treatment rather than UI-local guesses | Starting an attack while charging a jump or deliberately moving may not stop/cancel exactly when retail does; trained/untrained Recklessness presentation is identical | `ClientCombatSystem::StartAttackRequest @ 0x0056C040`; `gmCombatUI::ListenToElementMessage @ 0x004CC430` | ## 4. Temporary stopgap (TS) — 38 rows (TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-44 narrowed same day — NPC UP unified onto the interp queue, gate retained for orientation) diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index 64c96d99..b655d677 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -497,7 +497,7 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar. - **Wave 4.4e implemented — exact missile ammo number (live gate pending).** Pure Core reproduces retail's thrown-weapon-vs-separate-ammo resolution over the ordered player equipment list and its zero-to-one count normalization. Relevant equipment/stack events update authored missile indicator `0x10000194` with the DAT font. Warning-free Release build and 4,754-pass / 5-skip suite are green; AP-101 is retired. - **M2 held-object parenting shipped and live-gated 2026-07-11.** The combat toggle now ports `GetDefaultCombatMode` over ordered equipped contents, so a bow requests Missile instead of the old hardcoded Melee. CreateObject preserves parent/placement/timestamp fields, `0xF749` ParentEvent is handled, and `EquippedChildRenderController` renders the weapon as a separate child composed from the animated hand part + holding frame + child placement frame. Live gate passed: bow selected missile stance, rendered in-hand, followed animation, unequipped cleanly, and melee remained correct. App Release builds with zero warnings; the full 4,765-pass / 5-skip suite is green. AP-111 is retired; research: `docs/research/2026-07-11-combat-default-and-parent-event-pseudocode.md`. - **M2 local attack receive funnel implemented 2026-07-11; live gate pending.** Retail `ExecuteAttack` only sends the request; ACE chooses the concrete melee/missile action and returns it in a non-autonomous mt-0 `UpdateMotion`. The local branch now runs that state through the same constructor-defaulted `MoveToInterpretedState` funnel and 15-bit action-stamp gate as remotes, then applies sticky/long-jump tails. The old local-only direct `Commands[]` replay is deleted. Shared conversion lives in `InboundInterpretedMotionFactory`; research: `docs/research/2026-07-11-local-combat-motion-pseudocode.md`. -- **M2 basic retail combat bar implemented 2026-07-11; corrective live visual gate pending.** Production mounts authored `gmCombatUI` LayoutDesc `0x21000073`, shows it only for Melee/Missile, and routes mouse plus keyboard through one `CombatAttackController`. Corrections include the layered gray/dark-red/live-bright-red power track, exact Speed-left/Power-right justification, trained-Recklessness range, silent control-only `AttackDone(ActionCancelled)`, target-frame Keep in View with manual orbit, and persistent corpse motion: the AP-80 velocity-only NPC adaptation can now replace only Ready/Walk/Run, never authoritative Dead/actions. `CombatTargetController` ports the selection-cleared AutoTarget consumer, so a selected creature's authoritative Dead motion clears it and selects the nearest eligible creature when enabled. The 2026-07-12 replacement-corpse correction unifies both multi-frame and static/reactive spawns behind retail's CreateObject lifecycle: apply the wire's Dead state while detached, then `MotionTableManager::HandleEnterWorld` strips Ready→Dead links before the first in-world tick; multiple corpses remaining fallen passed the live user gate that day. Follow-up #205 makes the toolbar read the final canonical selection after a reentrant Auto Target notice and restricts automatic candidates to hostile non-player monsters (intentional PK-edge divergence IA-19); that live gate also passed 2026-07-12. Research: `docs/research/2026-07-11-retail-combat-bar-pseudocode.md`, `docs/research/2026-07-11-combat-target-camera-pseudocode.md`, `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`; AP-24/AP-95 retired, AP-80 narrowed, AP-110 narrowed, AP-112 now records only the remaining attack-start command-interpreter seam. +- **M2 basic retail combat bar implemented 2026-07-11; corrective live visual gate pending.** Production mounts authored `gmCombatUI` LayoutDesc `0x21000073`, shows it only for Melee/Missile, and routes mouse plus keyboard through one `CombatAttackController`. Corrections include the centered dark-red baseline (accepted IA-20), left-to-right live bright-red charge fill from Speed, exact Speed-left/Power-right justification, silent control-only `AttackDone(ActionCancelled)`, target-frame Keep in View with manual orbit, and persistent corpse motion: the AP-80 velocity-only NPC adaptation can now replace only Ready/Walk/Run, never authoritative Dead/actions. `CombatTargetController` ports the selection-cleared AutoTarget consumer, so a selected creature's authoritative Dead motion clears it and selects the nearest eligible creature when enabled. The 2026-07-12 replacement-corpse correction unifies both multi-frame and static/reactive spawns behind retail's CreateObject lifecycle: apply the wire's Dead state while detached, then `MotionTableManager::HandleEnterWorld` strips Ready→Dead links before the first in-world tick; multiple corpses remaining fallen passed the live user gate that day. Follow-up #205 makes the toolbar read the final canonical selection after a reentrant Auto Target notice and restricts automatic candidates to hostile non-player monsters (intentional PK-edge divergence IA-19); that live gate also passed 2026-07-12. Research: `docs/research/2026-07-11-retail-combat-bar-pseudocode.md`, `docs/research/2026-07-11-combat-target-camera-pseudocode.md`, `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`; AP-24/AP-95 retired, AP-80 narrowed, AP-110 narrowed, AP-112 records the remaining attack-start and exact trained-Recklessness seams. - **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158. - **✓ SHIPPED — D.5.4 — Client object/item data model (foundation).** Shipped 2026-06-18 (`b506f53`..`a33e897`, 11 commits). Renamed `ItemRepository`→`ClientObjectTable` / `ItemInstance`→`ClientObject`; broadened the table to hold EVERY server object (retail `weenie_object_table` shape). `CreateObject` is now the canonical merge-upsert (`ClientObjectTable.Ingest`, retail `SetWeenieDesc` semantics) via a new Core.Net `ObjectTableWiring` (off GameWindow); `DeleteObject` evicts; `PlayerDescription` is a membership manifest (`RecordMembership`); live container-membership index (`GetContents`, retail `object_inventory_table`). `_liveEntityInfoByGuid` retired (selection/describe resolve from the one table). Root fix: the old enrich-existing-only `EnrichItem` dropped `CreateObject`s for items with no `PlayerDescription` stub — live-Coldeve 4/6 hotbar slots blank; items are now created, not dropped. **Crux resolved:** retail is TWO tables (`object_table` + `weenie_object_table`), NOT one — acdream's `WorldEntity` (3D system) + `ClientObjectTable` (data/UI) split was already architecturally faithful; the fix was the ingestion path, not a table unification. 2671 tests green. - **Roadmap correction (2026-07-10):** the completion order is now the architecture-first campaign in `docs/superpowers/plans/2026-07-10-retail-ui-fidelity-completion.md`. Retail `gmToolbarUI` is object-only: preserve `ShortCutData.index_`, `objectID_`, and `spellID_`, but do not invent spell glyphs on this bar. `PlayerModule::favorite_spells_[8]` feeds separate spell bars. diff --git a/docs/research/2026-07-11-retail-combat-bar-pseudocode.md b/docs/research/2026-07-11-retail-combat-bar-pseudocode.md index 51643487..c59791e8 100644 --- a/docs/research/2026-07-11-retail-combat-bar-pseudocode.md +++ b/docs/research/2026-07-11-retail-combat-bar-pseudocode.md @@ -171,23 +171,31 @@ applies the associated player option. track = scrollbar DirectState image thumb = structural child element 1 live power fill = nested meter child 0x10000050 / image child 2 -trained Recklessness range = nested meter child 0x100005EF +dark interior texture source = nested meter child 0x100005EF ``` These roles are identified by authored element ids, never by resolved width. The combat thumb is a 12x14 green jewel; inheritance reflow can make its effective child geometry wide enough that a width heuristic incorrectly tiles the jewel as the background. The base track is `0x060074CA`, thumb -`0x06001923`, live bright-red fill `0x06001200`, and the trained- -Recklessness dark-red range `0x0600715E`. +`0x06001923`, live bright-red fill `0x06001200`, and dark-red interior +texture `0x0600715E`. The three visual layers remain separate. The gray scrollbar track spans the -whole authored control. The dark-red Recklessness range is visible only when -skill `0x32` has advancement class `Trained` or higher; retail re-evaluates it -when scalar message `0x0A` is received. The meter fill is driven independently +whole authored control. Retail skill-gates element `0x100005EF` to Recklessness +advancement class `Trained` or higher when scalar message `0x0A` is received. +Per the connected visual gate, acdream uses that dark-red media as the always- +visible middle baseline (intentional divergence IA-20); the remaining exact +skill-gated treatment stays in AP-112. The meter fill is driven independently by `RecvNotice_SetPowerbarLevel`, while the green thumb is driven by `RecvNotice_DesiredAttackPowerChanged`. +The meter authors attribute `0x6F = 1`. `UIElement_Meter` constructor and +`DrawChildren @ 0x0046FBD0` make direction 1 the forward horizontal clip, so +the bright texture grows left-to-right from Speed. Direction 3 is the reverse +horizontal clip. The importer reads this attribute; it does not infer direction +from the combat element id. + The Speed and Power text rectangles are later siblings of the scrollbar. Their inner edges define the visible meter interval so the authored gray track stays behind both captions. Speed's raw property `0x14` value is `2`, which is left diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 033cbbd9..3f3a2ae3 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -2135,8 +2135,7 @@ public sealed class GameWindow : IDisposable Combat, _combatAttackController, () => _persistedGameplay, - SetRetailCombatGameplay, - () => LocalPlayer.GetSkill(0x32u)?.Status >= 2u), + SetRetailCombatGameplay), Toolbar: new AcDream.App.UI.ToolbarRuntimeBindings( Objects, () => Shortcuts, diff --git a/src/AcDream.App/Studio/FixtureProvider.cs b/src/AcDream.App/Studio/FixtureProvider.cs index 364d68b6..e4e65aae 100644 --- a/src/AcDream.App/Studio/FixtureProvider.cs +++ b/src/AcDream.App/Studio/FixtureProvider.cs @@ -105,7 +105,6 @@ public static class FixtureProvider attacks, () => gameplay, value => gameplay = value, - () => true, new CombatUiLabels( "Speed", "Power", "Repeat Attacks", "Auto Target", "Keep in View", "High", "Medium", "Low"), diff --git a/src/AcDream.App/UI/Layout/CombatUiController.cs b/src/AcDream.App/UI/Layout/CombatUiController.cs index 0d69a568..e5ab72aa 100644 --- a/src/AcDream.App/UI/Layout/CombatUiController.cs +++ b/src/AcDream.App/UI/Layout/CombatUiController.cs @@ -66,7 +66,6 @@ public sealed class CombatUiController : IRetainedPanelController CombatAttackController attacks, Func gameplay, Action setGameplay, - Func recklessnessTrained, CombatUiLabels labels, Action setWindowVisible) { @@ -95,7 +94,6 @@ public sealed class CombatUiController : IRetainedPanelController _powerControl.SetScalarPosition(_attacks.DesiredPower); _powerControl.ScalarChanged = _attacks.SetDesiredPower; _powerControl.ScalarFill = () => _attacks.PowerBarLevel; - _powerControl.ScalarRangeVisible = recklessnessTrained; BindAttackButton(_high, AttackHeight.High); BindAttackButton(_medium, AttackHeight.Medium); @@ -131,7 +129,6 @@ public sealed class CombatUiController : IRetainedPanelController CombatAttackController attacks, Func gameplay, Action setGameplay, - Func recklessnessTrained, CombatUiLabels labels, Action setWindowVisible) { @@ -140,7 +137,6 @@ public sealed class CombatUiController : IRetainedPanelController ArgumentNullException.ThrowIfNull(attacks); ArgumentNullException.ThrowIfNull(gameplay); ArgumentNullException.ThrowIfNull(setGameplay); - ArgumentNullException.ThrowIfNull(recklessnessTrained); ArgumentNullException.ThrowIfNull(labels); ArgumentNullException.ThrowIfNull(setWindowVisible); @@ -158,8 +154,7 @@ public sealed class CombatUiController : IRetainedPanelController return new CombatUiController( layout, basic, advanced, power, high, medium, low, repeatAttacks, autoTarget, keepInView, - combat, attacks, gameplay, setGameplay, recklessnessTrained, - labels, setWindowVisible); + combat, attacks, gameplay, setGameplay, labels, setWindowVisible); } public void SyncVisibility() => OnCombatModeChanged(_combat.CurrentMode); @@ -238,7 +233,6 @@ public sealed class CombatUiController : IRetainedPanelController _attacks.StateChanged -= OnAttackStateChanged; _powerControl.ScalarChanged = null; _powerControl.ScalarFill = () => null; - _powerControl.ScalarRangeVisible = () => false; _high.OnPressed = null; _high.OnReleased = null; _medium.OnPressed = null; diff --git a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs index aa49c7b6..c773a26d 100644 --- a/src/AcDream.App/UI/Layout/DatWidgetFactory.cs +++ b/src/AcDream.App/UI/Layout/DatWidgetFactory.cs @@ -178,19 +178,22 @@ public static class DatWidgetFactory .FirstOrDefault(); bar.ScalarFillSprite = fill is null ? 0u : DefaultImage(fill); - // gmCombatUI preserves a second authored meter child for the - // trained-Recklessness range. The controller owns its visibility, - // exactly like gmCombatUI::ListenToElementMessage @ 0x004CC430; - // the widget only retains the media because it consumes DAT children. + // gmCombatUI preserves a second authored meter child for its dark + // red interior range. The widget retains the media because horizontal + // scrollbars consume their DAT children. ElementInfo? scalarRange = meter?.Children.FirstOrDefault( child => child.Id == 0x100005EFu); bar.ScalarRangeSprite = scalarRange is null ? 0u : DefaultImage(scalarRange); - // gmCombatUI meter 0x10000050 grows from the Power end at the - // right toward Speed at the left (retail screenshot oracle + - // gmCombatUI::RecvNotice_SetPowerbarLevel 0x004CC0E0). - bar.ScalarFillFromRight = meter?.Id == 0x10000050u; + // UIElement_Meter::UIElement_Meter @ 0x0046F4C0 defaults direction + // 1; DrawChildren @ 0x0046FBD0 clips that direction left-to-right. + // Direction 3 is the horizontal reverse. Read authored attribute + // 0x6F instead of inferring direction from the combat element id. + bar.ScalarFillFromRight = meter is not null + && meter.TryGetEffectiveProperty(0x6Fu, out UiPropertyValue direction) + && direction.Kind == UiPropertyKind.Enum + && direction.UnsignedValue == 3u; return bar; } diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index 47c39922..8c03695b 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -40,8 +40,7 @@ public sealed record CombatRuntimeBindings( CombatState State, CombatAttackController Attacks, Func Gameplay, - Action SetGameplay, - Func RecklessnessTrained); + Action SetGameplay); public sealed record ToolbarRuntimeBindings( ClientObjectTable Objects, @@ -481,7 +480,6 @@ public sealed class RetailUiRuntime : IDisposable _bindings.Combat.Attacks, _bindings.Combat.Gameplay, _bindings.Combat.SetGameplay, - _bindings.Combat.RecklessnessTrained, labels, visible => { diff --git a/src/AcDream.App/UI/UiScrollbar.cs b/src/AcDream.App/UI/UiScrollbar.cs index cf0a2db8..0a082f39 100644 --- a/src/AcDream.App/UI/UiScrollbar.cs +++ b/src/AcDream.App/UI/UiScrollbar.cs @@ -42,11 +42,10 @@ public sealed class UiScrollbar : UiElement public Func ScalarFill { get; set; } = () => null; public uint ScalarFillSprite { get; set; } /// - /// Optional authored texture beneath the live scalar fill. gmCombatUI uses - /// element 0x100005EF for the trained-Recklessness range. + /// Optional authored texture beneath the live scalar fill. gmCombatUI's + /// dark-red interior media comes from element 0x100005EF. /// public uint ScalarRangeSprite { get; set; } - public Func ScalarRangeVisible { get; set; } = () => false; public float ScalarRangeLeft { get; set; } public float ScalarRangeWidth { get; set; } = float.PositiveInfinity; /// @@ -139,7 +138,7 @@ public sealed class UiScrollbar : UiElement { DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height); (float rangeLeft, float rangeWidth) = ScalarRangeRect(); - if (ScalarRangeVisible() && ScalarRangeSprite != 0) + if (ScalarRangeSprite != 0) DrawTiled(ctx, resolve, ScalarRangeSprite, rangeLeft, 0f, rangeWidth, Height); if (ScalarFill() is float fill && ScalarFillSprite != 0) diff --git a/tests/AcDream.App.Tests/UI/Layout/CombatLayoutConformanceTests.cs b/tests/AcDream.App.Tests/UI/Layout/CombatLayoutConformanceTests.cs index b4ece67b..4f4d913c 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CombatLayoutConformanceTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CombatLayoutConformanceTests.cs @@ -28,7 +28,7 @@ public sealed class CombatLayoutConformanceTests Assert.Equal(0x06001923u, power.ThumbSprite); Assert.Equal(0x06001200u, power.ScalarFillSprite); Assert.Equal(0x0600715Eu, power.ScalarRangeSprite); - Assert.True(power.ScalarFillFromRight); + Assert.False(power.ScalarFillFromRight); var repeat = Assert.IsType( layout.FindElement(CombatUiController.RepeatAttacksId)); diff --git a/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs index 16d54036..311d3bb5 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CombatUiControllerTests.cs @@ -21,7 +21,6 @@ public sealed class CombatUiControllerTests GameplaySettings gameplay = GameplaySettings.Default; using var controller = CombatUiController.Bind( layout, combat, attacks, () => gameplay, value => gameplay = value, - () => false, Labels, visibility.Add)!; controller.SyncVisibility(); @@ -46,14 +45,12 @@ public sealed class CombatUiControllerTests GameplaySettings gameplay = GameplaySettings.Default; using var controller = CombatUiController.Bind( layout, combat, attacks, () => gameplay, value => gameplay = value, - () => true, Labels, _ => { })!; combat.SetCombatMode(CombatMode.Melee); high.OnEvent(new UiEvent(0, high, UiEventType.MouseDown, Data1: 2, Data2: 2)); now = 4.5d; Assert.Equal(0.5f, power.ScalarFill()!.Value, 3); - Assert.True(power.ScalarRangeVisible()); Assert.Equal(104f, power.ScalarRangeLeft, 3); Assert.Equal(299f, power.ScalarRangeWidth, 3); high.OnEvent(new UiEvent(0, high, UiEventType.MouseUp, Data1: 2, Data2: 2)); @@ -74,7 +71,6 @@ public sealed class CombatUiControllerTests GameplaySettings gameplay = GameplaySettings.Default; using var controller = CombatUiController.Bind( layout, combat, attacks, () => gameplay, value => gameplay = value, - () => false, Labels, _ => { })!; var autoTarget = Assert.IsType(layout.FindElement(CombatUiController.AutoTargetId)); @@ -94,7 +90,7 @@ public sealed class CombatUiControllerTests GameplaySettings gameplay = GameplaySettings.Default; using var controller = CombatUiController.Bind( layout, combat, attacks, () => gameplay, value => gameplay = value, - () => true, Labels, _ => { })!; + Labels, _ => { })!; var speed = Assert.IsType(layout.FindElement(CombatUiController.SpeedLabelId)); var powerLabel = Assert.IsType(layout.FindElement(CombatUiController.PowerLabelId)); diff --git a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs index c7eeb5c8..5d6f700b 100644 --- a/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/DatWidgetFactoryTests.cs @@ -485,6 +485,10 @@ public class DatWidgetFactoryTests Height = 14, Children = [fill, range], }; + var meterState = new UiStateInfo { Id = UiStateInfo.DirectStateId }; + meterState.Properties.Values[0x6Fu] = new UiPropertyValue + { Kind = UiPropertyKind.Enum, UnsignedValue = 1u }; + meter.States[UiStateInfo.DirectStateId] = meterState; var info = new ElementInfo { Id = CombatUiController.PowerControlId, @@ -502,7 +506,7 @@ public class DatWidgetFactoryTests Assert.Equal(0x06001923u, bar.ThumbSprite); Assert.Equal(0x06001200u, bar.ScalarFillSprite); Assert.Equal(0x0600715Eu, bar.ScalarRangeSprite); - Assert.True(bar.ScalarFillFromRight); + Assert.False(bar.ScalarFillFromRight); } private static ElementInfo TextInfo(params (uint Id, UiPropertyValue Value)[] properties) diff --git a/tests/AcDream.App.Tests/UI/UiScrollbarTests.cs b/tests/AcDream.App.Tests/UI/UiScrollbarTests.cs index b093ab8e..47694c77 100644 --- a/tests/AcDream.App.Tests/UI/UiScrollbarTests.cs +++ b/tests/AcDream.App.Tests/UI/UiScrollbarTests.cs @@ -104,26 +104,26 @@ public class UiScrollbarTests } [Theory] - [InlineData(0f, 100f, 0f)] - [InlineData(0.5f, 50f, 50f)] - [InlineData(1f, 0f, 100f)] - public void ScalarFillRect_CombatPower_GrowsRightToLeft( + [InlineData(0f, 0f, 0f)] + [InlineData(0.5f, 0f, 50f)] + [InlineData(1f, 0f, 100f)] + public void ScalarFillRect_CombatPower_GrowsLeftToRight( float fill, float expectedX, float expectedWidth) { - var (x, width) = UiScrollbar.ScalarFillRect(100f, fill, fromRight: true); + var (x, width) = UiScrollbar.ScalarFillRect(100f, fill, fromRight: false); Assert.Equal(expectedX, x, 3); Assert.Equal(expectedWidth, width, 3); } [Theory] - [InlineData(0f, 403f, 0f)] - [InlineData(0.5f, 253.5f, 149.5f)] + [InlineData(0f, 104f, 0f)] + [InlineData(0.5f, 104f, 149.5f)] [InlineData(1f, 104f, 299f)] public void ScalarFillRect_CombatPower_StaysBetweenAuthoredLabels( float fill, float expectedX, float expectedWidth) { var (x, width) = UiScrollbar.ScalarFillRect( - rangeLeft: 104f, rangeWidth: 299f, fill, fromRight: true); + rangeLeft: 104f, rangeWidth: 299f, fill, fromRight: false); Assert.Equal(expectedX, x, 3); Assert.Equal(expectedWidth, width, 3); }