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>
189 lines
6.4 KiB
Markdown
189 lines
6.4 KiB
Markdown
# 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.
|