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:
Erik 2026-07-12 21:25:24 +02:00
parent 256c1930bd
commit 707add8539
13 changed files with 50 additions and 49 deletions

View file

@ -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)

View file

@ -497,7 +497,7 @@ behavior. Estimated 1726 days focused work, 35 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.

View file

@ -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

View file

@ -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,

View file

@ -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"),

View file

@ -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;

View file

@ -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;
}

View file

@ -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 =>
{

View file

@ -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)

View file

@ -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));

View file

@ -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));

View file

@ -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)

View file

@ -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);
}