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:
parent
81b31857c6
commit
20df9d155d
49 changed files with 1949 additions and 634 deletions
|
|
@ -3,6 +3,7 @@ using AcDream.App.Net;
|
|||
using AcDream.App.UI;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Runtime.Session;
|
||||
|
||||
namespace AcDream.App.Combat;
|
||||
|
|
@ -164,37 +165,96 @@ internal sealed class LiveCombatModeCommandSlot : ILiveCombatModeCommand
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns the input-facing combat-mode command. The choice itself remains the
|
||||
/// retail port in <see cref="CombatInputPlanner"/>:
|
||||
/// <c>ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310</c> and
|
||||
/// <c>ClientCombatSystem::ToggleCombatMode @ 0x0056C8C0</c>. ACE remains the
|
||||
/// authority; the local state update preserves the accepted immediate toolbar
|
||||
/// response while the server confirmation travels through the normal route.
|
||||
/// </summary>
|
||||
internal sealed class LiveCombatModeCommandController : ILiveCombatModeCommand
|
||||
internal sealed class RuntimeCombatModeOperationsSlot
|
||||
: IRuntimeCombatModeOperations
|
||||
{
|
||||
private IRuntimeCombatModeOperations? _owner;
|
||||
|
||||
public IDisposable BindOwned(IRuntimeCombatModeOperations owner)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(owner);
|
||||
if (_owner is not null)
|
||||
throw new InvalidOperationException(
|
||||
"Runtime combat-mode operations are already bound.");
|
||||
_owner = owner;
|
||||
return new Binding(this, owner);
|
||||
}
|
||||
|
||||
private void Unbind(IRuntimeCombatModeOperations expected)
|
||||
{
|
||||
if (ReferenceEquals(_owner, expected))
|
||||
_owner = null;
|
||||
}
|
||||
|
||||
public bool IsInWorld => _owner?.IsInWorld == true;
|
||||
public IReadOnlyList<ClientObject> GetOrderedEquipment() =>
|
||||
_owner?.GetOrderedEquipment() ?? [];
|
||||
public void NotifyExplicitCombatModeRequest() =>
|
||||
_owner?.NotifyExplicitCombatModeRequest();
|
||||
public void SendChangeCombatMode(CombatMode mode) =>
|
||||
_owner?.SendChangeCombatMode(mode);
|
||||
|
||||
private sealed class Binding : IDisposable
|
||||
{
|
||||
private RuntimeCombatModeOperationsSlot? _slot;
|
||||
private readonly IRuntimeCombatModeOperations _expected;
|
||||
|
||||
public Binding(
|
||||
RuntimeCombatModeOperationsSlot slot,
|
||||
IRuntimeCombatModeOperations expected)
|
||||
{
|
||||
_slot = slot;
|
||||
_expected = expected;
|
||||
}
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _slot, null)?.Unbind(_expected);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class LiveCombatModeOperations
|
||||
: IRuntimeCombatModeOperations
|
||||
{
|
||||
private readonly ILiveCombatModeAuthority _authority;
|
||||
private readonly ICombatEquipmentSource _equipment;
|
||||
private readonly CombatState _combat;
|
||||
private readonly IExplicitCombatModeIntentSink _itemIntent;
|
||||
|
||||
public LiveCombatModeOperations(
|
||||
ILiveCombatModeAuthority authority,
|
||||
ICombatEquipmentSource equipment,
|
||||
IExplicitCombatModeIntentSink itemIntent)
|
||||
{
|
||||
_authority = authority ?? throw new ArgumentNullException(nameof(authority));
|
||||
_equipment = equipment ?? throw new ArgumentNullException(nameof(equipment));
|
||||
_itemIntent = itemIntent ?? throw new ArgumentNullException(nameof(itemIntent));
|
||||
}
|
||||
|
||||
public bool IsInWorld => _authority.IsInWorld;
|
||||
public IReadOnlyList<ClientObject> GetOrderedEquipment() =>
|
||||
_equipment.GetOrderedEquipment();
|
||||
public void NotifyExplicitCombatModeRequest() =>
|
||||
_itemIntent.NotifyExplicitCombatModeRequest();
|
||||
public void SendChangeCombatMode(CombatMode mode) =>
|
||||
_authority.SendChangeCombatMode(mode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// App presentation adapter for Runtime's retail combat-mode decision.
|
||||
/// </summary>
|
||||
internal sealed class RuntimeCombatModeCommandAdapter : ILiveCombatModeCommand
|
||||
{
|
||||
private readonly RuntimeCombatModeState _owner;
|
||||
private readonly Action<string> _log;
|
||||
private readonly Action<string>? _toast;
|
||||
private readonly Action<string> _systemMessage;
|
||||
|
||||
public LiveCombatModeCommandController(
|
||||
ILiveCombatModeAuthority authority,
|
||||
ICombatEquipmentSource equipment,
|
||||
CombatState combat,
|
||||
IExplicitCombatModeIntentSink itemIntent,
|
||||
public RuntimeCombatModeCommandAdapter(
|
||||
RuntimeCombatModeState owner,
|
||||
Action<string>? log = null,
|
||||
Action<string>? toast = null,
|
||||
Action<string>? systemMessage = null)
|
||||
{
|
||||
_authority = authority ?? throw new ArgumentNullException(nameof(authority));
|
||||
_equipment = equipment ?? throw new ArgumentNullException(nameof(equipment));
|
||||
_combat = combat ?? throw new ArgumentNullException(nameof(combat));
|
||||
_itemIntent = itemIntent ?? throw new ArgumentNullException(nameof(itemIntent));
|
||||
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
||||
_log = log ?? (_ => { });
|
||||
_toast = toast;
|
||||
_systemMessage = systemMessage ?? (_ => { });
|
||||
|
|
@ -202,48 +262,19 @@ internal sealed class LiveCombatModeCommandController : ILiveCombatModeCommand
|
|||
|
||||
public void Toggle()
|
||||
{
|
||||
if (!_authority.IsInWorld)
|
||||
RuntimeCombatModeRequestResult result = _owner.Toggle();
|
||||
if (result.Status == RuntimeCombatModeRequestStatus.Inactive)
|
||||
return;
|
||||
|
||||
// Every explicit user combat request supersedes an in-flight auto-wield
|
||||
// settlement, including a request retail rejects before SetCombatMode.
|
||||
_itemIntent.NotifyExplicitCombatModeRequest();
|
||||
|
||||
CombatMode currentMode = _combat.CurrentMode;
|
||||
CombatMode nextMode;
|
||||
if (currentMode != CombatMode.NonCombat)
|
||||
if (result.Status == RuntimeCombatModeRequestStatus.Rejected)
|
||||
{
|
||||
// ToggleCombatMode @ 0x0056C8C0 immediately selects NonCombat and
|
||||
// never queries default equipment on this branch.
|
||||
nextMode = CombatMode.NonCombat;
|
||||
}
|
||||
else
|
||||
{
|
||||
DefaultCombatModeDecision defaultDecision =
|
||||
CombatInputPlanner.GetDefaultCombatModeDecision(
|
||||
_equipment.GetOrderedEquipment());
|
||||
nextMode = CombatInputPlanner.ToggleMode(
|
||||
currentMode,
|
||||
defaultDecision.Mode);
|
||||
|
||||
// GetDefaultCombatMode prints this notice before SetCombatMode.
|
||||
// SetCombatMode then sees NonCombat == NonCombat and does no work.
|
||||
if (defaultDecision.IncompatibleHeldItem is { } held)
|
||||
{
|
||||
string notice =
|
||||
$"You can't enter combat mode while wielding the {held.GetAppropriateName()}";
|
||||
_log($"combat: {notice}");
|
||||
_systemMessage(notice);
|
||||
return;
|
||||
}
|
||||
string notice = result.Notice ?? string.Empty;
|
||||
_log($"combat: {notice}");
|
||||
_systemMessage(notice);
|
||||
return;
|
||||
}
|
||||
|
||||
// Preserve the accepted command order: explicit intent was published
|
||||
// above, then send the request and publish local presentation.
|
||||
_authority.SendChangeCombatMode(nextMode);
|
||||
_combat.SetCombatMode(nextMode);
|
||||
|
||||
string message = $"Combat mode {nextMode}";
|
||||
string message = $"Combat mode {result.Mode}";
|
||||
_log($"combat: {message}");
|
||||
_toast?.Invoke(message);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue