fix(animation): preserve wire motion on every spawn path
Replace the door-specific static-animation seed with one retail description-then-enter-world initializer shared by normal and reactive spawns. This preserves authoritative Dead and On/Off states, prevents replacement corpses from returning to Ready, and pins the lifecycle with Core and App tests. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
9b97102c67
commit
8be933fc94
9 changed files with 335 additions and 64 deletions
|
|
@ -46,6 +46,39 @@ Copy this block when adding a new issue:
|
|||
|
||||
---
|
||||
|
||||
## #204 — Replacement corpses replay the death transition and stand back up
|
||||
|
||||
**Status:** DONE — 2026-07-12, user visually confirmed multiple corpses remain fallen
|
||||
**Severity:** HIGH
|
||||
**Component:** live entities / animation / CreateObject lifecycle
|
||||
|
||||
**Description:** A killed monster played its death animation correctly, but
|
||||
when ACE replaced the victim object with its lootable corpse, the body visibly
|
||||
returned to the standing pose and replayed the fall.
|
||||
|
||||
**Root cause:** Six-object live trace proved ACE sent every corpse CreateObject
|
||||
with the correct `NonCombat + Dead` motion. A follow-up trace after the first
|
||||
correction showed the replacement corpse nevertheless completed `Ready`: its
|
||||
persistent Dead rest pose is static, so it bypassed the normal multi-frame
|
||||
spawn branch and entered the broader #187 reactive branch. That branch had
|
||||
been generalized from doors by its gate but still hard-coded Door On/Off from
|
||||
`PhysicsState`; for a corpse those commands did not select a cycle, leaving the
|
||||
motion-table default Ready and producing the hard upright pop.
|
||||
|
||||
**Resolution:** Both live-spawn branches now use one data-driven initializer.
|
||||
It installs the motion-table default, applies the authoritative MovementData
|
||||
stance/command, and then calls `MotionTableManager.HandleEnterWorld()` exactly
|
||||
like retail's description-then-enter-world lifecycle. No corpse or door type
|
||||
switch remains. App tests pin Dead and Door On/Off wire-state resolution; a
|
||||
Core conformance test pins removal of the detached Ready→Dead link before the
|
||||
first rendered tick.
|
||||
|
||||
**Research:** `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`
|
||||
|
||||
**Acceptance:** Kill several monsters. Each falls once, is replaced by a
|
||||
lootable corpse in the same fallen pose, and never visibly stands between the
|
||||
victim deletion and corpse display.
|
||||
|
||||
## #203 — Unequipping armor freezes character and world animations — FIXED
|
||||
|
||||
**Status:** DONE — 2026-07-11 (this change)
|
||||
|
|
|
|||
|
|
@ -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 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. 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 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. 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.
|
||||
- **✓ 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.
|
||||
|
|
|
|||
|
|
@ -466,7 +466,12 @@ include dungeons.
|
|||
meter, silent AttackDone control status, target-frame Keep in View with
|
||||
retained manual orbit, persistent corpse motion protected from the AP-80
|
||||
velocity adaptation, and post-death nearest-target acquisition when Auto
|
||||
Target is enabled. See `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`.
|
||||
Target is enabled. Both multi-frame and static/reactive spawns now share
|
||||
retail's wire-driven description-before-enter-world lifecycle: the corpse's
|
||||
authoritative Dead state is retained and `HandleEnterWorld` strips its
|
||||
Ready→Dead transition before the first rendered tick. Corpse persistence was
|
||||
visually confirmed live on 2026-07-12. See
|
||||
`docs/research/2026-07-12-death-and-auto-target-pseudocode.md`.
|
||||
- **L.1b** — Command router + motion-state cleanup (prereq for L.1c).
|
||||
|
||||
**Freeze on landing:**
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ Sources: Sept 2013 EoR named retail client.
|
|||
- `CPlayerSystem::SelectNext` (`0x0055F9A0`)
|
||||
- `MovementManager::unpack_movement` (`0x00524440`)
|
||||
- `CPhysicsObj::MoveOrTeleport` (`0x00516330`)
|
||||
- `SmartBox::HandleCreateObject` (`0x00454C80`)
|
||||
- `ACCObjectMaint::CreateObject` (`0x00558870`)
|
||||
- `CPhysicsObj::set_description` (`0x00514F40`)
|
||||
- `CPhysicsObj::enter_world` (`0x00516170`)
|
||||
- `CPartArray::HandleEnterWorld` (`0x00517D70`)
|
||||
- `MotionTableManager::HandleEnterWorld` (`0x0051BDD0`)
|
||||
|
||||
## Animation authority
|
||||
|
||||
|
|
@ -27,6 +33,84 @@ states. An authoritative action or substate such as attack, hit reaction, or
|
|||
Dead always wins. This preserves the adaptation without allowing a late
|
||||
position delta to make a corpse stand again.
|
||||
|
||||
## CreateObject motion lifecycle
|
||||
|
||||
ACE creates a new corpse object after the victim's death animation finishes.
|
||||
Its CreateObject packet correctly carries `NonCombat + Dead`; this was verified
|
||||
in the 2026-07-12 live trace for six corpse GUIDs. The remaining stand-up was
|
||||
not a parser or server-authority bug. The decisive follow-up trace showed the
|
||||
replacement corpse completing `Ready` immediately after spawn. Dead is a
|
||||
static persistent rest pose for this setup, so the corpse entered acdream's
|
||||
"reactive" static-animation branch rather than the normal multi-frame branch.
|
||||
That branch's gate had been generalized from doors to every nonzero motion
|
||||
table, but its initializer was still door-specific: it inferred On/Off from
|
||||
`PhysicsState`, discarded the wire's Dead command, and left the corpse on the
|
||||
table's Ready default.
|
||||
|
||||
Retail performs these operations in this order:
|
||||
|
||||
```text
|
||||
SmartBox::HandleCreateObject(packet):
|
||||
object = ACCObjectMaint::CreateObject(...)
|
||||
|
||||
ACCObjectMaint::CreateObject(...):
|
||||
object.set_description(physicsDesc, applyMovement=true)
|
||||
|
||||
CPhysicsObj::set_description(...):
|
||||
SetMotionTableID(physicsDesc.motionTable)
|
||||
-> initialize the motion table's default Ready state
|
||||
unpack_movement(physicsDesc.movement)
|
||||
-> apply the wire's NonCombat + Dead state
|
||||
-> temporarily build [Ready-to-Dead transition link, Dead cycle]
|
||||
|
||||
SmartBox::HandleCreateObject(...), after CreateObject returns:
|
||||
object.enter_world(position)
|
||||
|
||||
CPhysicsObj::enter_world(...):
|
||||
partArray.HandleEnterWorld()
|
||||
|
||||
CPartArray::HandleEnterWorld():
|
||||
motionTableManager.HandleEnterWorld()
|
||||
|
||||
MotionTableManager::HandleEnterWorld():
|
||||
sequence.remove_all_link_animations()
|
||||
while pendingAnimations is not empty:
|
||||
AnimationDone(success=false)
|
||||
```
|
||||
|
||||
Therefore the first in-world corpse pose comes from the persistent Dead cycle,
|
||||
not the Ready-to-Dead transition that the already-deleted victim just played.
|
||||
This is a generic CreateObject lifecycle rule for every motion-table object;
|
||||
there is no corpse-name or corpse-type special case in the animation path.
|
||||
|
||||
acdream must apply this sequence identically in both of its render-registration
|
||||
paths:
|
||||
|
||||
```text
|
||||
initialize_spawn_motion(motionTable, wireMovementData):
|
||||
sequencer.initialize_state() // table default
|
||||
sequencer.set_cycle(wire.stance, wire.motion)
|
||||
sequencer.handle_enter_world() // strip transient links
|
||||
|
||||
normal multi-frame spawn:
|
||||
sequencer = initialize_spawn_motion(...)
|
||||
|
||||
static/reactive spawn (doors, corpses, gates, ...):
|
||||
sequencer = initialize_spawn_motion(...)
|
||||
```
|
||||
|
||||
Cross-checks:
|
||||
|
||||
- ACE `WorldObject_Networking.SerializePhysicsData` writes the corpse's
|
||||
`CurrentMotionState` into CreateObject MovementData.
|
||||
- ACE `Corpse.SetEphemeralValues` sets that state to `NonCombat + Dead`.
|
||||
- ACE `Door.SetEphemeralValues` likewise supplies `NonCombat + On/Off`; the
|
||||
client does not need to infer door motion from `PhysicsState`.
|
||||
- The existing acdream `MotionTableManager.HandleEnterWorld` port and its
|
||||
#174 conformance tests already implement the retail strip-and-drain
|
||||
semantics. The defect was that the static/reactive spawn path neither used
|
||||
the wire motion generically nor called that lifecycle step.
|
||||
|
||||
## Auto Target after the selected creature dies
|
||||
|
||||
```text
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue