fix(ui): complete live targeted healing flow
Route ACE server commands through the existing chat path, bind the retail paperdoll hit mask instead of its obscured viewport, and prefer authoritative private health vitals. Record the user-confirmed live gate and pin the production DAT widget type in tests.
This commit is contained in:
parent
eb6229394a
commit
05f6222865
11 changed files with 181 additions and 24 deletions
|
|
@ -484,7 +484,7 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar.
|
|||
- **D.5 remaining — sub-phase ledger.** D.5.1 (toolbar + the `UiItemSlot`/`UiItemList`/`IconComposer` spine) ✅, D.5.2 (stateful icons) ✅, D.5.4 (client object/item data model) ✅, D.2b-B window manager (`abbd97b`) ✅, D.2b-B B-Grid (inventory sub-window mount + `UiItemList` grid) ✅, D.2b-B B-Controller (inventory population + burden meter + captions, `03fbf44`; **visually confirmed 2026-06-21** — two render bugs fixed at the gate `417b137`: backdrop wash-out [a #145 continuation — mounted sub-window slots must keep their own frame ZLevel, not inherit the base root's 1000] + captions [drive the host UiText directly]) ✅, D.2b-B B-Wire (`EncumbranceVal`/PropertyInt 5 wire delivery, AP-48/49 retired) ✅, inventory window finish Stage 1 (scroll/frame/vertical-resize/102-slot grid) ✅, empty-slot art ✅, container-switching + open/selected indicators + main-pack icon + per-bag capacity bar ✅, B-Drag (inventory drag-drop / item moving) ✅, **Sub-phase C (paperdoll): Slice 1 equip slots ✅ + Slice 2 3-D doll `UiViewport` (Type 0xD via the `IUiViewportRenderer` RTT seam) + Slots toggle ✅ (visually confirmed 2026-06-25, `8fa66c2` — pose/camera/heading all retail-verbatim)** ✅, **UI Studio** (`AcDream.App ui-studio`) ✅, **importer dat-fidelity (Fix A/B/C)** ✅, **Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`) ✅, **Wave 3.1 unified selection** (Core `SelectionState`; AP-58 retired) ✅, and **Wave 3.2 pure item-use/drop policy** (`ItemInteractionPolicy`; AP-99 retired) ✅ are shipped. Build order from here: **finish Wave 3 activation router/cursors, then D.5.3 selected-object mana + stack entry/slider and the separate favorite-spell bars; ISSUES #146/#147/#197.** Each ☐ below gets its own focused oracle/spec/plan/gate.
|
||||
- **✓ SHIPPED — UI Studio** (2026-06-26, branch `claude/hopeful-maxwell-214a12`, ~33693c6→HEAD). Standalone `AcDream.App ui-studio <datdir> [--layout 0xNNNN | --dump <slug>] [--screenshot <png>]` Silk tool that previews any panel through the **production renderer** (`RenderBootstrap.cs` + GameWindow untouched). Sources: 26-window retail dump (`docs/research/2026-06-25-retail-ui-layout-dump.json`) via `--dump`, or live dat import + fixtures via `--layout`. Interactive canvas (click-routing to `UiHost`, Interact/Inspect ImGui toggle), headless `--screenshot`. Architecture: `src/AcDream.App/Studio/` (`StudioWindow`, `PanelFbo`, `FixtureProvider`, `LayoutSource`) + `src/AcDream.App/Rendering/RenderBootstrap.cs`. ISSUES #156 (inventory all-black in studio) and #157 (GameWindow font resolver) filed.
|
||||
- **✓ SHIPPED — Importer dat-fidelity** (Fix A/B/C, 2026-06-26, same branch). **Boundary established: look = importer (font/justification/color from dat); state/behavior = runtime controller logic.** Fix A: justification property `0x14`/`0x15` → `UiText` Centered/RightAligned/VerticalJustify. Fix B: FontColor property `0x1B` → `UiText.DefaultColor`; character colors proved RUNTIME (no importer fix). Fix C: FontDid → per-element dat font via a resolver in `DatWidgetFactory` — **STUDIO path only**; GameWindow passes `null` (Issue #157). Fix 4 (UIState-group activation): INVESTIGATED — no importer fix exists; the dat has no Visible encoding; runtime `gm*UI` is correct.
|
||||
- **✓ SHIPPED — Wave 3.3 unified primary-click router.** `ItemInteractionController.OfferPrimaryClick` returns `NotActive` / `ConsumedSuccess` / `ConsumedRejected`; inventory, bags, main pack, paperdoll, toolbar, radar, and world clicks all offer target mode before local fallback. Rejected targets cannot drift selection or open/use the clicked object. Issue #197 and AP-107 retired; retained end-to-end matrix green.
|
||||
- **✓ SHIPPED — Wave 3.3 unified primary-click router.** `ItemInteractionController.OfferPrimaryClick` returns `NotActive` / `ConsumedSuccess` / `ConsumedRejected`; inventory, bags, main pack, paperdoll, toolbar, radar, and world clicks all offer target mode before local fallback. Rejected targets cannot drift selection or open/use the clicked object. Issue #197 and AP-107 retired; retained matrix and live ACE-provisioned healing gate user-confirmed. The 3-D doll routes through authored hit mask `0x100001D6` (resolved `UiButton` above viewport `0x100001D5`), and local health prefers authoritative private-vital state.
|
||||
- **✓ 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.
|
||||
|
|
|
|||
|
|
@ -363,11 +363,11 @@ consume the typed action seam when those surfaces land; until then the policy
|
|||
refuses unsafe wire sends rather than bypassing their gates.
|
||||
|
||||
**Acceptance (2026-07-11):** Non-debug Release live-world gate passed for
|
||||
ordinary item/container activation, equip/inventory synchronization, and grounded
|
||||
world drop, with no observed UI regression. A healing kit was not available in
|
||||
the test character's inventory, so the targeted-heal click was not exercised
|
||||
live; the compatible/invalid/self target matrix remains pinned by Core and App
|
||||
controller tests. Full suite green before launch: 4,575 passed, 5 skipped.
|
||||
ordinary item/container activation, equip/inventory synchronization, grounded
|
||||
world drop, and targeted healing. ACE command forwarding provisioned WCID 632
|
||||
with `/ci 632`; `/harmself` then proved the main-pack and paperdoll self-target
|
||||
paths plus authoritative health-bar drain/recovery. Full suite green at the final
|
||||
gate: 4,583 passed, 5 skipped.
|
||||
|
||||
### 3.3 Add one item primary-click chokepoint
|
||||
|
||||
|
|
@ -393,6 +393,14 @@ results. The consumed target is retained for retail's 500 ms double-click window
|
|||
so the second Click/DoubleClick pair cannot reactivate or equip the rejected
|
||||
object after target mode clears. Issue #197 and AP-107 are retired.
|
||||
|
||||
**Acceptance (2026-07-11):** User-confirmed the non-debug Release fallback
|
||||
matrix for inactive target mode across inventory, paperdoll, toolbar, radar,
|
||||
and world clicks, then the active targeted-heal flow through both the main pack
|
||||
and 3-D paperdoll. The production DAT resolves the authored doll hit mask
|
||||
`0x100001D6` as a `UiButton`; binding the viewport beneath it cannot receive a
|
||||
live click. The mask now owns self-target clicks and hides with the viewport in
|
||||
slot view. Full suite green at the final gate: 4,583 passed, 5 skipped.
|
||||
|
||||
### 3.4 Complete retail cursor precedence and catalog
|
||||
|
||||
- [ ] Port every reachable branch of `ClientUISystem::UpdateCursorState`, not only
|
||||
|
|
|
|||
|
|
@ -160,9 +160,9 @@ and preserve placement's action list separately from its boolean return (vendor
|
|||
sale and secure-trade attempts act while returning false).
|
||||
|
||||
The 2026-07-11 non-debug Release gate passed ordinary use/container activation,
|
||||
equip synchronization, and grounded world drop. No healing kit was available,
|
||||
so targeted healing was not live-tested; retain the automated compatible,
|
||||
invalid, and self-target coverage as the acceptance evidence for that branch.
|
||||
equip synchronization, grounded world drop, and targeted healing. ACE `/ci 632`
|
||||
provisioned a peerless kit and `/harmself` created a real health deficit; main-pack
|
||||
and paperdoll self-targets both healed successfully.
|
||||
|
||||
## 2026-07-11 primary-click ownership
|
||||
|
||||
|
|
@ -176,3 +176,32 @@ to expose callbacks without importing controllers. The router retains a consumed
|
|||
target through the toolkit's 500 ms double-click window; otherwise UiRoot's
|
||||
second Click plus DoubleClick could run ordinary activation after the first click
|
||||
cleared target mode.
|
||||
|
||||
The 2026-07-11 non-debug Release fallback gate passed for inventory,
|
||||
paperdoll, toolbar, radar, and world primary clicks with target mode inactive,
|
||||
and the active healing-kit target flow passed through the main pack and 3-D doll.
|
||||
The production DAT resolves `0x100001D6`, the full-height paperdoll hit/drag mask,
|
||||
as a `UiButton` over `UiViewport 0x100001D5`. Bind target behavior to the mask,
|
||||
not only the obscured viewport, and hide/show both together with the Slots toggle.
|
||||
|
||||
For local vitals, `PrivateUpdateVital`/`PrivateUpdateVitalCurrent` in
|
||||
`LocalPlayerState` are authoritative. `VitalsVM.HealthPercent` must prefer that
|
||||
private health snapshot over the world-facing `CombatState` percentage, using
|
||||
the latter only before private state arrives. ACE `/harmself` exposed the stale
|
||||
owner because it sends private vital deltas without changing that combat cache.
|
||||
|
||||
## ACE command forwarding
|
||||
|
||||
`ChatCommandRouter` is the single submit path for retained and developer chat.
|
||||
Known client/chat verbs resolve locally; unknown letter verbs are server
|
||||
commands. They are always published as `ChatChannelKind.Say`, regardless of the
|
||||
selected channel, because commands must ride `GameAction Talk (0x0015)`.
|
||||
`ChatInputParser` preserves `@verb` and canonicalizes `/verb` to `@verb` on the
|
||||
wire: ACE's `GameActionTalk` checks only the at-sign form before invoking its
|
||||
`CommandManager`. Do not add a parallel admin-command transport.
|
||||
|
||||
For live test-item provisioning, ACE `@ci` accepts a weenie class id/classname
|
||||
plus optional stack size, palette, and shade. Healing kit WCIDs are 628 crude,
|
||||
629 plain, 630 good, 631 excellent, and 632 peerless. The `+Acdream` developer
|
||||
character can therefore create the best kit with either `/ci 632` or `@ci 632`;
|
||||
both forms must produce canonical `@ci 632` Talk text.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ namespace AcDream.App.UI.Layout;
|
|||
/// </summary>
|
||||
public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelController
|
||||
{
|
||||
public const uint DollViewportId = 0x100001D5u;
|
||||
public const uint DollDragMaskId = 0x100001D6u;
|
||||
|
||||
// ── Slots-toggle public surface ───────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// The 9 armor-slot element-ids whose Visible state the Slots button (0x100005BE) toggles.
|
||||
|
|
@ -84,6 +87,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
private readonly PaperdollViewState _viewState = new();
|
||||
private readonly List<UiItemList> _armorSlots = new();
|
||||
private UiElement? _dollViewport; // UiViewport wired in Slice 3; UiElement? keeps this task independent
|
||||
private UiElement? _dollDragMask;
|
||||
private bool _disposed;
|
||||
|
||||
private PaperdollController(
|
||||
|
|
@ -137,13 +141,30 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
foreach (var id in ArmorSlotElementIds)
|
||||
if (layout.FindElement(id) is UiItemList armor) _armorSlots.Add(armor);
|
||||
|
||||
_dollViewport = layout.FindElement(0x100001D5u); // doll viewport (may be null until Slice 3)
|
||||
_dollViewport = layout.FindElement(DollViewportId); // doll viewport (may be null until Slice 3)
|
||||
// The doll IS the player: clicking it during target-use self-targets
|
||||
// like the equip cells do. (The CURSOR over it stays the pending
|
||||
// four-arrows — retail resolves target cursors off the SmartBox world
|
||||
// object only; UI hover is always pending. 2026-07-03 visual gate.)
|
||||
Action offerSelf = () => { _itemInteraction?.OfferSelfPrimaryClick(); };
|
||||
if (_dollViewport is UiViewport doll)
|
||||
doll.Clicked = () => { _itemInteraction?.OfferSelfPrimaryClick(); };
|
||||
doll.Clicked = offerSelf;
|
||||
|
||||
// Retail's authored click/drag mask sits above the viewport and owns
|
||||
// doll hit-testing (gmPaperDollUI::ListenToElementMessage). Binding only
|
||||
// the viewport cannot win a live hit against this full-height overlay.
|
||||
switch (layout.FindElement(DollDragMaskId))
|
||||
{
|
||||
case UiButton dragMaskButton:
|
||||
_dollDragMask = dragMaskButton;
|
||||
dragMaskButton.OnClick = offerSelf;
|
||||
break;
|
||||
case UiDatElement dragMaskElement:
|
||||
_dollDragMask = dragMaskElement;
|
||||
dragMaskElement.ClickThrough = false;
|
||||
dragMaskElement.OnClick = offerSelf;
|
||||
break;
|
||||
}
|
||||
|
||||
var slotsBtnEl = layout.FindElement(0x100005BEu);
|
||||
if (slotsBtnEl is UiButton slotsBtn)
|
||||
|
|
@ -266,6 +287,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
private void ApplyView()
|
||||
{
|
||||
if (_dollViewport is not null) _dollViewport.Visible = _viewState.DollVisible;
|
||||
if (_dollDragMask is not null) _dollDragMask.Visible = _viewState.DollVisible;
|
||||
foreach (var a in _armorSlots) a.Visible = _viewState.ArmorSlotsVisible;
|
||||
}
|
||||
|
||||
|
|
@ -279,5 +301,16 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
_objects.ObjectRemoved -= OnObjectChanged;
|
||||
_objects.ObjectUpdated -= OnObjectChanged;
|
||||
_selection.Changed -= OnSelectionChanged;
|
||||
if (_dollViewport is UiViewport doll)
|
||||
doll.Clicked = null;
|
||||
switch (_dollDragMask)
|
||||
{
|
||||
case UiButton button:
|
||||
button.OnClick = null;
|
||||
break;
|
||||
case UiDatElement element:
|
||||
element.OnClick = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,6 +145,9 @@ public static class WeenieErrorMessages
|
|||
[0x0053] = "You fail to affect _ because you are not the same sort of player killer as them!",
|
||||
[0x0054] = "You fail to affect _ because you are acting across a house boundary!",
|
||||
|
||||
// Healing
|
||||
[0x04FF] = "_ is already at full health!", // _IsAtFullHealth
|
||||
|
||||
// Inventory / etiquette
|
||||
[0x001E] = "_ is too busy to accept gifts right now.",
|
||||
[0x002B] = "_ cannot carry anymore.",
|
||||
|
|
|
|||
|
|
@ -4,17 +4,18 @@ using AcDream.Core.Player;
|
|||
namespace AcDream.UI.Abstractions.Panels.Vitals;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for the vitals HUD panel. Reads live health percentage for the
|
||||
/// local player from <see cref="CombatState"/> (which is fed by the server's
|
||||
/// <c>UpdateHealth (0x01C0)</c> GameEvent).
|
||||
/// ViewModel for the vitals HUD panel. Reads the local player's authoritative
|
||||
/// private vital snapshot, falling back to world-combat health before that
|
||||
/// snapshot is available.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Sources:</b>
|
||||
/// </para>
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item>HP — <see cref="CombatState.GetHealthPercent"/> (percent-only,
|
||||
/// updated by <c>UpdateHealth (0x01C0)</c>).</item>
|
||||
/// <item>HP — <see cref="LocalPlayerState.HealthPercent"/> from
|
||||
/// <c>PrivateUpdateVital(Current)</c>, with
|
||||
/// <see cref="CombatState.GetHealthPercent"/> as fallback.</item>
|
||||
/// <item>Stamina / Mana — <see cref="LocalPlayerState"/>'s
|
||||
/// <c>StaminaPercent</c> / <c>ManaPercent</c>, populated from the
|
||||
/// <c>CreatureProfile</c> embedded in <c>PlayerDescription
|
||||
|
|
@ -60,11 +61,12 @@ public sealed class VitalsVM
|
|||
public void SetLocalPlayerGuid(uint guid) => _localPlayerGuid = guid;
|
||||
|
||||
/// <summary>
|
||||
/// Current health percent (0..1) for the local player. Returns 1.0
|
||||
/// before login or if the server has never sent an UpdateHealth for
|
||||
/// this GUID.
|
||||
/// Current health percent (0..1) for the local player. The private vital
|
||||
/// is authoritative for self; world-combat percentage is the pre-login
|
||||
/// and legacy fallback.
|
||||
/// </summary>
|
||||
public float HealthPercent => _combat.GetHealthPercent(_localPlayerGuid);
|
||||
public float HealthPercent
|
||||
=> _local?.HealthPercent ?? _combat.GetHealthPercent(_localPlayerGuid);
|
||||
|
||||
/// <summary>
|
||||
/// Stamina percent (0..1), or <c>null</c> when no
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
|
@ -50,7 +51,11 @@ public sealed class RetailFixtureConformanceTests
|
|||
Assert.NotNull(Find(root, InventoryController.ContentsGridId));
|
||||
Assert.NotNull(Find(root, InventoryController.ContainerListId));
|
||||
Assert.NotNull(Find(root, InventoryController.BurdenMeterId));
|
||||
Assert.NotNull(Find(root, 0x100001D5u)); // mounted paperdoll viewport
|
||||
Assert.NotNull(Find(root, PaperdollController.DollViewportId));
|
||||
Assert.NotNull(Find(root, PaperdollController.DollDragMaskId));
|
||||
|
||||
var layout = FixtureLoader.LoadInventory();
|
||||
Assert.IsType<UiButton>(layout.FindElement(PaperdollController.DollDragMaskId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -73,6 +73,25 @@ public sealed class RetailUiInteractionFlowTests
|
|||
Width = 56,
|
||||
Height = 20,
|
||||
};
|
||||
var dollViewport = new UiViewport
|
||||
{
|
||||
DatElementId = PaperdollController.DollViewportId,
|
||||
Left = 270,
|
||||
Top = 80,
|
||||
Width = 70,
|
||||
Height = 90,
|
||||
};
|
||||
var dollDragMask = new UiButton(
|
||||
new ElementInfo { Id = PaperdollController.DollDragMaskId, Type = 1 },
|
||||
static _ => (0u, 0, 0))
|
||||
{
|
||||
DatElementId = PaperdollController.DollDragMaskId,
|
||||
Left = 270,
|
||||
Top = 80,
|
||||
Width = 70,
|
||||
Height = 90,
|
||||
ZOrder = 1,
|
||||
};
|
||||
var meter = new UiMeter
|
||||
{
|
||||
DatElementId = InventoryController.BurdenMeterId,
|
||||
|
|
@ -107,6 +126,8 @@ public sealed class RetailUiInteractionFlowTests
|
|||
[InventoryController.ContentsScrollbarId] = scrollbar,
|
||||
[ChestArmorSlotId] = chestArmor,
|
||||
[SlotsButtonId] = slotsButton,
|
||||
[PaperdollController.DollViewportId] = dollViewport,
|
||||
[PaperdollController.DollDragMaskId] = dollDragMask,
|
||||
};
|
||||
|
||||
layoutRoot.AddChild(grid);
|
||||
|
|
@ -114,6 +135,8 @@ public sealed class RetailUiInteractionFlowTests
|
|||
layoutRoot.AddChild(mainPack);
|
||||
layoutRoot.AddChild(chestArmor);
|
||||
layoutRoot.AddChild(slotsButton);
|
||||
layoutRoot.AddChild(dollViewport);
|
||||
layoutRoot.AddChild(dollDragMask);
|
||||
layoutRoot.AddChild(meter);
|
||||
layoutRoot.AddChild(titleText);
|
||||
layoutRoot.AddChild(burdenText);
|
||||
|
|
@ -260,6 +283,23 @@ public sealed class RetailUiInteractionFlowTests
|
|||
Assert.False(interaction.IsTargetModeActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InventoryTargetUse_thenPaperdollBodyClick_selfTargetsThroughUiRoot()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.SeedHealthKit();
|
||||
var interaction = h.BindInventoryInteraction();
|
||||
h.BindPaperdoll(interaction);
|
||||
var probe = h.Probe();
|
||||
|
||||
Assert.True(probe.DoubleClickItem(HealthKit, ItemDragSource.Inventory));
|
||||
Assert.True(interaction.IsTargetModeActive);
|
||||
Assert.True(probe.ClickElement(PaperdollController.DollDragMaskId));
|
||||
|
||||
Assert.Equal(new[] { (HealthKit, Player) }, h.UseWithTarget);
|
||||
Assert.False(interaction.IsTargetModeActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InventoryTargetUse_invalidInventoryItem_isConsumedWithoutSelectionFallback()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -136,4 +136,12 @@ public sealed class WeenieErrorMessagesTests
|
|||
"You fail to affect Drudge because they cannot be harmed!",
|
||||
WeenieErrorMessages.Format(0x004F, "Drudge"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Format_HealingTargetAlreadyFull_SubstitutesParam()
|
||||
{
|
||||
Assert.Equal(
|
||||
"+Acdream is already at full health!",
|
||||
WeenieErrorMessages.Format(0x04FF, "+Acdream"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,14 +68,16 @@ public class ChatCommandRouterTests
|
|||
Assert.DoesNotContain(log.Snapshot(), e => e.Text.Contains("Unknown command"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ServerCommandWithArgs_PublishesAtFormAsSay_EvenOnChannelDefault()
|
||||
[Theory]
|
||||
[InlineData("/ci 629 5")]
|
||||
[InlineData("@ci 629 5")]
|
||||
public void ServerCommandWithArgs_PublishesCanonicalAtFormAsSay_EvenOnChannelDefault(string command)
|
||||
{
|
||||
// Commands resolve before channel routing (retail behavior):
|
||||
// even with Fellowship as the active input channel, a command
|
||||
// goes out as Say/Talk — the only wire action ACE parses @ on.
|
||||
var (vm, _, bus) = Fixture();
|
||||
var outcome = ChatCommandRouter.Submit("/ci 629 5", vm, bus, ChatChannelKind.Fellowship);
|
||||
var outcome = ChatCommandRouter.Submit(command, vm, bus, ChatChannelKind.Fellowship);
|
||||
Assert.Equal(SubmitOutcome.Sent, outcome);
|
||||
Assert.Equal(ChatChannelKind.Say, bus.Last!.Channel);
|
||||
Assert.Equal("@ci 629 5", bus.Last.Text);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,33 @@ public sealed class VitalsVMTests
|
|||
Assert.Equal(0.42f, vm.HealthPercent, precision: 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HealthPercent_LocalPrivateVitalOverridesStaleCombatPercent()
|
||||
{
|
||||
var combat = new CombatState();
|
||||
uint guid = 0x5000_0042u;
|
||||
combat.OnUpdateHealth(guid, 1f);
|
||||
|
||||
var local = new LocalPlayerState();
|
||||
local.OnVitalUpdate(vitalId: 2u, ranks: 50u, start: 50u, xp: 0u, current: 1u);
|
||||
var vm = new VitalsVM(combat, local);
|
||||
vm.SetLocalPlayerGuid(guid);
|
||||
|
||||
Assert.Equal(0.01f, vm.HealthPercent, precision: 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HealthPercent_ReadsCurrentOnlyPrivateVitalDeltaWithoutStaleCache()
|
||||
{
|
||||
var local = new LocalPlayerState();
|
||||
local.OnVitalUpdate(vitalId: 2u, ranks: 50u, start: 50u, xp: 0u, current: 100u);
|
||||
var vm = new VitalsVM(new CombatState(), local);
|
||||
|
||||
local.OnVitalCurrent(vitalId: 2u, current: 25u);
|
||||
|
||||
Assert.Equal(0.25f, vm.HealthPercent, precision: 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HealthPercent_ReturnsOne_WhenGuidUnknown()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue