acdream/docs/research/2026-07-26-slice-j5-3-combat-magic-intent.md
Erik 20df9d155d refactor(runtime): own combat and magic intent
Move attack build/repeat state, combat-mode policy, authoritative auto-target transitions, and spell-cast intent beneath RuntimeActionState. Keep App as the input, world-query, DAT-policy, transport, and presentation adapter while preserving retail request and busy ordering. Add direct/graphical parity, reset, failure, and instance-isolation coverage.

Co-authored-by: Codex <noreply@openai.com>
2026-07-26 11:56:40 +02:00

6.4 KiB

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:

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:

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:

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:

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:

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.