fix(combat): charge power bar from speed
Read the authored meter direction instead of inferring it from the combat element id, so the bright charge texture grows left-to-right from Speed. Keep the user-approved dark-red middle baseline visible independently and record its exact retail Recklessness visibility edge. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
256c1930bd
commit
707add8539
13 changed files with 50 additions and 49 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
CombatAttackController attacks,
|
||||
Func<GameplaySettings> gameplay,
|
||||
Action<GameplaySettings> setGameplay,
|
||||
Func<bool> recklessnessTrained,
|
||||
CombatUiLabels labels,
|
||||
Action<bool> 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<GameplaySettings> gameplay,
|
||||
Action<GameplaySettings> setGameplay,
|
||||
Func<bool> recklessnessTrained,
|
||||
CombatUiLabels labels,
|
||||
Action<bool> 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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@ public sealed record CombatRuntimeBindings(
|
|||
CombatState State,
|
||||
CombatAttackController Attacks,
|
||||
Func<GameplaySettings> Gameplay,
|
||||
Action<GameplaySettings> SetGameplay,
|
||||
Func<bool> RecklessnessTrained);
|
||||
Action<GameplaySettings> 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 =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -42,11 +42,10 @@ public sealed class UiScrollbar : UiElement
|
|||
public Func<float?> ScalarFill { get; set; } = () => null;
|
||||
public uint ScalarFillSprite { get; set; }
|
||||
/// <summary>
|
||||
/// Optional authored texture beneath the live scalar fill. gmCombatUI uses
|
||||
/// element <c>0x100005EF</c> for the trained-Recklessness range.
|
||||
/// Optional authored texture beneath the live scalar fill. gmCombatUI's
|
||||
/// dark-red interior media comes from element <c>0x100005EF</c>.
|
||||
/// </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>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<UiButton>(
|
||||
layout.FindElement(CombatUiController.RepeatAttacksId));
|
||||
|
|
|
|||
|
|
@ -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<UiButton>(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<UiText>(layout.FindElement(CombatUiController.SpeedLabelId));
|
||||
var powerLabel = Assert.IsType<UiText>(layout.FindElement(CombatUiController.PowerLabelId));
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue