fix(combat): restore retail power bar layers
Preserve the authored gray track, trained-Recklessness range, live bright charge meter, and independent desired-power thumb while keeping Speed and Power over gray side regions. Correct retail text justification value 2 to left alignment and retain direct RenderSurface decoding in the texture inspection tool used to verify the assets. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
0f2d98c501
commit
256c1930bd
18 changed files with 200 additions and 32 deletions
|
|
@ -213,7 +213,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-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-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-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, ready-stance gating, request/release, server-response queueing, and auto-repeat, but omits `StartAttackRequest`'s `FinishJump`/`MaybeStopCompletely` command-interpreter calls and the trained-Recklessness submeter | `src/AcDream.App/Combat/CombatAttackController.cs`; `src/AcDream.App/UI/Layout/CombatUiController.cs` | The M2 attack contract and authored basic panel are live; the two omitted seams require the jump-command owner and skill-advancement state rather than UI-local guesses | Starting an attack while charging a jump or deliberately moving may not stop/cancel exactly when retail does; trained Recklessness lacks its green range overlay | `ClientCombatSystem::StartAttackRequest @ 0x0056C040`; `gmCombatUI::ListenToElementMessage @ 0x004CC430`; `RecvNotice_SetCombatMode @ 0x004CC620` |
|
| 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` |
|
||||||
|
|
||||||
## 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)
|
## 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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
- **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 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 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 right-to-left red charge meter, 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 advanced/Recklessness and command-interpreter seams.
|
- **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.
|
||||||
- **✓ 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 — 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.
|
- **✓ 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.
|
- **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.
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ Concrete subclasses (`DatReaderWriter.Types.*`):
|
||||||
|-----|-------------------|---------|-----------|
|
|-----|-------------------|---------|-----------|
|
||||||
| `0x1A` | `ArrayBaseProperty` (contains `DataIdBaseProperty`) | **Font DID** — array with one item; the inner `DataIdBaseProperty.Value` is the font dat object id | `UIElement_Text::SetFontDIDHelper(this, 0x1a, ...)` @`0x46829e` |
|
| `0x1A` | `ArrayBaseProperty` (contains `DataIdBaseProperty`) | **Font DID** — array with one item; the inner `DataIdBaseProperty.Value` is the font dat object id | `UIElement_Text::SetFontDIDHelper(this, 0x1a, ...)` @`0x46829e` |
|
||||||
| `0x1B` | `ArrayBaseProperty` (contains `ColorBaseProperty`) | **Font color** — array with one item; `ColorARGB {R,G,B,A}` | `UIElement_Text::SetFontColorHelper(this, 0x1b, ...)` @`0x4682c2` |
|
| `0x1B` | `ArrayBaseProperty` (contains `ColorBaseProperty`) | **Font color** — array with one item; `ColorARGB {R,G,B,A}` | `UIElement_Text::SetFontColorHelper(this, 0x1b, ...)` @`0x4682c2` |
|
||||||
| `0x14` | `EnumBaseProperty` | **Horizontal justification** | `UIElement_Text::SetHorizontalJustification` @`0x467200` |
|
| `0x14` | `EnumBaseProperty` | **Horizontal justification** — `1` center, `3/5` right, every other value (including constructor default `2`) left | `UIElement_Text::CalcJustification` @`0x467260` |
|
||||||
| `0x15` | `EnumBaseProperty` | **Vertical justification** | `UIElement_Text::SetVerticalJustification` @`0x467230` |
|
| `0x15` | `EnumBaseProperty` | **Vertical justification** | `UIElement_Text::SetVerticalJustification` @`0x467230` |
|
||||||
| `0x1C` / `0x1D` | `ArrayBaseProperty` | Tag font color / tag font | (secondary font style for in-text tags) |
|
| `0x1C` / `0x1D` | `ArrayBaseProperty` | Tag font color / tag font | (secondary font style for in-text tags) |
|
||||||
| `0x16` | `BoolBaseProperty` | Some text flag | |
|
| `0x16` | `BoolBaseProperty` | Some text flag | |
|
||||||
|
|
|
||||||
|
|
@ -171,10 +171,26 @@ applies the associated player option.
|
||||||
track = scrollbar DirectState image
|
track = scrollbar DirectState image
|
||||||
thumb = structural child element 1
|
thumb = structural child element 1
|
||||||
live power fill = nested meter child 0x10000050 / image child 2
|
live power fill = nested meter child 0x10000050 / image child 2
|
||||||
|
trained Recklessness range = nested meter child 0x100005EF
|
||||||
```
|
```
|
||||||
|
|
||||||
These roles are identified by authored element ids, never by resolved width.
|
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
|
The combat thumb is a 12x14 green jewel; inheritance reflow can make its
|
||||||
effective child geometry wide enough that a width heuristic incorrectly tiles
|
effective child geometry wide enough that a width heuristic incorrectly tiles
|
||||||
the jewel as the background. The base track is `0x060074CA`, thumb
|
the jewel as the background. The base track is `0x060074CA`, thumb
|
||||||
`0x06001923`, and live red fill `0x06001200`.
|
`0x06001923`, live bright-red fill `0x06001200`, and the trained-
|
||||||
|
Recklessness dark-red range `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
|
||||||
|
by `RecvNotice_SetPowerbarLevel`, while the green thumb is driven by
|
||||||
|
`RecvNotice_DesiredAttackPowerChanged`.
|
||||||
|
|
||||||
|
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
|
||||||
|
justification: `UIElement_Text::CalcJustification @ 0x00467260` centers only
|
||||||
|
value `1`, right-aligns values `3` and `5`, and treats every other value as left.
|
||||||
|
Power uses right justification.
|
||||||
|
|
|
||||||
|
|
@ -2135,7 +2135,8 @@ public sealed class GameWindow : IDisposable
|
||||||
Combat,
|
Combat,
|
||||||
_combatAttackController,
|
_combatAttackController,
|
||||||
() => _persistedGameplay,
|
() => _persistedGameplay,
|
||||||
SetRetailCombatGameplay),
|
SetRetailCombatGameplay,
|
||||||
|
() => LocalPlayer.GetSkill(0x32u)?.Status >= 2u),
|
||||||
Toolbar: new AcDream.App.UI.ToolbarRuntimeBindings(
|
Toolbar: new AcDream.App.UI.ToolbarRuntimeBindings(
|
||||||
Objects,
|
Objects,
|
||||||
() => Shortcuts,
|
() => Shortcuts,
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,7 @@ public static class FixtureProvider
|
||||||
attacks,
|
attacks,
|
||||||
() => gameplay,
|
() => gameplay,
|
||||||
value => gameplay = value,
|
value => gameplay = value,
|
||||||
|
() => true,
|
||||||
new CombatUiLabels(
|
new CombatUiLabels(
|
||||||
"Speed", "Power", "Repeat Attacks", "Auto Target", "Keep in View",
|
"Speed", "Power", "Repeat Attacks", "Auto Target", "Keep in View",
|
||||||
"High", "Medium", "Low"),
|
"High", "Medium", "Low"),
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ public sealed class CombatUiController : IRetainedPanelController
|
||||||
CombatAttackController attacks,
|
CombatAttackController attacks,
|
||||||
Func<GameplaySettings> gameplay,
|
Func<GameplaySettings> gameplay,
|
||||||
Action<GameplaySettings> setGameplay,
|
Action<GameplaySettings> setGameplay,
|
||||||
|
Func<bool> recklessnessTrained,
|
||||||
CombatUiLabels labels,
|
CombatUiLabels labels,
|
||||||
Action<bool> setWindowVisible)
|
Action<bool> setWindowVisible)
|
||||||
{
|
{
|
||||||
|
|
@ -94,6 +95,7 @@ public sealed class CombatUiController : IRetainedPanelController
|
||||||
_powerControl.SetScalarPosition(_attacks.DesiredPower);
|
_powerControl.SetScalarPosition(_attacks.DesiredPower);
|
||||||
_powerControl.ScalarChanged = _attacks.SetDesiredPower;
|
_powerControl.ScalarChanged = _attacks.SetDesiredPower;
|
||||||
_powerControl.ScalarFill = () => _attacks.PowerBarLevel;
|
_powerControl.ScalarFill = () => _attacks.PowerBarLevel;
|
||||||
|
_powerControl.ScalarRangeVisible = recklessnessTrained;
|
||||||
|
|
||||||
BindAttackButton(_high, AttackHeight.High);
|
BindAttackButton(_high, AttackHeight.High);
|
||||||
BindAttackButton(_medium, AttackHeight.Medium);
|
BindAttackButton(_medium, AttackHeight.Medium);
|
||||||
|
|
@ -105,8 +107,11 @@ public sealed class CombatUiController : IRetainedPanelController
|
||||||
_repeatAttacks.Label = labels.RepeatAttacks;
|
_repeatAttacks.Label = labels.RepeatAttacks;
|
||||||
_autoTarget.Label = labels.AutoTarget;
|
_autoTarget.Label = labels.AutoTarget;
|
||||||
_keepInView.Label = labels.KeepInView;
|
_keepInView.Label = labels.KeepInView;
|
||||||
SetStaticText(layout.FindElement(SpeedLabelId) as UiText, labels.Speed);
|
UiText? speedLabel = layout.FindElement(SpeedLabelId) as UiText;
|
||||||
SetStaticText(layout.FindElement(PowerLabelId) as UiText, labels.Power);
|
UiText? powerLabel = layout.FindElement(PowerLabelId) as UiText;
|
||||||
|
SetStaticText(speedLabel, labels.Speed, rightAligned: false);
|
||||||
|
SetStaticText(powerLabel, labels.Power, rightAligned: true);
|
||||||
|
ConfigurePowerRange(speedLabel, powerLabel);
|
||||||
|
|
||||||
_repeatAttacks.OnClick = () =>
|
_repeatAttacks.OnClick = () =>
|
||||||
_setGameplay(_gameplay() with { AutoRepeatAttack = _repeatAttacks.Selected });
|
_setGameplay(_gameplay() with { AutoRepeatAttack = _repeatAttacks.Selected });
|
||||||
|
|
@ -126,6 +131,7 @@ public sealed class CombatUiController : IRetainedPanelController
|
||||||
CombatAttackController attacks,
|
CombatAttackController attacks,
|
||||||
Func<GameplaySettings> gameplay,
|
Func<GameplaySettings> gameplay,
|
||||||
Action<GameplaySettings> setGameplay,
|
Action<GameplaySettings> setGameplay,
|
||||||
|
Func<bool> recklessnessTrained,
|
||||||
CombatUiLabels labels,
|
CombatUiLabels labels,
|
||||||
Action<bool> setWindowVisible)
|
Action<bool> setWindowVisible)
|
||||||
{
|
{
|
||||||
|
|
@ -134,6 +140,7 @@ public sealed class CombatUiController : IRetainedPanelController
|
||||||
ArgumentNullException.ThrowIfNull(attacks);
|
ArgumentNullException.ThrowIfNull(attacks);
|
||||||
ArgumentNullException.ThrowIfNull(gameplay);
|
ArgumentNullException.ThrowIfNull(gameplay);
|
||||||
ArgumentNullException.ThrowIfNull(setGameplay);
|
ArgumentNullException.ThrowIfNull(setGameplay);
|
||||||
|
ArgumentNullException.ThrowIfNull(recklessnessTrained);
|
||||||
ArgumentNullException.ThrowIfNull(labels);
|
ArgumentNullException.ThrowIfNull(labels);
|
||||||
ArgumentNullException.ThrowIfNull(setWindowVisible);
|
ArgumentNullException.ThrowIfNull(setWindowVisible);
|
||||||
|
|
||||||
|
|
@ -151,7 +158,8 @@ public sealed class CombatUiController : IRetainedPanelController
|
||||||
return new CombatUiController(
|
return new CombatUiController(
|
||||||
layout, basic, advanced, power, high, medium, low,
|
layout, basic, advanced, power, high, medium, low,
|
||||||
repeatAttacks, autoTarget, keepInView,
|
repeatAttacks, autoTarget, keepInView,
|
||||||
combat, attacks, gameplay, setGameplay, labels, setWindowVisible);
|
combat, attacks, gameplay, setGameplay, recklessnessTrained,
|
||||||
|
labels, setWindowVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SyncVisibility() => OnCombatModeChanged(_combat.CurrentMode);
|
public void SyncVisibility() => OnCombatModeChanged(_combat.CurrentMode);
|
||||||
|
|
@ -190,7 +198,23 @@ public sealed class CombatUiController : IRetainedPanelController
|
||||||
_keepInView.Selected = gameplay.ViewCombatTarget;
|
_keepInView.Selected = gameplay.ViewCombatTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SetStaticText(UiText? text, string value)
|
private void ConfigurePowerRange(UiText? speedLabel, UiText? powerLabel)
|
||||||
|
{
|
||||||
|
if (speedLabel is null || powerLabel is null) return;
|
||||||
|
|
||||||
|
// All three elements are siblings in gmCombatUI's basic page. Keeping
|
||||||
|
// the meter between the authored text rectangles leaves the base gray
|
||||||
|
// track behind Speed/Power while the dark/live red textures occupy the
|
||||||
|
// semantic power range between them.
|
||||||
|
float left = speedLabel.Left + speedLabel.Width - _powerControl.Left;
|
||||||
|
float right = powerLabel.Left - _powerControl.Left;
|
||||||
|
_powerControl.ScalarRangeLeft = Math.Clamp(left, 0f, _powerControl.Width);
|
||||||
|
_powerControl.ScalarRangeWidth = Math.Max(
|
||||||
|
0f, Math.Clamp(right, 0f, _powerControl.Width)
|
||||||
|
- _powerControl.ScalarRangeLeft);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetStaticText(UiText? text, string value, bool rightAligned)
|
||||||
{
|
{
|
||||||
if (text is null) return;
|
if (text is null) return;
|
||||||
// Retail UIElement_Text starts with zero margins; these 15px-high
|
// Retail UIElement_Text starts with zero margins; these 15px-high
|
||||||
|
|
@ -198,6 +222,8 @@ public sealed class CombatUiController : IRetainedPanelController
|
||||||
// generic 4px inset would leave less than one glyph row and clip them all.
|
// generic 4px inset would leave less than one glyph row and clip them all.
|
||||||
text.OneLine = true;
|
text.OneLine = true;
|
||||||
text.Padding = 0f;
|
text.Padding = 0f;
|
||||||
|
text.Centered = false;
|
||||||
|
text.RightAligned = rightAligned;
|
||||||
UiText.Line[] line = [new UiText.Line(value, text.DefaultColor)];
|
UiText.Line[] line = [new UiText.Line(value, text.DefaultColor)];
|
||||||
text.LinesProvider = () => line;
|
text.LinesProvider = () => line;
|
||||||
}
|
}
|
||||||
|
|
@ -212,6 +238,7 @@ public sealed class CombatUiController : IRetainedPanelController
|
||||||
_attacks.StateChanged -= OnAttackStateChanged;
|
_attacks.StateChanged -= OnAttackStateChanged;
|
||||||
_powerControl.ScalarChanged = null;
|
_powerControl.ScalarChanged = null;
|
||||||
_powerControl.ScalarFill = () => null;
|
_powerControl.ScalarFill = () => null;
|
||||||
|
_powerControl.ScalarRangeVisible = () => false;
|
||||||
_high.OnPressed = null;
|
_high.OnPressed = null;
|
||||||
_high.OnReleased = null;
|
_high.OnReleased = null;
|
||||||
_medium.OnPressed = null;
|
_medium.OnPressed = null;
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,16 @@ public static class DatWidgetFactory
|
||||||
.OrderByDescending(child => child.ReadOrder)
|
.OrderByDescending(child => child.ReadOrder)
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
bar.ScalarFillSprite = fill is null ? 0u : DefaultImage(fill);
|
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.
|
||||||
|
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
|
// gmCombatUI meter 0x10000050 grows from the Power end at the
|
||||||
// right toward Speed at the left (retail screenshot oracle +
|
// right toward Speed at the left (retail screenshot oracle +
|
||||||
// gmCombatUI::RecvNotice_SetPowerbarLevel 0x004CC0E0).
|
// gmCombatUI::RecvNotice_SetPowerbarLevel 0x004CC0E0).
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ namespace AcDream.App.UI.Layout;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Horizontal text justification read from dat property 0x14 (UIElement HorizontalJustification).
|
/// Horizontal text justification read from dat property 0x14 (UIElement HorizontalJustification).
|
||||||
/// Values match the retail enum: 0=Left, 1=Center, 3/5=Right.
|
/// Retail <c>CalcJustification @ 0x00467260</c> treats 1 as Center,
|
||||||
|
/// 3/5 as Right, and every other value (including constructor default 2) as Left.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public enum HJustify : byte { Left = 0, Center = 1, Right = 2 }
|
public enum HJustify : byte { Left = 0, Center = 1, Right = 2 }
|
||||||
|
|
||||||
|
|
@ -348,7 +349,7 @@ public static class ElementReader
|
||||||
{
|
{
|
||||||
info.HJustify = horizontal.UnsignedValue switch
|
info.HJustify = horizontal.UnsignedValue switch
|
||||||
{
|
{
|
||||||
0u => HJustify.Left,
|
0u or 2u => HJustify.Left,
|
||||||
3u or 5u => HJustify.Right,
|
3u or 5u => HJustify.Right,
|
||||||
_ => HJustify.Center,
|
_ => HJustify.Center,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -435,7 +435,8 @@ public static class LayoutImporter
|
||||||
if (sd.Properties is not null)
|
if (sd.Properties is not null)
|
||||||
{
|
{
|
||||||
// HorizontalJustification (0x14): EnumBaseProperty.
|
// HorizontalJustification (0x14): EnumBaseProperty.
|
||||||
// Retail values: 0=Left, 1=Center, 3=Right, 5=Right (treat 5 as Right per spec).
|
// Retail CalcJustification @ 0x00467260: 1=Center, 3/5=Right,
|
||||||
|
// every other value (including constructor default 2)=Left.
|
||||||
// Only update if still at the default (Center); derived-wins handled in Merge.
|
// Only update if still at the default (Center); derived-wins handled in Merge.
|
||||||
if (info.HJustify == HJustify.Center
|
if (info.HJustify == HJustify.Center
|
||||||
&& sd.Properties.TryGetValue(0x14u, out var hRaw)
|
&& sd.Properties.TryGetValue(0x14u, out var hRaw)
|
||||||
|
|
@ -443,11 +444,11 @@ public static class LayoutImporter
|
||||||
{
|
{
|
||||||
info.HJustify = hEnum.Value switch
|
info.HJustify = hEnum.Value switch
|
||||||
{
|
{
|
||||||
0u => HJustify.Left,
|
0u or 2u => HJustify.Left,
|
||||||
1u => HJustify.Center,
|
1u => HJustify.Center,
|
||||||
3u => HJustify.Right,
|
3u => HJustify.Right,
|
||||||
5u => HJustify.Right,
|
5u => HJustify.Right,
|
||||||
_ => HJustify.Center, // unknown → center (safe default)
|
_ => HJustify.Left,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ public sealed record CombatRuntimeBindings(
|
||||||
CombatState State,
|
CombatState State,
|
||||||
CombatAttackController Attacks,
|
CombatAttackController Attacks,
|
||||||
Func<GameplaySettings> Gameplay,
|
Func<GameplaySettings> Gameplay,
|
||||||
Action<GameplaySettings> SetGameplay);
|
Action<GameplaySettings> SetGameplay,
|
||||||
|
Func<bool> RecklessnessTrained);
|
||||||
|
|
||||||
public sealed record ToolbarRuntimeBindings(
|
public sealed record ToolbarRuntimeBindings(
|
||||||
ClientObjectTable Objects,
|
ClientObjectTable Objects,
|
||||||
|
|
@ -480,6 +481,7 @@ public sealed class RetailUiRuntime : IDisposable
|
||||||
_bindings.Combat.Attacks,
|
_bindings.Combat.Attacks,
|
||||||
_bindings.Combat.Gameplay,
|
_bindings.Combat.Gameplay,
|
||||||
_bindings.Combat.SetGameplay,
|
_bindings.Combat.SetGameplay,
|
||||||
|
_bindings.Combat.RecklessnessTrained,
|
||||||
labels,
|
labels,
|
||||||
visible =>
|
visible =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,14 @@ public sealed class UiScrollbar : UiElement
|
||||||
public Func<float?> ScalarFill { get; set; } = () => null;
|
public Func<float?> ScalarFill { get; set; } = () => null;
|
||||||
public uint ScalarFillSprite { get; set; }
|
public uint ScalarFillSprite { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// Optional authored texture beneath the live scalar fill. gmCombatUI uses
|
||||||
|
/// element <c>0x100005EF</c> for the trained-Recklessness range.
|
||||||
|
/// </summary>
|
||||||
|
public uint ScalarRangeSprite { get; set; }
|
||||||
|
public Func<bool> ScalarRangeVisible { get; set; } = () => false;
|
||||||
|
public float ScalarRangeLeft { get; set; }
|
||||||
|
public float ScalarRangeWidth { get; set; } = float.PositiveInfinity;
|
||||||
|
/// <summary>
|
||||||
/// Draw the scalar meter from the power end (right) toward the speed end
|
/// Draw the scalar meter from the power end (right) toward the speed end
|
||||||
/// (left). This is the authored gmCombatUI power meter direction.
|
/// (left). This is the authored gmCombatUI power meter direction.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -113,9 +121,15 @@ public sealed class UiScrollbar : UiElement
|
||||||
/// <summary>Returns the clipped scalar-meter span in local pixels.</summary>
|
/// <summary>Returns the clipped scalar-meter span in local pixels.</summary>
|
||||||
public static (float x, float width) ScalarFillRect(
|
public static (float x, float width) ScalarFillRect(
|
||||||
float totalWidth, float fill, bool fromRight)
|
float totalWidth, float fill, bool fromRight)
|
||||||
|
=> ScalarFillRect(0f, totalWidth, fill, fromRight);
|
||||||
|
|
||||||
|
/// <summary>Returns a clipped scalar-meter span inside an authored sub-range.</summary>
|
||||||
|
public static (float x, float width) ScalarFillRect(
|
||||||
|
float rangeLeft, float rangeWidth, float fill, bool fromRight)
|
||||||
{
|
{
|
||||||
float visibleWidth = totalWidth * Math.Clamp(fill, 0f, 1f);
|
float safeWidth = MathF.Max(0f, rangeWidth);
|
||||||
return (fromRight ? totalWidth - visibleWidth : 0f, visibleWidth);
|
float visibleWidth = safeWidth * Math.Clamp(fill, 0f, 1f);
|
||||||
|
return (fromRight ? rangeLeft + safeWidth - visibleWidth : rangeLeft, visibleWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDraw(UiRenderContext ctx)
|
protected override void OnDraw(UiRenderContext ctx)
|
||||||
|
|
@ -124,14 +138,16 @@ public sealed class UiScrollbar : UiElement
|
||||||
if (Horizontal)
|
if (Horizontal)
|
||||||
{
|
{
|
||||||
DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height);
|
DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height);
|
||||||
|
(float rangeLeft, float rangeWidth) = ScalarRangeRect();
|
||||||
|
if (ScalarRangeVisible() && ScalarRangeSprite != 0)
|
||||||
|
DrawTiled(ctx, resolve, ScalarRangeSprite,
|
||||||
|
rangeLeft, 0f, rangeWidth, Height);
|
||||||
if (ScalarFill() is float fill && ScalarFillSprite != 0)
|
if (ScalarFill() is float fill && ScalarFillSprite != 0)
|
||||||
{
|
{
|
||||||
var (fillX, visibleWidth) = ScalarFillRect(Width, fill, ScalarFillFromRight);
|
var (fillX, visibleWidth) = ScalarFillRect(
|
||||||
if (ScalarFillFromRight)
|
rangeLeft, rangeWidth, fill, ScalarFillFromRight);
|
||||||
DrawTiledRightClipped(
|
DrawTiledClipped(ctx, resolve, ScalarFillSprite,
|
||||||
ctx, resolve, ScalarFillSprite, fillX, visibleWidth, Height);
|
rangeLeft, fillX, visibleWidth, Height);
|
||||||
else
|
|
||||||
DrawTiled(ctx, resolve, ScalarFillSprite, 0f, 0f, visibleWidth, Height);
|
|
||||||
}
|
}
|
||||||
float thumbWidth = ScalarThumbWidth(resolve);
|
float thumbWidth = ScalarThumbWidth(resolve);
|
||||||
float travel = MathF.Max(0f, Width - thumbWidth);
|
float travel = MathF.Max(0f, Width - thumbWidth);
|
||||||
|
|
@ -196,14 +212,14 @@ public sealed class UiScrollbar : UiElement
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draws the rightmost portion of a full-width tiled scalar image. UVs
|
/// Draws a clipped portion of a tiled range without restarting the texture
|
||||||
/// start at the same offset they would have in the uncut image, matching
|
/// phase at the clip edge. This mirrors retail meter child clipping.
|
||||||
/// retail meter clipping rather than restarting the wave at the clip edge.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void DrawTiledRightClipped(
|
private void DrawTiledClipped(
|
||||||
UiRenderContext ctx,
|
UiRenderContext ctx,
|
||||||
Func<uint, (uint tex, int w, int h)> resolve,
|
Func<uint, (uint tex, int w, int h)> resolve,
|
||||||
uint id,
|
uint id,
|
||||||
|
float rangeLeft,
|
||||||
float x,
|
float x,
|
||||||
float w,
|
float w,
|
||||||
float h)
|
float h)
|
||||||
|
|
@ -211,7 +227,19 @@ public sealed class UiScrollbar : UiElement
|
||||||
if (id == 0 || w <= 0f || h <= 0f) return;
|
if (id == 0 || w <= 0f || h <= 0f) return;
|
||||||
var (tex, tw, th) = resolve(id);
|
var (tex, tw, th) = resolve(id);
|
||||||
if (tex == 0 || tw == 0 || th == 0) return;
|
if (tex == 0 || tw == 0 || th == 0) return;
|
||||||
ctx.DrawSprite(tex, x, 0f, w, h, x / tw, 0f, (x + w) / tw, h / th, Vector4.One);
|
float u0 = (x - rangeLeft) / tw;
|
||||||
|
float u1 = u0 + w / tw;
|
||||||
|
ctx.DrawSprite(tex, x, 0f, w, h, u0, 0f, u1, h / th, Vector4.One);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (float left, float width) ScalarRangeRect()
|
||||||
|
{
|
||||||
|
float left = Math.Clamp(ScalarRangeLeft, 0f, Width);
|
||||||
|
float requestedWidth = float.IsPositiveInfinity(ScalarRangeWidth)
|
||||||
|
? Width - left
|
||||||
|
: MathF.Max(0f, ScalarRangeWidth);
|
||||||
|
float width = Math.Clamp(requestedWidth, 0f, Width - left);
|
||||||
|
return (left, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool OnEvent(in UiEvent e)
|
public override bool OnEvent(in UiEvent e)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ public sealed class CombatLayoutConformanceTests
|
||||||
Assert.Equal(0x060074CAu, power.TrackSprite);
|
Assert.Equal(0x060074CAu, power.TrackSprite);
|
||||||
Assert.Equal(0x06001923u, power.ThumbSprite);
|
Assert.Equal(0x06001923u, power.ThumbSprite);
|
||||||
Assert.Equal(0x06001200u, power.ScalarFillSprite);
|
Assert.Equal(0x06001200u, power.ScalarFillSprite);
|
||||||
|
Assert.Equal(0x0600715Eu, power.ScalarRangeSprite);
|
||||||
Assert.True(power.ScalarFillFromRight);
|
Assert.True(power.ScalarFillFromRight);
|
||||||
|
|
||||||
var repeat = Assert.IsType<UiButton>(
|
var repeat = Assert.IsType<UiButton>(
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ public sealed class CombatUiControllerTests
|
||||||
GameplaySettings gameplay = GameplaySettings.Default;
|
GameplaySettings gameplay = GameplaySettings.Default;
|
||||||
using var controller = CombatUiController.Bind(
|
using var controller = CombatUiController.Bind(
|
||||||
layout, combat, attacks, () => gameplay, value => gameplay = value,
|
layout, combat, attacks, () => gameplay, value => gameplay = value,
|
||||||
|
() => false,
|
||||||
Labels, visibility.Add)!;
|
Labels, visibility.Add)!;
|
||||||
|
|
||||||
controller.SyncVisibility();
|
controller.SyncVisibility();
|
||||||
|
|
@ -45,12 +46,16 @@ public sealed class CombatUiControllerTests
|
||||||
GameplaySettings gameplay = GameplaySettings.Default;
|
GameplaySettings gameplay = GameplaySettings.Default;
|
||||||
using var controller = CombatUiController.Bind(
|
using var controller = CombatUiController.Bind(
|
||||||
layout, combat, attacks, () => gameplay, value => gameplay = value,
|
layout, combat, attacks, () => gameplay, value => gameplay = value,
|
||||||
|
() => true,
|
||||||
Labels, _ => { })!;
|
Labels, _ => { })!;
|
||||||
combat.SetCombatMode(CombatMode.Melee);
|
combat.SetCombatMode(CombatMode.Melee);
|
||||||
|
|
||||||
high.OnEvent(new UiEvent(0, high, UiEventType.MouseDown, Data1: 2, Data2: 2));
|
high.OnEvent(new UiEvent(0, high, UiEventType.MouseDown, Data1: 2, Data2: 2));
|
||||||
now = 4.5d;
|
now = 4.5d;
|
||||||
Assert.Equal(0.5f, power.ScalarFill()!.Value, 3);
|
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));
|
high.OnEvent(new UiEvent(0, high, UiEventType.MouseUp, Data1: 2, Data2: 2));
|
||||||
|
|
||||||
var attack = Assert.Single(sent);
|
var attack = Assert.Single(sent);
|
||||||
|
|
@ -69,6 +74,7 @@ public sealed class CombatUiControllerTests
|
||||||
GameplaySettings gameplay = GameplaySettings.Default;
|
GameplaySettings gameplay = GameplaySettings.Default;
|
||||||
using var controller = CombatUiController.Bind(
|
using var controller = CombatUiController.Bind(
|
||||||
layout, combat, attacks, () => gameplay, value => gameplay = value,
|
layout, combat, attacks, () => gameplay, value => gameplay = value,
|
||||||
|
() => false,
|
||||||
Labels, _ => { })!;
|
Labels, _ => { })!;
|
||||||
var autoTarget = Assert.IsType<UiButton>(layout.FindElement(CombatUiController.AutoTargetId));
|
var autoTarget = Assert.IsType<UiButton>(layout.FindElement(CombatUiController.AutoTargetId));
|
||||||
|
|
||||||
|
|
@ -79,6 +85,28 @@ public sealed class CombatUiControllerTests
|
||||||
Assert.False(gameplay.AutoTarget);
|
Assert.False(gameplay.AutoTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AuthoredPowerLabels_KeepGrayTrackAtSides_AndUseRetailAlignment()
|
||||||
|
{
|
||||||
|
var combat = new CombatState();
|
||||||
|
using var attacks = CreateAttacks(combat, () => 0d, []);
|
||||||
|
var (layout, _, _, power, _, _, _) = BuildLayout();
|
||||||
|
GameplaySettings gameplay = GameplaySettings.Default;
|
||||||
|
using var controller = CombatUiController.Bind(
|
||||||
|
layout, combat, attacks, () => gameplay, value => gameplay = value,
|
||||||
|
() => true, Labels, _ => { })!;
|
||||||
|
|
||||||
|
var speed = Assert.IsType<UiText>(layout.FindElement(CombatUiController.SpeedLabelId));
|
||||||
|
var powerLabel = Assert.IsType<UiText>(layout.FindElement(CombatUiController.PowerLabelId));
|
||||||
|
Assert.False(speed.Centered);
|
||||||
|
Assert.False(speed.RightAligned);
|
||||||
|
Assert.False(powerLabel.Centered);
|
||||||
|
Assert.True(powerLabel.RightAligned);
|
||||||
|
Assert.Equal(speed.Left + speed.Width - power.Left, power.ScalarRangeLeft, 3);
|
||||||
|
Assert.Equal(powerLabel.Left - power.Left - power.ScalarRangeLeft,
|
||||||
|
power.ScalarRangeWidth, 3);
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly CombatUiLabels Labels = new(
|
private static readonly CombatUiLabels Labels = new(
|
||||||
"Speed", "Power", "Repeat Attacks", "Auto Target", "Keep in View",
|
"Speed", "Power", "Repeat Attacks", "Auto Target", "Keep in View",
|
||||||
"High", "Medium", "Low");
|
"High", "Medium", "Low");
|
||||||
|
|
@ -110,7 +138,16 @@ public sealed class CombatUiControllerTests
|
||||||
var root = new UiPanel { Width = 610, Height = 90 };
|
var root = new UiPanel { Width = 610, Height = 90 };
|
||||||
var basic = new UiPanel();
|
var basic = new UiPanel();
|
||||||
var advanced = new UiPanel();
|
var advanced = new UiPanel();
|
||||||
var power = new UiScrollbar { Width = 507, Height = 14, Horizontal = true };
|
var power = new UiScrollbar
|
||||||
|
{
|
||||||
|
Left = 13,
|
||||||
|
Top = 14,
|
||||||
|
Width = 507,
|
||||||
|
Height = 14,
|
||||||
|
Horizontal = true,
|
||||||
|
};
|
||||||
|
var speedLabel = new UiText { Left = 17, Top = 14, Width = 100, Height = 15 };
|
||||||
|
var powerLabel = new UiText { Left = 416, Top = 14, Width = 100, Height = 15 };
|
||||||
UiButton Button(uint id) => new(
|
UiButton Button(uint id) => new(
|
||||||
new ElementInfo { Id = id, Type = 1, Width = 72, Height = 19 }, NoTex)
|
new ElementInfo { Id = id, Type = 1, Width = 72, Height = 19 }, NoTex)
|
||||||
{
|
{
|
||||||
|
|
@ -137,6 +174,8 @@ public sealed class CombatUiControllerTests
|
||||||
[CombatUiController.BasicPanelId] = basic,
|
[CombatUiController.BasicPanelId] = basic,
|
||||||
[CombatUiController.AdvancedPanelId] = advanced,
|
[CombatUiController.AdvancedPanelId] = advanced,
|
||||||
[CombatUiController.PowerControlId] = power,
|
[CombatUiController.PowerControlId] = power,
|
||||||
|
[CombatUiController.SpeedLabelId] = speedLabel,
|
||||||
|
[CombatUiController.PowerLabelId] = powerLabel,
|
||||||
[CombatUiController.HighButtonId] = high,
|
[CombatUiController.HighButtonId] = high,
|
||||||
[CombatUiController.MediumButtonId] = medium,
|
[CombatUiController.MediumButtonId] = medium,
|
||||||
[CombatUiController.LowButtonId] = low,
|
[CombatUiController.LowButtonId] = low,
|
||||||
|
|
|
||||||
|
|
@ -466,13 +466,24 @@ public class DatWidgetFactoryTests
|
||||||
fill.StateMedia[""] = (0x06001200u, 1);
|
fill.StateMedia[""] = (0x06001200u, 1);
|
||||||
fill.States[UiStateInfo.DirectStateId] = new UiStateInfo
|
fill.States[UiStateInfo.DirectStateId] = new UiStateInfo
|
||||||
{ Id = UiStateInfo.DirectStateId, Image = new UiImageMedia(0x06001200u, 1) };
|
{ Id = UiStateInfo.DirectStateId, Image = new UiImageMedia(0x06001200u, 1) };
|
||||||
|
var range = new ElementInfo
|
||||||
|
{
|
||||||
|
Id = 0x100005EFu,
|
||||||
|
Type = 3,
|
||||||
|
X = 63,
|
||||||
|
Width = 407,
|
||||||
|
Height = 14,
|
||||||
|
};
|
||||||
|
range.StateMedia[""] = (0x0600715Eu, 1);
|
||||||
|
range.States[UiStateInfo.DirectStateId] = new UiStateInfo
|
||||||
|
{ Id = UiStateInfo.DirectStateId, Image = new UiImageMedia(0x0600715Eu, 1) };
|
||||||
var meter = new ElementInfo
|
var meter = new ElementInfo
|
||||||
{
|
{
|
||||||
Id = CombatUiController.PowerControlId + 1,
|
Id = CombatUiController.PowerControlId + 1,
|
||||||
Type = 7,
|
Type = 7,
|
||||||
Width = 507,
|
Width = 507,
|
||||||
Height = 14,
|
Height = 14,
|
||||||
Children = [fill],
|
Children = [fill, range],
|
||||||
};
|
};
|
||||||
var info = new ElementInfo
|
var info = new ElementInfo
|
||||||
{
|
{
|
||||||
|
|
@ -490,6 +501,7 @@ public class DatWidgetFactoryTests
|
||||||
Assert.Equal(0x06001919u, bar.TrackSprite);
|
Assert.Equal(0x06001919u, bar.TrackSprite);
|
||||||
Assert.Equal(0x06001923u, bar.ThumbSprite);
|
Assert.Equal(0x06001923u, bar.ThumbSprite);
|
||||||
Assert.Equal(0x06001200u, bar.ScalarFillSprite);
|
Assert.Equal(0x06001200u, bar.ScalarFillSprite);
|
||||||
|
Assert.Equal(0x0600715Eu, bar.ScalarRangeSprite);
|
||||||
Assert.True(bar.ScalarFillFromRight);
|
Assert.True(bar.ScalarFillFromRight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,20 @@ public sealed class UiPropertyBagTests
|
||||||
.Properties.Values[0x14u].UnsignedValue);
|
.Properties.Values[0x14u].UnsignedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ElementMerge_RetailTextDefaultTwo_IsLeftJustified()
|
||||||
|
{
|
||||||
|
var info = new ElementInfo();
|
||||||
|
info.States[UiStateInfo.DirectStateId] = State(
|
||||||
|
UiStateInfo.DirectStateId,
|
||||||
|
"",
|
||||||
|
(0x14u, Enum(2u)));
|
||||||
|
|
||||||
|
var merged = ElementReader.Merge(new ElementInfo(), info);
|
||||||
|
|
||||||
|
Assert.Equal(HJustify.Left, merged.HJustify);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ConvertProperty_PreservesNestedRetailValues()
|
public void ConvertProperty_PreservesNestedRetailValues()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -114,4 +114,17 @@ public class UiScrollbarTests
|
||||||
Assert.Equal(expectedX, x, 3);
|
Assert.Equal(expectedX, x, 3);
|
||||||
Assert.Equal(expectedWidth, width, 3);
|
Assert.Equal(expectedWidth, width, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0f, 403f, 0f)]
|
||||||
|
[InlineData(0.5f, 253.5f, 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);
|
||||||
|
Assert.Equal(expectedX, x, 3);
|
||||||
|
Assert.Equal(expectedWidth, width, 3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,8 @@ foreach (var id in ids)
|
||||||
|
|
||||||
RenderSurface? rs = null;
|
RenderSurface? rs = null;
|
||||||
uint lookupId = id;
|
uint lookupId = id;
|
||||||
if (dats.TryGet<Surface>(id, out var surface) && surface is not null)
|
if ((id & 0xFF000000u) == 0x08000000u
|
||||||
|
&& dats.TryGet<Surface>(id, out var surface) && surface is not null)
|
||||||
{
|
{
|
||||||
lookupId = (uint)surface.OrigTextureId;
|
lookupId = (uint)surface.OrigTextureId;
|
||||||
var color = surface.ColorValue is null
|
var color = surface.ColorValue is null
|
||||||
|
|
@ -54,7 +55,8 @@ foreach (var id in ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SurfaceTexture wrapper (0x05xxxxxx) → first inner RenderSurface (0x06xxxxxx).
|
// SurfaceTexture wrapper (0x05xxxxxx) → first inner RenderSurface (0x06xxxxxx).
|
||||||
if (dats.TryGet<SurfaceTexture>(lookupId, out var st) && st is not null && st.Textures.Count > 0)
|
if ((lookupId & 0xFF000000u) == 0x05000000u
|
||||||
|
&& dats.TryGet<SurfaceTexture>(lookupId, out var st) && st is not null && st.Textures.Count > 0)
|
||||||
{
|
{
|
||||||
uint rsid = (uint)st.Textures[0];
|
uint rsid = (uint)st.Textures[0];
|
||||||
Console.WriteLine($" SurfaceTexture wrapper, {st.Textures.Count} mip(s), first = 0x{rsid:X8}");
|
Console.WriteLine($" SurfaceTexture wrapper, {st.Textures.Count} mip(s), first = 0x{rsid:X8}");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue