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>
This commit is contained in:
Erik 2026-07-26 11:56:40 +02:00
parent 81b31857c6
commit 20df9d155d
49 changed files with 1949 additions and 634 deletions

View file

@ -1,6 +1,7 @@
using AcDream.Core.Combat;
using AcDream.Core.Items;
using AcDream.Core.Selection;
using AcDream.Core.Spells;
namespace AcDream.Runtime.Gameplay;
@ -8,6 +9,9 @@ public readonly record struct RuntimeActionOwnershipSnapshot(
bool IsDisposed,
bool InternalSubscriptionsAttached,
bool InteractionTransactionsDisposed,
bool CombatAttackDisposed,
bool CombatTargetDisposed,
bool SpellCastReset,
uint SelectedObjectId,
uint PreviousObjectId,
uint PreviousValidObjectId,
@ -23,6 +27,9 @@ public readonly record struct RuntimeActionOwnershipSnapshot(
IsDisposed
&& !InternalSubscriptionsAttached
&& InteractionTransactionsDisposed
&& CombatAttackDisposed
&& CombatTargetDisposed
&& SpellCastReset
&& SelectedObjectId == 0u
&& PreviousObjectId == 0u
&& PreviousValidObjectId == 0u
@ -33,9 +40,9 @@ public readonly record struct RuntimeActionOwnershipSnapshot(
}
/// <summary>
/// Canonical presentation-independent owner for selection, combat
/// notifications/mode, and temporary interaction target mode. Graphical and
/// no-window hosts borrow these exact instances.
/// Canonical presentation-independent owner for selection, interaction
/// transactions, combat mode/attack/target intent, and spell-cast intent.
/// Graphical and no-window hosts borrow these exact instances.
/// </summary>
public sealed class RuntimeActionState : IDisposable
{
@ -44,21 +51,52 @@ public sealed class RuntimeActionState : IDisposable
private long _selectionRevision;
private long _combatRevision;
private long _interactionRevision;
private long _combatIntentRevision;
private long _magicIntentRevision;
public RuntimeActionState(InventoryTransactionState inventoryTransactions)
public RuntimeActionState(
InventoryTransactionState inventoryTransactions,
Spellbook spellbook,
IRuntimeCombatAttackOperations combatAttackOperations,
IRuntimeCombatTargetOperations combatTargetOperations,
IRuntimeCombatModeOperations combatModeOperations,
IRuntimeSpellCastOperations spellCastOperations,
Func<double>? now = null)
{
ArgumentNullException.ThrowIfNull(inventoryTransactions);
ArgumentNullException.ThrowIfNull(spellbook);
ArgumentNullException.ThrowIfNull(combatAttackOperations);
ArgumentNullException.ThrowIfNull(combatTargetOperations);
ArgumentNullException.ThrowIfNull(combatModeOperations);
ArgumentNullException.ThrowIfNull(spellCastOperations);
Selection = new SelectionState();
Combat = new CombatState();
Interaction = new InteractionState();
Transactions = new RuntimeInteractionTransactionState(
inventoryTransactions);
CombatAttack = new RuntimeCombatAttackState(
Combat,
combatAttackOperations,
now);
CombatTarget = new RuntimeCombatTargetState(
Combat,
Selection,
combatTargetOperations);
CombatMode = new RuntimeCombatModeState(
Combat,
combatModeOperations);
SpellCast = new RuntimeSpellCastState(
spellbook,
Selection,
spellCastOperations);
View = new ActionView(this);
Selection.Changed += OnSelectionChanged;
Combat.CombatModeChanged += OnCombatModeChanged;
Combat.HealthChanged += OnHealthChanged;
Interaction.Changed += OnInteractionChanged;
CombatAttack.StateChanged += OnCombatAttackChanged;
SpellCast.StateChanged += OnSpellCastChanged;
_internalSubscriptionsAttached = true;
}
@ -66,6 +104,10 @@ public sealed class RuntimeActionState : IDisposable
public CombatState Combat { get; }
public InteractionState Interaction { get; }
public RuntimeInteractionTransactionState Transactions { get; }
public RuntimeCombatAttackState CombatAttack { get; }
public RuntimeCombatTargetState CombatTarget { get; }
public RuntimeCombatModeState CombatMode { get; }
public RuntimeSpellCastState SpellCast { get; }
public IRuntimeActionView View { get; }
public bool IsDisposed => _disposed;
@ -73,6 +115,10 @@ public sealed class RuntimeActionState : IDisposable
_disposed,
_internalSubscriptionsAttached,
Transactions.IsDisposed,
CombatAttack.IsDisposed,
CombatTarget.IsDisposed,
SpellCast.LastRequestedSpellId is null
&& SpellCast.LastRequestedTargetId is null,
Selection.SelectedObjectId ?? 0u,
Selection.PreviousObjectId ?? 0u,
Selection.PreviousValidObjectId ?? 0u,
@ -95,6 +141,8 @@ public sealed class RuntimeActionState : IDisposable
List<Exception>? failures = null;
Try(Transactions.ResetSession, ref failures);
Try(Interaction.ResetSession, ref failures);
Try(SpellCast.Reset, ref failures);
Try(CombatAttack.ResetSession, ref failures);
Try(() => Selection.Reset(), ref failures);
Try(Combat.Clear, ref failures);
if (failures is not null)
@ -115,6 +163,8 @@ public sealed class RuntimeActionState : IDisposable
{
Try(Transactions.Dispose, ref failures);
Try(Interaction.ResetSession, ref failures);
Try(SpellCast.Reset, ref failures);
Try(CombatAttack.ResetSession, ref failures);
Try(() => Selection.Reset(), ref failures);
Try(Combat.Clear, ref failures);
}
@ -124,6 +174,10 @@ public sealed class RuntimeActionState : IDisposable
Combat.CombatModeChanged -= OnCombatModeChanged;
Combat.HealthChanged -= OnHealthChanged;
Interaction.Changed -= OnInteractionChanged;
CombatAttack.StateChanged -= OnCombatAttackChanged;
SpellCast.StateChanged -= OnSpellCastChanged;
Try(CombatAttack.Dispose, ref failures);
Try(CombatTarget.Dispose, ref failures);
_internalSubscriptionsAttached = false;
_disposed = true;
}
@ -148,6 +202,12 @@ public sealed class RuntimeActionState : IDisposable
private void OnInteractionChanged(InteractionModeTransition _) =>
Interlocked.Increment(ref _interactionRevision);
private void OnCombatAttackChanged() =>
Interlocked.Increment(ref _combatIntentRevision);
private void OnSpellCastChanged() =>
Interlocked.Increment(ref _magicIntentRevision);
private static void Try(Action action, ref List<Exception>? failures)
{
try
@ -174,7 +234,19 @@ public sealed class RuntimeActionState : IDisposable
Interlocked.Read(ref owner._interactionRevision),
owner.Interaction.Current.Kind,
owner.Interaction.Current.SourceObjectId,
owner.Transactions.CaptureOwnership());
owner.Transactions.CaptureOwnership(),
new RuntimeCombatAttackSnapshot(
Interlocked.Read(ref owner._combatIntentRevision),
owner.CombatAttack.RequestedHeight,
owner.CombatAttack.DesiredPower,
owner.CombatAttack.PowerBarLevel,
owner.CombatAttack.BuildInProgress,
owner.CombatAttack.AttackRequestInProgress,
owner.CombatAttack.RequestedAttackPower),
new RuntimeSpellCastSnapshot(
Interlocked.Read(ref owner._magicIntentRevision),
owner.SpellCast.LastRequestedSpellId ?? 0u,
owner.SpellCast.LastRequestedTargetId ?? 0u));
public bool TryGetHealth(uint objectId, out float healthPercent)
{