# Slice J5.3 — retail combat and magic intent ownership ## Scope This note pins the retail ordering that must remain unchanged while the presentation-independent combat and casting state moves from `AcDream.App` to `AcDream.Runtime`. The move changes ownership and command types only. ACE remains authoritative for combat mode settlement, attacks, casts, damage, motion, component consumption, effects, and completion. Primary oracle: `docs/research/named-retail/acclient_2013_pseudo_c.txt`. Cross-checks: - the ACE/ACViewer request-layout cross-checks pinned in `docs/research/2026-07-11-retail-combat-bar-pseudocode.md` and `docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md`; - the ACE corpse/death-state cross-check pinned in `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`; - existing conformance ports in `src/AcDream.Core/Combat/CombatModel.cs` and `src/AcDream.Core/Spells/RetailSpellTargetPolicy.cs` ## Named retail mechanisms ### `ClientCombatSystem::Begin @ 0x0056A460` Pseudocode: ```text combatMode = NonCombat pendingCombatMode = Undefined clear jump and power-bar timing clear request, server-pending, queued-request, attack, and repeat state requestedPower = 0 requestedHeight = Medium desiredUiPower = 0.5 trackingTarget = false advancedCombat = false ``` This is the session-reset state of the Runtime combat-intent owner. ### `ClientCombatSystem::StartAttackRequest @ 0x0056C040` Pseudocode: ```text target = GetAttackTarget() if advanced combat or (target exists and ObjectIsAttackable(target)): attackRequestInProgress = true requestedAttackPower = 1.0 FinishJump() MaybeStopCompletely() currentBuildIsAutomatic = false AttemptStartBuildingAttack() else: display the retail invalid-target notice ``` The request flags commit before the shared movement owner is asked to finish jump/stop. The host operation preserves that exact boundary and may publish the resulting movement packet before a later attack send. ### `StartPowerBarBuild @ 0x0056ADB0`, `EndAttackRequest @ 0x0056C0E0`, and `UseTime @ 0x0056C1F0` Pseudocode: ```text StartPowerBarBuild: buildInProgress = true buildStartTime = current game time publish level 0 latestPowerBarLevel = 0 EndAttackRequest: if no request: return requestInProgress = false currentLevel = GetPowerBarLevel() requestedPower = explicit power, advanced current level, or min(desiredUiPower, currentLevel) if server response pending: remember one queued release else if desired power reached or repeating: ExecuteAttack() UseTime: while building, publish the current level if the player is not ready, cancel/reset according to repeat state after release, execute once the captured requested level is reached ``` ### `AbortAutomaticAttack @ 0x0056AE90` If a request, attack, server response, or repeat is active, retail sends `Event_CancelAttack`, clears repeat, and hides the combat power build. `ACCmdInterp::HandleNewForwardMovement @ 0x0058B1F0` invokes this before the new forward movement enters the interpreter. Runtime therefore receives a typed movement-abort command at that existing ordering boundary. ### `HandleAttackDoneEvent @ 0x0056C500` Retail releases the attack busy reference (when held), clears server-pending state, aborts repeat on authoritative error, starts the next automatic build or attack only when repeat remains enabled, and finally drains the one queued release. Runtime must never predict completion. ### `GetDefaultCombatMode @ 0x0056B310` and `ToggleCombatMode @ 0x0056C8C0` From non-combat, retail scans equipped contents in inventory-placement order: missile equipment selects Missile, a caster in Held selects Magic, ordinary melee/two-handed equipment selects Melee, and an incompatible Held item rejects the transition with its item-name notice. With no weapon it selects unarmed Melee. From any combat mode, Toggle selects NonCombat without running the default scan. The exact decision remains the existing Core port `CombatInputPlanner.GetDefaultCombatModeDecision`; Runtime owns the command state and App supplies ordered equipment plus transport/presentation sinks. ### `RecvNotice_SelectionChanged @ 0x0056BD80` and `AutoTarget @ 0x0056BC80` When selection becomes zero in Melee/Missile mode and Auto Target is enabled, retail enters its next-target selection path. An authoritative Dead motion makes the current target unavailable and reaches the same selection-change path. Runtime owns the identity transition; App supplies the world query that selects the closest retail-compatible hostile. ### `ClientMagicSystem::CastSpell @ 0x00568040` Pseudocode: ```text resolve the exact known spell metadata if components are required: resolve the appropriate formula reject when a required component is unavailable resolve self, untargeted, or selected target validate the target against the spell target mask FreeHandsAndCastSpell(spellId, targetId) ``` ### `ClientMagicSystem::FreeHandsAndCastSpell @ 0x00566EF0` Pseudocode: ```text MaybeStopCompletely() if targetId == 0: Event_CastUntargetedSpell(spellId) else: Event_CastTargetedSpell(targetId, spellId) IncrementBusyCount() ``` The cast owner therefore stops first, emits exactly one packet, and increments the shared busy reference only after a successful send. It does not locally turn the avatar: retail sends after `MaybeStopCompletely`, and the authoritative server turn/cast motion supplies the visible facing. Send failure clears the remembered request and cannot increment busy. `ClientMagicSystem::OnEndCharacterSession @ 0x00566F50` clears selected and targeting spell state, which maps to Runtime session reset. ## Ownership result Runtime owns: - attack build/request/repeat/queued-release state and time; - typed attack commands and movement-abort intent; - combat-mode command policy; - authoritative target-death/auto-target identity transitions; - cast validation intent and last requested spell/target; - reset, teardown, revisions, and presentation-independent snapshots. App retains: - input-key and retained-widget mapping; - ordered equipped-object and closest-hostile world queries; - local movement preparation and outbound transport; - target/spell compatibility data that depends on the live object table; - DAT spell catalog/formulas/components/icons; - messages, bars, animations, audio, particles, and camera presentation. No new retail divergence is introduced by this ownership move. ## Implementation and gates Production commit: `20df9d155db50706a42420d60a9b15861cf4bfe7`. `RuntimeActionState` constructs and owns the exact `RuntimeCombatAttackState`, `RuntimeCombatTargetState`, `RuntimeCombatModeState`, and `RuntimeSpellCastState`. Retained graphical bars and input adapters borrow those owners. Direct Runtime combat and magic commands invoke the same operations and produce the same normalized snapshots without constructing UI. App retains the named presentation/content/transport responsibilities above. Release build passes with zero warnings and errors. Focused and complete gates pass 223 Runtime tests, 3,754 App tests / 3 skips, and 8,566 complete-solution tests / 5 skips. The exact-binary connected lifecycle/reconnect report is `logs/connected-world-gate-20260726-115752/report.json`: two sessions, 7/7 checkpoints, two graceful code-zero exits, and zero failures.