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
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.App.Input;
|
||||
using AcDream.App.Net;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
||||
namespace AcDream.App.Combat;
|
||||
|
|
@ -54,11 +55,12 @@ internal sealed class CombatFeedbackSlot : ICombatFeedbackSink
|
|||
public void Show(string message) => _viewModel?.AddToast(message);
|
||||
}
|
||||
|
||||
internal sealed class CombatAttackOperationsSlot : ICombatAttackOperations
|
||||
internal sealed class CombatAttackOperationsSlot
|
||||
: IRuntimeCombatAttackOperations
|
||||
{
|
||||
private ICombatAttackOperations? _owner;
|
||||
private IRuntimeCombatAttackOperations? _owner;
|
||||
|
||||
public void Bind(ICombatAttackOperations owner)
|
||||
public void Bind(IRuntimeCombatAttackOperations owner)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(owner);
|
||||
if (_owner is not null && !ReferenceEquals(_owner, owner))
|
||||
|
|
@ -67,7 +69,7 @@ internal sealed class CombatAttackOperationsSlot : ICombatAttackOperations
|
|||
_owner = owner;
|
||||
}
|
||||
|
||||
public IDisposable BindOwned(ICombatAttackOperations owner)
|
||||
public IDisposable BindOwned(IRuntimeCombatAttackOperations owner)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(owner);
|
||||
if (_owner is not null)
|
||||
|
|
@ -77,7 +79,7 @@ internal sealed class CombatAttackOperationsSlot : ICombatAttackOperations
|
|||
return new Binding(this, owner);
|
||||
}
|
||||
|
||||
private void Unbind(ICombatAttackOperations expected)
|
||||
private void Unbind(IRuntimeCombatAttackOperations expected)
|
||||
{
|
||||
if (ReferenceEquals(_owner, expected))
|
||||
_owner = null;
|
||||
|
|
@ -95,11 +97,11 @@ internal sealed class CombatAttackOperationsSlot : ICombatAttackOperations
|
|||
private sealed class Binding : IDisposable
|
||||
{
|
||||
private CombatAttackOperationsSlot? _slot;
|
||||
private readonly ICombatAttackOperations _expected;
|
||||
private readonly IRuntimeCombatAttackOperations _expected;
|
||||
|
||||
public Binding(
|
||||
CombatAttackOperationsSlot slot,
|
||||
ICombatAttackOperations expected)
|
||||
IRuntimeCombatAttackOperations expected)
|
||||
{
|
||||
_slot = slot;
|
||||
_expected = expected;
|
||||
|
|
@ -114,7 +116,8 @@ internal sealed class CombatAttackOperationsSlot : ICombatAttackOperations
|
|||
/// Production combat request dependencies. The attack state machine retains
|
||||
/// this typed owner rather than callbacks into the application window.
|
||||
/// </summary>
|
||||
internal sealed class LiveCombatAttackOperations : ICombatAttackOperations
|
||||
internal sealed class LiveCombatAttackOperations
|
||||
: IRuntimeCombatAttackOperations
|
||||
{
|
||||
private readonly CombatState _combat;
|
||||
private readonly ICombatAttackTargetSource _targets;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
63
src/AcDream.App/Combat/RuntimeCombatTargetOperationsSlot.cs
Normal file
63
src/AcDream.App/Combat/RuntimeCombatTargetOperationsSlot.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.App.Combat;
|
||||
|
||||
/// <summary>
|
||||
/// Early Runtime construction seam for the App-owned closest-hostile query.
|
||||
/// The slot owns no target identity or selection state.
|
||||
/// </summary>
|
||||
internal sealed class RuntimeCombatTargetOperationsSlot
|
||||
: IRuntimeCombatTargetOperations
|
||||
{
|
||||
private IRuntimeCombatTargetOperations? _owner;
|
||||
|
||||
public IDisposable BindOwned(IRuntimeCombatTargetOperations owner)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(owner);
|
||||
if (_owner is not null)
|
||||
throw new InvalidOperationException(
|
||||
"Runtime combat-target operations are already bound.");
|
||||
_owner = owner;
|
||||
return new Binding(this, owner);
|
||||
}
|
||||
|
||||
private void Unbind(IRuntimeCombatTargetOperations expected)
|
||||
{
|
||||
if (ReferenceEquals(_owner, expected))
|
||||
_owner = null;
|
||||
}
|
||||
|
||||
public bool AutoTarget => _owner?.AutoTarget == true;
|
||||
public uint? SelectClosestTarget() => _owner?.SelectClosestTarget();
|
||||
|
||||
private sealed class Binding : IDisposable
|
||||
{
|
||||
private RuntimeCombatTargetOperationsSlot? _slot;
|
||||
private readonly IRuntimeCombatTargetOperations _expected;
|
||||
|
||||
public Binding(
|
||||
RuntimeCombatTargetOperationsSlot slot,
|
||||
IRuntimeCombatTargetOperations expected)
|
||||
{
|
||||
_slot = slot;
|
||||
_expected = expected;
|
||||
}
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _slot, null)?.Unbind(_expected);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class LiveCombatTargetOperations(
|
||||
Func<bool> autoTarget,
|
||||
Func<uint?> selectClosestTarget)
|
||||
: IRuntimeCombatTargetOperations
|
||||
{
|
||||
private readonly Func<bool> _autoTarget = autoTarget
|
||||
?? throw new ArgumentNullException(nameof(autoTarget));
|
||||
private readonly Func<uint?> _selectClosestTarget = selectClosestTarget
|
||||
?? throw new ArgumentNullException(nameof(selectClosestTarget));
|
||||
|
||||
public bool AutoTarget => _autoTarget();
|
||||
public uint? SelectClosestTarget() => _selectClosestTarget();
|
||||
}
|
||||
|
|
@ -45,7 +45,9 @@ internal sealed record InteractionRetainedUiDependencies(
|
|||
InputDispatcher? InputDispatcher,
|
||||
RuntimeSettingsController Settings,
|
||||
RuntimeActionState Actions,
|
||||
ICombatAttackOperations CombatAttackOperations,
|
||||
IRuntimeCombatAttackOperations CombatAttackOperations,
|
||||
RuntimeCombatTargetOperationsSlot CombatTargetOperations,
|
||||
RuntimeSpellCastOperationsSlot SpellCastOperations,
|
||||
RuntimeInventoryState Inventory,
|
||||
MagicCatalog MagicCatalog,
|
||||
RuntimeCharacterState Character,
|
||||
|
|
@ -70,14 +72,13 @@ internal sealed record RetainedUiComposition(
|
|||
VitalsVM Vitals,
|
||||
ChatVM Chat,
|
||||
CharacterSheetProvider CharacterSheet,
|
||||
MagicRuntime Magic,
|
||||
FrameScreenshotController? Screenshots);
|
||||
|
||||
internal sealed record InteractionRetainedUiResult(
|
||||
CombatAttackController CombatAttack,
|
||||
CombatTargetController CombatTarget,
|
||||
RuntimeCombatAttackState CombatAttack,
|
||||
ExternalContainerLifecycleController ExternalContainerLifecycle,
|
||||
ItemInteractionController ItemInteraction,
|
||||
MagicRuntime Magic,
|
||||
RetainedUiComposition? RetainedUi,
|
||||
InteractionUiLateBindings LateBindings);
|
||||
|
||||
|
|
@ -89,16 +90,15 @@ internal interface IGameWindowInteractionRetainedUiPublication
|
|||
internal enum InteractionRetainedUiCompositionPoint
|
||||
{
|
||||
LateBindingsCreated,
|
||||
CombatAttackCreated,
|
||||
CombatTargetCreated,
|
||||
ExternalContainerLifecycleCreated,
|
||||
ItemInteractionCreated,
|
||||
MagicRuntimeCreated,
|
||||
RetainedUiDisabled,
|
||||
UiHostAcquired,
|
||||
InputCaptureBound,
|
||||
CursorAssetsCreated,
|
||||
CharacterSheetCreated,
|
||||
MagicRuntimeCreated,
|
||||
MouseInputWired,
|
||||
KeyboardInputWired,
|
||||
UiAssetsCreated,
|
||||
|
|
@ -225,10 +225,7 @@ internal sealed class InteractionUiLateBindings : IDisposable
|
|||
|
||||
internal interface IInteractionRetainedUiCompositionFactory
|
||||
{
|
||||
CombatAttackController CreateCombatAttack(
|
||||
InteractionRetainedUiDependencies dependencies);
|
||||
|
||||
CombatTargetController CreateCombatTarget(
|
||||
IDisposable BindCombatTarget(
|
||||
InteractionRetainedUiDependencies dependencies,
|
||||
DeferredSelectionUiAuthority selection);
|
||||
|
||||
|
|
@ -240,12 +237,18 @@ internal interface IInteractionRetainedUiCompositionFactory
|
|||
InteractionRetainedUiDependencies dependencies,
|
||||
InteractionUiLateBindings lateBindings);
|
||||
|
||||
MagicRuntime CreateMagicRuntime(
|
||||
InteractionRetainedUiDependencies dependencies,
|
||||
InteractionUiLateBindings lateBindings,
|
||||
ItemInteractionController itemInteraction);
|
||||
|
||||
RetainedUiComposition CreateRetainedUi(
|
||||
InteractionRetainedUiDependencies dependencies,
|
||||
InteractionUiLateBindings lateBindings,
|
||||
RetailUiRuntimeLease lease,
|
||||
CombatAttackController combatAttack,
|
||||
RuntimeCombatAttackState combatAttack,
|
||||
ItemInteractionController itemInteraction,
|
||||
MagicRuntime magic,
|
||||
Action<InteractionRetainedUiCompositionPoint> checkpoint);
|
||||
|
||||
void Release(IDisposable resource);
|
||||
|
|
@ -254,19 +257,13 @@ internal interface IInteractionRetainedUiCompositionFactory
|
|||
internal sealed class RetailInteractionRetainedUiCompositionFactory
|
||||
: IInteractionRetainedUiCompositionFactory
|
||||
{
|
||||
public CombatAttackController CreateCombatAttack(
|
||||
InteractionRetainedUiDependencies d) =>
|
||||
new(d.Actions.Combat, d.CombatAttackOperations);
|
||||
|
||||
public CombatTargetController CreateCombatTarget(
|
||||
public IDisposable BindCombatTarget(
|
||||
InteractionRetainedUiDependencies d,
|
||||
DeferredSelectionUiAuthority selection) =>
|
||||
new(
|
||||
d.Actions.Combat,
|
||||
d.Actions.Selection,
|
||||
d.CombatTargetOperations.BindOwned(new LiveCombatTargetOperations(
|
||||
autoTarget: () => d.Settings.Gameplay.AutoTarget,
|
||||
selectClosestTarget: () =>
|
||||
selection.SelectClosestCombatTarget(showToast: false));
|
||||
selection.SelectClosestCombatTarget(showToast: false)));
|
||||
|
||||
public ExternalContainerLifecycleController CreateExternalContainerLifecycle(
|
||||
InteractionRetainedUiDependencies d,
|
||||
|
|
@ -339,12 +336,38 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
requestUse: selection.RequestUse);
|
||||
}
|
||||
|
||||
public MagicRuntime CreateMagicRuntime(
|
||||
InteractionRetainedUiDependencies d,
|
||||
InteractionUiLateBindings late,
|
||||
ItemInteractionController itemInteraction) =>
|
||||
MagicRuntime.Create(
|
||||
d.MagicCatalog,
|
||||
d.Actions.SpellCast,
|
||||
d.SpellCastOperations,
|
||||
d.Inventory.Objects,
|
||||
localPlayerId: () => d.PlayerIdentity.ServerGuid,
|
||||
accountName: () => late.Session.AccountName
|
||||
?? d.Options.LiveUser
|
||||
?? string.Empty,
|
||||
stopCompletely: d.CombatAttackOperations.PrepareAttackRequest,
|
||||
sendUntargeted: spellId =>
|
||||
late.Session.CurrentSession?.SendCastUntargetedSpell(spellId),
|
||||
sendTargeted: (target, spellId) =>
|
||||
late.Session.CurrentSession?.SendCastTargetedSpell(
|
||||
target,
|
||||
spellId),
|
||||
displayMessage:
|
||||
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au),
|
||||
incrementBusy: itemInteraction.IncrementBusyCount,
|
||||
canSend: () => late.Session.IsInWorld);
|
||||
|
||||
public RetainedUiComposition CreateRetainedUi(
|
||||
InteractionRetainedUiDependencies d,
|
||||
InteractionUiLateBindings late,
|
||||
RetailUiRuntimeLease lease,
|
||||
CombatAttackController combatAttack,
|
||||
RuntimeCombatAttackState combatAttack,
|
||||
ItemInteractionController itemInteraction,
|
||||
MagicRuntime magic,
|
||||
Action<InteractionRetainedUiCompositionPoint> checkpoint)
|
||||
{
|
||||
IDisposable? inputCapture = null;
|
||||
|
|
@ -404,27 +427,6 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
credits));
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.CharacterSheetCreated);
|
||||
|
||||
MagicRuntime magic = MagicRuntime.Create(
|
||||
d.MagicCatalog,
|
||||
d.Character.Spellbook,
|
||||
d.Inventory.Objects,
|
||||
selectedObject: () =>
|
||||
d.Actions.Selection.SelectedObjectId,
|
||||
localPlayerId: () => d.PlayerIdentity.ServerGuid,
|
||||
accountName: () => late.Session.AccountName
|
||||
?? d.Options.LiveUser
|
||||
?? string.Empty,
|
||||
stopCompletely: d.CombatAttackOperations.PrepareAttackRequest,
|
||||
sendUntargeted: spellId =>
|
||||
late.Session.CurrentSession?.SendCastUntargetedSpell(spellId),
|
||||
sendTargeted: (target, spellId) =>
|
||||
late.Session.CurrentSession?.SendCastTargetedSpell(target, spellId),
|
||||
displayMessage:
|
||||
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au),
|
||||
incrementBusy: itemInteraction.IncrementBusyCount,
|
||||
canSend: () => late.Session.IsInWorld);
|
||||
checkpoint(InteractionRetainedUiCompositionPoint.MagicRuntimeCreated);
|
||||
|
||||
uint MagicSkillLevel(MagicSchool school)
|
||||
{
|
||||
static uint SkillId(MagicSchool value) => value switch
|
||||
|
|
@ -717,7 +719,6 @@ internal sealed class RetailInteractionRetainedUiCompositionFactory
|
|||
vitals,
|
||||
chat,
|
||||
characterSheet,
|
||||
magic,
|
||||
screenshots);
|
||||
}
|
||||
catch (Exception failure)
|
||||
|
|
@ -805,15 +806,12 @@ internal sealed class InteractionRetainedUiCompositionPhase
|
|||
_dependencies.SelectionCameraFactory(late.SelectionViewPlane));
|
||||
Fault(InteractionRetainedUiCompositionPoint.LateBindingsCreated);
|
||||
|
||||
var attackLease = scope.Acquire(
|
||||
"combat attack controller",
|
||||
() => _factory.CreateCombatAttack(_dependencies),
|
||||
_factory.Release);
|
||||
Fault(InteractionRetainedUiCompositionPoint.CombatAttackCreated);
|
||||
var targetLease = scope.Acquire(
|
||||
"combat target controller",
|
||||
() => _factory.CreateCombatTarget(_dependencies, late.Selection),
|
||||
_factory.Release);
|
||||
IDisposable combatTargetBinding = _factory.BindCombatTarget(
|
||||
_dependencies,
|
||||
late.Selection);
|
||||
late.AdoptLateOwnerBinding(
|
||||
"combat-target operations",
|
||||
combatTargetBinding);
|
||||
Fault(InteractionRetainedUiCompositionPoint.CombatTargetCreated);
|
||||
var externalLease = scope.Acquire(
|
||||
"external container lifecycle",
|
||||
|
|
@ -827,6 +825,14 @@ internal sealed class InteractionRetainedUiCompositionPhase
|
|||
() => _factory.CreateItemInteraction(_dependencies, late),
|
||||
_factory.Release);
|
||||
Fault(InteractionRetainedUiCompositionPoint.ItemInteractionCreated);
|
||||
var magicLease = scope.Acquire(
|
||||
"magic runtime",
|
||||
() => _factory.CreateMagicRuntime(
|
||||
_dependencies,
|
||||
late,
|
||||
itemLease.Resource),
|
||||
_factory.Release);
|
||||
Fault(InteractionRetainedUiCompositionPoint.MagicRuntimeCreated);
|
||||
|
||||
var uiLease = scope.Own(
|
||||
"retained UI runtime lease",
|
||||
|
|
@ -839,8 +845,9 @@ internal sealed class InteractionRetainedUiCompositionPhase
|
|||
_dependencies,
|
||||
late,
|
||||
_retainedUiLease,
|
||||
attackLease.Resource,
|
||||
_dependencies.Actions.CombatAttack,
|
||||
itemLease.Resource,
|
||||
magicLease.Resource,
|
||||
Fault);
|
||||
}
|
||||
else
|
||||
|
|
@ -849,18 +856,17 @@ internal sealed class InteractionRetainedUiCompositionPhase
|
|||
}
|
||||
|
||||
var result = new InteractionRetainedUiResult(
|
||||
attackLease.Resource,
|
||||
targetLease.Resource,
|
||||
_dependencies.Actions.CombatAttack,
|
||||
externalLease.Resource,
|
||||
itemLease.Resource,
|
||||
magicLease.Resource,
|
||||
retainedUi,
|
||||
late);
|
||||
_publication.PublishInteractionRetainedUi(result);
|
||||
lateLease.Transfer();
|
||||
attackLease.Transfer();
|
||||
targetLease.Transfer();
|
||||
externalLease.Transfer();
|
||||
itemLease.Transfer();
|
||||
magicLease.Transfer();
|
||||
uiLease.Transfer();
|
||||
Fault(InteractionRetainedUiCompositionPoint.ResultPublished);
|
||||
scope.Complete();
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ internal sealed record SessionPlayerDependencies(
|
|||
WorldSceneDebugState WorldSceneDebugState,
|
||||
RuntimeDiagnosticCommandSlot RuntimeDiagnosticCommands,
|
||||
LiveCombatModeCommandSlot CombatModeCommands,
|
||||
RuntimeCombatModeOperationsSlot CombatModeOperations,
|
||||
HostQuiescenceGate HostQuiescence,
|
||||
PhysicsEngine PhysicsEngine,
|
||||
PhysicsDataCache PhysicsDataCache,
|
||||
|
|
@ -514,7 +515,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
d.PhysicsEngine,
|
||||
content.Dats,
|
||||
content.AnimationLoader,
|
||||
interaction.CombatTarget,
|
||||
d.Actions.CombatTarget,
|
||||
d.WorldOrigin,
|
||||
d.TeleportSink,
|
||||
d.PlayerController,
|
||||
|
|
@ -804,7 +805,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
interaction.RetainedUi?.Runtime,
|
||||
vitals,
|
||||
interaction.RetainedUi?.CharacterSheet,
|
||||
interaction.RetainedUi?.Magic,
|
||||
interaction.Magic,
|
||||
live.PaperdollPresenter),
|
||||
new LiveSessionInteractionRuntime(
|
||||
d.Settings,
|
||||
|
|
@ -850,14 +851,18 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
Action<string>? debugToast = d.SettingsDevTools.DevTools is { } devOwner
|
||||
? message => devOwner.Debug.AddToast(message)
|
||||
: null;
|
||||
var combatCommand = new LiveCombatModeCommandController(
|
||||
var combatModeOperations = new LiveCombatModeOperations(
|
||||
new LiveSessionCombatModeAuthority(sessionHost),
|
||||
new LocalPlayerCombatEquipmentSource(
|
||||
d.EntityObjects.Objects,
|
||||
d.PlayerIdentity),
|
||||
d.Actions.Combat,
|
||||
new ItemInteractionCombatModeIntentSink(
|
||||
interaction.ItemInteraction),
|
||||
interaction.ItemInteraction));
|
||||
bindings.Adopt(
|
||||
"runtime combat-mode operations",
|
||||
d.CombatModeOperations.BindOwned(combatModeOperations));
|
||||
var combatCommand = new RuntimeCombatModeCommandAdapter(
|
||||
d.Actions.CombatMode,
|
||||
d.Log,
|
||||
debugToast,
|
||||
text => d.Communication.Chat.OnSystemMessage(text, 0x1Au));
|
||||
|
|
@ -879,8 +884,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
worldReveal,
|
||||
d.UpdateClock,
|
||||
live.SelectionInteractions,
|
||||
gameplayInput,
|
||||
combatCommand);
|
||||
gameplayInput);
|
||||
bindings.Adopt("current game runtime adapter", gameRuntime);
|
||||
bindings.Adopt(
|
||||
"retained-UI game runtime commands",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.App.Update;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.Input;
|
||||
|
|
@ -13,18 +14,55 @@ internal interface ICombatInputFrameController
|
|||
|
||||
internal sealed class CombatAttackInputFrameAdapter : ICombatInputFrameController
|
||||
{
|
||||
private readonly CombatAttackController _owner;
|
||||
private readonly RuntimeCombatAttackState _owner;
|
||||
|
||||
public CombatAttackInputFrameAdapter(CombatAttackController owner) =>
|
||||
public CombatAttackInputFrameAdapter(RuntimeCombatAttackState owner) =>
|
||||
_owner = owner ?? throw new ArgumentNullException(nameof(owner));
|
||||
|
||||
public void Tick() => _owner.Tick();
|
||||
|
||||
public void HandleMovementInput(InputAction action, ActivationType activation) =>
|
||||
_owner.HandleMovementInput(action, activation);
|
||||
public void HandleMovementInput(InputAction action, ActivationType activation)
|
||||
{
|
||||
if (activation != ActivationType.Press
|
||||
|| action is not (
|
||||
InputAction.MovementForward
|
||||
or InputAction.MovementBackup
|
||||
or InputAction.MovementRunLock
|
||||
or InputAction.MovementJump))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
public bool HandleInputAction(InputAction action, ActivationType activation) =>
|
||||
_owner.HandleInputAction(action, activation);
|
||||
_owner.HandleCommand(new RuntimeCombatAttackInput(
|
||||
RuntimeCombatAttackCommand.AbortForMovement,
|
||||
RuntimeInputActivation.Press));
|
||||
}
|
||||
|
||||
public bool HandleInputAction(InputAction action, ActivationType activation)
|
||||
{
|
||||
RuntimeCombatAttackCommand? command = action switch
|
||||
{
|
||||
InputAction.CombatLowAttack =>
|
||||
RuntimeCombatAttackCommand.LowAttack,
|
||||
InputAction.CombatMediumAttack =>
|
||||
RuntimeCombatAttackCommand.MediumAttack,
|
||||
InputAction.CombatHighAttack =>
|
||||
RuntimeCombatAttackCommand.HighAttack,
|
||||
InputAction.CombatDecreaseAttackPower =>
|
||||
RuntimeCombatAttackCommand.DecreasePower,
|
||||
InputAction.CombatIncreaseAttackPower =>
|
||||
RuntimeCombatAttackCommand.IncreasePower,
|
||||
_ => null,
|
||||
};
|
||||
if (command is null)
|
||||
return false;
|
||||
|
||||
return _owner.HandleCommand(new RuntimeCombatAttackInput(
|
||||
command.Value,
|
||||
activation == ActivationType.Press
|
||||
? RuntimeInputActivation.Press
|
||||
: RuntimeInputActivation.Release));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ internal sealed record LiveSessionInteractionRuntime(
|
|||
PlayerModeController PlayerMode,
|
||||
PlayerModeAutoEntry PlayerModeAutoEntry,
|
||||
ItemInteractionController ItemInteraction,
|
||||
CombatAttackController CombatAttack,
|
||||
RuntimeCombatAttackState CombatAttack,
|
||||
SelectionInteractionController SelectionInteractions);
|
||||
|
||||
internal sealed record LiveSessionWorldRuntime(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ using AcDream.Core.Net.Messages;
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter;
|
||||
|
|
@ -48,7 +49,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
private readonly PhysicsEngine _physicsEngine;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly IAnimationLoader _animLoader;
|
||||
private readonly CombatTargetController? _combatTargetController;
|
||||
private readonly RuntimeCombatTargetState? _combatTargetController;
|
||||
private readonly LiveWorldOriginState _origin;
|
||||
private readonly AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink
|
||||
_localPlayerTeleport;
|
||||
|
|
@ -90,7 +91,7 @@ internal sealed class LiveEntityNetworkUpdateController
|
|||
PhysicsEngine physicsEngine,
|
||||
IDatReaderWriter dats,
|
||||
IAnimationLoader animLoader,
|
||||
CombatTargetController? combatTargetController,
|
||||
RuntimeCombatTargetState? combatTargetController,
|
||||
LiveWorldOriginState origin,
|
||||
AcDream.App.Streaming.ILocalPlayerTeleportNetworkSink localPlayerTeleport,
|
||||
ILocalPlayerControllerSource playerControllerSource,
|
||||
|
|
|
|||
|
|
@ -373,10 +373,15 @@ public sealed class GameWindow :
|
|||
private readonly DeferredRenderFrameDiagnosticsSource _uiFrameDiagnostics = new();
|
||||
private readonly AcDream.App.Combat.CombatAttackOperationsSlot
|
||||
_combatAttackOperations = new();
|
||||
private readonly AcDream.App.Combat.RuntimeCombatTargetOperationsSlot
|
||||
_combatTargetOperations = new();
|
||||
private readonly AcDream.App.Combat.RuntimeCombatModeOperationsSlot
|
||||
_combatModeOperations = new();
|
||||
private readonly AcDream.App.Spells.RuntimeSpellCastOperationsSlot
|
||||
_spellCastOperations = new();
|
||||
private readonly AcDream.App.Combat.CombatFeedbackSlot
|
||||
_combatFeedback = new();
|
||||
private AcDream.App.Combat.CombatAttackController? _combatAttackController;
|
||||
private AcDream.App.Combat.CombatTargetController? _combatTargetController;
|
||||
private RuntimeCombatAttackState? _combatAttackController;
|
||||
private AcDream.App.UI.ItemInteractionController? _itemInteractionController;
|
||||
private AcDream.App.World.ExternalContainerLifecycleController? _externalContainerLifecycle;
|
||||
private AcDream.App.Spells.MagicRuntime? _magicRuntime;
|
||||
|
|
@ -561,8 +566,14 @@ public sealed class GameWindow :
|
|||
{
|
||||
_options = options ?? throw new System.ArgumentNullException(nameof(options));
|
||||
_runtimeInventory = new RuntimeInventoryState(_runtimeEntityObjects);
|
||||
_runtimeActions = new RuntimeActionState(_runtimeInventory.Transactions);
|
||||
_runtimeCharacter = new RuntimeCharacterState();
|
||||
_runtimeActions = new RuntimeActionState(
|
||||
_runtimeInventory.Transactions,
|
||||
_runtimeCharacter.Spellbook,
|
||||
_combatAttackOperations,
|
||||
_combatTargetOperations,
|
||||
_combatModeOperations,
|
||||
_spellCastOperations);
|
||||
var alphaScratchBudgets =
|
||||
AcDream.App.Rendering.Residency.AlphaScratchBudgetProfile.Create(
|
||||
_options.ResidencyBudgets.AlphaScratchBytes);
|
||||
|
|
@ -905,7 +916,6 @@ public sealed class GameWindow :
|
|||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
if (_combatAttackController is not null
|
||||
|| _combatTargetController is not null
|
||||
|| _externalContainerLifecycle is not null
|
||||
|| _itemInteractionController is not null
|
||||
|| _interactionUiLateBindings is not null
|
||||
|
|
@ -921,10 +931,10 @@ public sealed class GameWindow :
|
|||
}
|
||||
|
||||
_combatAttackController = result.CombatAttack;
|
||||
_combatTargetController = result.CombatTarget;
|
||||
_externalContainerLifecycle = result.ExternalContainerLifecycle;
|
||||
_itemInteractionController = result.ItemInteraction;
|
||||
_interactionUiLateBindings = result.LateBindings;
|
||||
_magicRuntime = result.Magic;
|
||||
if (result.RetainedUi is { } retained)
|
||||
{
|
||||
_uiHost = retained.Host;
|
||||
|
|
@ -932,7 +942,6 @@ public sealed class GameWindow :
|
|||
_vitalsVm ??= retained.Vitals;
|
||||
_retailChatVm = retained.Chat;
|
||||
_characterSheetProvider = retained.CharacterSheet;
|
||||
_magicRuntime = retained.Magic;
|
||||
_frameScreenshots = retained.Screenshots;
|
||||
}
|
||||
}
|
||||
|
|
@ -1256,6 +1265,8 @@ public sealed class GameWindow :
|
|||
_runtimeSettings,
|
||||
_runtimeActions,
|
||||
_combatAttackOperations,
|
||||
_combatTargetOperations,
|
||||
_spellCastOperations,
|
||||
_runtimeInventory,
|
||||
contentEffectsAudio.MagicCatalog,
|
||||
_runtimeCharacter,
|
||||
|
|
@ -1358,6 +1369,7 @@ public sealed class GameWindow :
|
|||
_worldSceneDebugState,
|
||||
_runtimeDiagnosticCommands,
|
||||
_liveCombatModeCommands,
|
||||
_combatModeOperations,
|
||||
_hostQuiescence,
|
||||
_physicsEngine,
|
||||
_physicsDataCache,
|
||||
|
|
@ -1564,8 +1576,7 @@ public sealed class GameWindow :
|
|||
new LiveShutdownRoots(
|
||||
_cameraPointerInput,
|
||||
_retailUiLease,
|
||||
_combatTargetController,
|
||||
_combatAttackController,
|
||||
_magicRuntime,
|
||||
_itemInteractionController,
|
||||
_externalContainerLifecycle,
|
||||
_streamer,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ using AcDream.App.Rendering.Scene;
|
|||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Settings;
|
||||
using AcDream.App.Spells;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.World;
|
||||
|
|
@ -76,8 +77,7 @@ internal sealed record FrameShutdownRoots(
|
|||
internal sealed record LiveShutdownRoots(
|
||||
CameraPointerInputController? CameraPointer,
|
||||
RetailUiRuntimeLease RetailUi,
|
||||
CombatTargetController? CombatTarget,
|
||||
CombatAttackController? CombatAttack,
|
||||
MagicRuntime? Magic,
|
||||
ItemInteractionController? ItemInteraction,
|
||||
ExternalContainerLifecycleController? ExternalContainers,
|
||||
LandblockStreamer? Streamer,
|
||||
|
|
@ -377,8 +377,7 @@ internal static class GameWindowShutdownManifest
|
|||
Hard("interaction/UI late bindings", () => frame.InteractionBindings?.Dispose()),
|
||||
Hard("mouse capture", () => live.CameraPointer?.ReleaseMouseLookAfterSessionRetirement()),
|
||||
Hard("retail UI", () => DisposeRetailUi(live.RetailUi)),
|
||||
Hard("combat target", () => live.CombatTarget?.Dispose()),
|
||||
Hard("combat attack", () => live.CombatAttack?.Dispose()),
|
||||
Hard("magic runtime", () => live.Magic?.Dispose()),
|
||||
Hard("item interaction", () => live.ItemInteraction?.Dispose()),
|
||||
Hard("external containers", () => live.ExternalContainers?.Dispose()),
|
||||
Hard("streamer", () => live.Streamer?.Dispose()),
|
||||
|
|
@ -393,12 +392,12 @@ internal static class GameWindowShutdownManifest
|
|||
]),
|
||||
new ResourceShutdownStage("runtime entity/object stream",
|
||||
[
|
||||
Hard(
|
||||
"runtime character state",
|
||||
live.Character.Dispose),
|
||||
Hard(
|
||||
"runtime action state",
|
||||
live.Actions.Dispose),
|
||||
Hard(
|
||||
"runtime character state",
|
||||
live.Character.Dispose),
|
||||
Hard(
|
||||
"runtime inventory state",
|
||||
live.Inventory.Dispose),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Interaction;
|
||||
using AcDream.App.Net;
|
||||
|
|
@ -43,8 +42,7 @@ internal sealed class CurrentGameRuntimeAdapter
|
|||
WorldRevealCoordinator worldReveal,
|
||||
IGameRuntimeClock clock,
|
||||
SelectionInteractionController selection,
|
||||
GameplayInputFrameController gameplayInput,
|
||||
ILiveCombatModeCommand combat)
|
||||
GameplayInputFrameController gameplayInput)
|
||||
{
|
||||
_view = new CurrentGameRuntimeViewAdapter(
|
||||
session,
|
||||
|
|
@ -75,7 +73,6 @@ internal sealed class CurrentGameRuntimeAdapter
|
|||
actions,
|
||||
selection,
|
||||
gameplayInput,
|
||||
combat,
|
||||
_events);
|
||||
}
|
||||
|
||||
|
|
@ -95,6 +92,7 @@ internal sealed class CurrentGameRuntimeAdapter
|
|||
public IRuntimeSessionCommands Session => _commands;
|
||||
public IRuntimeSelectionCommands Selection => _commands;
|
||||
public IRuntimeCombatCommands Combat => _commands;
|
||||
public IRuntimeMagicCommands Magic => _commands;
|
||||
public IRuntimeMovementCommands MovementCommands => _commands;
|
||||
IRuntimeMovementCommands IGameRuntimeCommands.Movement => _commands;
|
||||
public IRuntimeChatCommands ChatCommands => _commands;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Interaction;
|
||||
using AcDream.App.Net;
|
||||
|
|
@ -19,6 +18,7 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
: IRuntimeSessionCommands,
|
||||
IRuntimeSelectionCommands,
|
||||
IRuntimeCombatCommands,
|
||||
IRuntimeMagicCommands,
|
||||
IRuntimeMovementCommands,
|
||||
IRuntimeChatCommands,
|
||||
IRuntimePortalCommands,
|
||||
|
|
@ -36,7 +36,6 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
private readonly RuntimeActionState _actions;
|
||||
private readonly SelectionInteractionController _selection;
|
||||
private readonly GameplayInputFrameController _gameplayInput;
|
||||
private readonly ILiveCombatModeCommand _combat;
|
||||
private readonly ICurrentGameRuntimeEventSink _events;
|
||||
|
||||
public CurrentGameRuntimeCommandAdapter(
|
||||
|
|
@ -49,7 +48,6 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
RuntimeActionState actions,
|
||||
SelectionInteractionController selection,
|
||||
GameplayInputFrameController gameplayInput,
|
||||
ILiveCombatModeCommand combat,
|
||||
ICurrentGameRuntimeEventSink events)
|
||||
{
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
|
|
@ -63,7 +61,6 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||
_gameplayInput = gameplayInput
|
||||
?? throw new ArgumentNullException(nameof(gameplayInput));
|
||||
_combat = combat ?? throw new ArgumentNullException(nameof(combat));
|
||||
_events = events ?? throw new ArgumentNullException(nameof(events));
|
||||
}
|
||||
|
||||
|
|
@ -178,8 +175,16 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
RuntimeCommandStatus status;
|
||||
if (command == RuntimeCombatCommand.ToggleMode)
|
||||
{
|
||||
_combat.Toggle();
|
||||
status = RuntimeCommandStatus.Accepted;
|
||||
RuntimeCombatModeRequestResult result =
|
||||
_actions.CombatMode.Toggle();
|
||||
status = result.Status switch
|
||||
{
|
||||
RuntimeCombatModeRequestStatus.Sent =>
|
||||
RuntimeCommandStatus.Accepted,
|
||||
RuntimeCombatModeRequestStatus.Inactive =>
|
||||
RuntimeCommandStatus.Inactive,
|
||||
_ => RuntimeCommandStatus.Rejected,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -190,6 +195,49 @@ internal sealed class CurrentGameRuntimeCommandAdapter
|
|||
return Result(status);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult ExecuteAttack(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeCombatAttackInput command)
|
||||
{
|
||||
RuntimeCommandStatus gate = Validate(
|
||||
expectedGeneration,
|
||||
requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
|
||||
RuntimeCommandStatus status = _actions.CombatAttack.HandleCommand(
|
||||
command)
|
||||
? RuntimeCommandStatus.Accepted
|
||||
: RuntimeCommandStatus.Unsupported;
|
||||
_events.EmitCommand(
|
||||
RuntimeCommandDomain.Combat,
|
||||
operation: 0x100 + (int)command.Command,
|
||||
status);
|
||||
return Result(status);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeMagicCommand command)
|
||||
{
|
||||
RuntimeCommandStatus gate = Validate(
|
||||
expectedGeneration,
|
||||
requireWorld: true);
|
||||
if (gate != RuntimeCommandStatus.Accepted)
|
||||
return Result(gate);
|
||||
|
||||
CastRequestResult cast = _actions.SpellCast.Cast(command.SpellId);
|
||||
RuntimeCommandStatus status = cast == CastRequestResult.Sent
|
||||
? RuntimeCommandStatus.Accepted
|
||||
: RuntimeCommandStatus.Rejected;
|
||||
_events.EmitCommand(
|
||||
RuntimeCommandDomain.Magic,
|
||||
operation: (int)cast,
|
||||
status,
|
||||
command.SpellId);
|
||||
return Result(status, command.SpellId);
|
||||
}
|
||||
|
||||
public RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
RuntimeMovementCommand command)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
|
@ -158,25 +159,29 @@ public sealed class MagicCatalog
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// App-layer owner for the live retail magic request path. The server owns
|
||||
/// turning, motion, fizzle, mana/component consumption, effects, and results.
|
||||
/// App content/policy projection for Runtime's live retail magic request
|
||||
/// owner. The server owns turning, motion, fizzle, mana/component consumption,
|
||||
/// effects, and results.
|
||||
/// </summary>
|
||||
public sealed class MagicRuntime
|
||||
public sealed class MagicRuntime : IDisposable
|
||||
{
|
||||
private readonly SpellComponentRequirementService _requirements;
|
||||
private IDisposable? _castOperationsBinding;
|
||||
|
||||
private MagicRuntime(
|
||||
MagicCatalog catalog,
|
||||
SpellCastingController casting,
|
||||
SpellComponentRequirementService requirements)
|
||||
RuntimeSpellCastState casting,
|
||||
SpellComponentRequirementService requirements,
|
||||
IDisposable castOperationsBinding)
|
||||
{
|
||||
Catalog = catalog;
|
||||
Casting = casting;
|
||||
_requirements = requirements;
|
||||
_castOperationsBinding = castOperationsBinding;
|
||||
}
|
||||
|
||||
public MagicCatalog Catalog { get; }
|
||||
public SpellCastingController Casting { get; }
|
||||
public RuntimeSpellCastState Casting { get; }
|
||||
|
||||
public IReadOnlyList<SpellExamineComponent> GetExamineComponents(uint spellId)
|
||||
{
|
||||
|
|
@ -200,11 +205,11 @@ public sealed class MagicRuntime
|
|||
return result;
|
||||
}
|
||||
|
||||
public static MagicRuntime Create(
|
||||
internal static MagicRuntime Create(
|
||||
MagicCatalog catalog,
|
||||
Spellbook spellbook,
|
||||
RuntimeSpellCastState casting,
|
||||
RuntimeSpellCastOperationsSlot operationsSlot,
|
||||
ClientObjectTable objects,
|
||||
Func<uint?> selectedObject,
|
||||
Func<uint> localPlayerId,
|
||||
Func<string> accountName,
|
||||
Action stopCompletely,
|
||||
|
|
@ -215,39 +220,29 @@ public sealed class MagicRuntime
|
|||
Func<bool> canSend)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(catalog);
|
||||
ArgumentNullException.ThrowIfNull(spellbook);
|
||||
ArgumentNullException.ThrowIfNull(casting);
|
||||
ArgumentNullException.ThrowIfNull(operationsSlot);
|
||||
ArgumentNullException.ThrowIfNull(objects);
|
||||
ArgumentNullException.ThrowIfNull(displayMessage);
|
||||
|
||||
SpellComponentRequirementService requirements = catalog.CreateRequirementService(
|
||||
objects, localPlayerId, accountName);
|
||||
bool TargetCompatible(uint target, SpellMetadata spell, bool showMessage)
|
||||
{
|
||||
if (objects.Get(target) is not { } targetObject)
|
||||
return false;
|
||||
|
||||
SpellTargetPolicyResult result = RetailSpellTargetPolicy.Evaluate(
|
||||
localPlayerId(), targetObject, spell);
|
||||
if (showMessage && !result.Allowed && result.Message is { } message)
|
||||
displayMessage(message);
|
||||
return result.Allowed;
|
||||
}
|
||||
|
||||
var casting = new SpellCastingController(
|
||||
spellbook,
|
||||
selectedObject,
|
||||
var operations = new LiveSpellCastOperations(
|
||||
requirements,
|
||||
objects,
|
||||
localPlayerId,
|
||||
stopCompletely,
|
||||
sendUntargeted,
|
||||
sendTargeted,
|
||||
displayMessage,
|
||||
targetCompatible: (target, spell) => TargetCompatible(target, spell, true),
|
||||
hasRequiredComponents: requirements.HasRequiredComponents,
|
||||
incrementBusy: incrementBusy,
|
||||
canSend: canSend,
|
||||
targetCompatibleSilent: (target, spell) => TargetCompatible(target, spell, false));
|
||||
return new MagicRuntime(catalog, casting, requirements);
|
||||
incrementBusy,
|
||||
canSend);
|
||||
IDisposable binding = operationsSlot.BindOwned(operations);
|
||||
return new MagicRuntime(catalog, casting, requirements, binding);
|
||||
}
|
||||
|
||||
public void Reset() => Casting.Reset();
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _castOperationsBinding, null)?.Dispose();
|
||||
}
|
||||
|
|
|
|||
142
src/AcDream.App/Spells/RuntimeSpellCastOperationsSlot.cs
Normal file
142
src/AcDream.App/Spells/RuntimeSpellCastOperationsSlot.cs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.App.Spells;
|
||||
|
||||
/// <summary>
|
||||
/// Early Runtime construction seam for the App-owned live spell catalog,
|
||||
/// object-table policy, movement, transport, message, and busy sinks.
|
||||
/// Runtime owns cast intent; this slot owns no gameplay state.
|
||||
/// </summary>
|
||||
internal sealed class RuntimeSpellCastOperationsSlot
|
||||
: IRuntimeSpellCastOperations
|
||||
{
|
||||
private IRuntimeSpellCastOperations? _owner;
|
||||
|
||||
public IDisposable BindOwned(IRuntimeSpellCastOperations owner)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(owner);
|
||||
if (_owner is not null)
|
||||
throw new InvalidOperationException(
|
||||
"Runtime spell-cast operations are already bound.");
|
||||
_owner = owner;
|
||||
return new Binding(this, owner);
|
||||
}
|
||||
|
||||
private void Unbind(IRuntimeSpellCastOperations expected)
|
||||
{
|
||||
if (ReferenceEquals(_owner, expected))
|
||||
_owner = null;
|
||||
}
|
||||
|
||||
public uint LocalPlayerId => _owner?.LocalPlayerId ?? 0u;
|
||||
public bool CanSend => _owner?.CanSend == true;
|
||||
|
||||
public bool HasRequiredComponents(uint spellId) =>
|
||||
_owner?.HasRequiredComponents(spellId) == true;
|
||||
|
||||
public bool IsTargetCompatible(
|
||||
uint targetId,
|
||||
SpellMetadata spell,
|
||||
bool showMessage) =>
|
||||
_owner?.IsTargetCompatible(targetId, spell, showMessage) == true;
|
||||
|
||||
public void StopCompletely() => _owner?.StopCompletely();
|
||||
public void SendUntargeted(uint spellId) =>
|
||||
_owner?.SendUntargeted(spellId);
|
||||
public void SendTargeted(uint targetId, uint spellId) =>
|
||||
_owner?.SendTargeted(targetId, spellId);
|
||||
public void DisplayMessage(string message) =>
|
||||
_owner?.DisplayMessage(message);
|
||||
public void IncrementBusy() => _owner?.IncrementBusy();
|
||||
|
||||
private sealed class Binding : IDisposable
|
||||
{
|
||||
private RuntimeSpellCastOperationsSlot? _slot;
|
||||
private readonly IRuntimeSpellCastOperations _expected;
|
||||
|
||||
public Binding(
|
||||
RuntimeSpellCastOperationsSlot slot,
|
||||
IRuntimeSpellCastOperations expected)
|
||||
{
|
||||
_slot = slot;
|
||||
_expected = expected;
|
||||
}
|
||||
|
||||
public void Dispose() =>
|
||||
Interlocked.Exchange(ref _slot, null)?.Unbind(_expected);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class LiveSpellCastOperations : IRuntimeSpellCastOperations
|
||||
{
|
||||
private readonly SpellComponentRequirementService _requirements;
|
||||
private readonly ClientObjectTable _objects;
|
||||
private readonly Func<uint> _localPlayerId;
|
||||
private readonly Action _stopCompletely;
|
||||
private readonly Action<uint> _sendUntargeted;
|
||||
private readonly Action<uint, uint> _sendTargeted;
|
||||
private readonly Action<string> _displayMessage;
|
||||
private readonly Action _incrementBusy;
|
||||
private readonly Func<bool> _canSend;
|
||||
|
||||
public LiveSpellCastOperations(
|
||||
SpellComponentRequirementService requirements,
|
||||
ClientObjectTable objects,
|
||||
Func<uint> localPlayerId,
|
||||
Action stopCompletely,
|
||||
Action<uint> sendUntargeted,
|
||||
Action<uint, uint> sendTargeted,
|
||||
Action<string> displayMessage,
|
||||
Action incrementBusy,
|
||||
Func<bool> canSend)
|
||||
{
|
||||
_requirements = requirements
|
||||
?? throw new ArgumentNullException(nameof(requirements));
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_localPlayerId = localPlayerId
|
||||
?? throw new ArgumentNullException(nameof(localPlayerId));
|
||||
_stopCompletely = stopCompletely
|
||||
?? throw new ArgumentNullException(nameof(stopCompletely));
|
||||
_sendUntargeted = sendUntargeted
|
||||
?? throw new ArgumentNullException(nameof(sendUntargeted));
|
||||
_sendTargeted = sendTargeted
|
||||
?? throw new ArgumentNullException(nameof(sendTargeted));
|
||||
_displayMessage = displayMessage
|
||||
?? throw new ArgumentNullException(nameof(displayMessage));
|
||||
_incrementBusy = incrementBusy
|
||||
?? throw new ArgumentNullException(nameof(incrementBusy));
|
||||
_canSend = canSend ?? throw new ArgumentNullException(nameof(canSend));
|
||||
}
|
||||
|
||||
public uint LocalPlayerId => _localPlayerId();
|
||||
public bool CanSend => _canSend();
|
||||
|
||||
public bool HasRequiredComponents(uint spellId) =>
|
||||
_requirements.HasRequiredComponents(spellId);
|
||||
|
||||
public bool IsTargetCompatible(
|
||||
uint targetId,
|
||||
SpellMetadata spell,
|
||||
bool showMessage)
|
||||
{
|
||||
if (_objects.Get(targetId) is not { } targetObject)
|
||||
return false;
|
||||
|
||||
SpellTargetPolicyResult result = RetailSpellTargetPolicy.Evaluate(
|
||||
LocalPlayerId,
|
||||
targetObject,
|
||||
spell);
|
||||
if (showMessage && !result.Allowed && result.Message is { } message)
|
||||
_displayMessage(message);
|
||||
return result.Allowed;
|
||||
}
|
||||
|
||||
public void StopCompletely() => _stopCompletely();
|
||||
public void SendUntargeted(uint spellId) => _sendUntargeted(spellId);
|
||||
public void SendTargeted(uint targetId, uint spellId) =>
|
||||
_sendTargeted(targetId, spellId);
|
||||
public void DisplayMessage(string message) => _displayMessage(message);
|
||||
public void IncrementBusy() => _incrementBusy();
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
using System;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.App.Spells;
|
||||
|
||||
/// <summary>
|
||||
/// App-layer owner for retail cast intent. It validates the client-owned
|
||||
/// selection rules, stops movement, and emits exactly one targeted or
|
||||
/// untargeted request. ACE owns every gameplay result after that boundary.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Port of <c>ClientMagicSystem::CastSpell</c> (0x00568040) and
|
||||
/// <c>FreeHandsAndCastSpell</c> (0x00566EF0). This controller deliberately
|
||||
/// does not consume mana/components or start local effects.
|
||||
/// </remarks>
|
||||
public sealed class SpellCastingController
|
||||
{
|
||||
private readonly Spellbook _spellbook;
|
||||
private readonly Func<uint?> _selectedObject;
|
||||
private readonly Func<uint> _localPlayerId;
|
||||
private readonly Action _stopCompletely;
|
||||
private readonly Action<uint> _sendUntargeted;
|
||||
private readonly Action<uint, uint> _sendTargeted;
|
||||
private readonly Action<string> _displayMessage;
|
||||
private readonly Func<uint, SpellMetadata, bool> _targetCompatible;
|
||||
private readonly Func<uint, SpellMetadata, bool> _targetCompatibleSilent;
|
||||
private readonly Func<uint, bool> _hasRequiredComponents;
|
||||
private readonly Action _incrementBusy;
|
||||
private readonly Func<bool> _canSend;
|
||||
|
||||
public SpellCastingController(
|
||||
Spellbook spellbook,
|
||||
Func<uint?> selectedObject,
|
||||
Func<uint> localPlayerId,
|
||||
Action stopCompletely,
|
||||
Action<uint> sendUntargeted,
|
||||
Action<uint, uint> sendTargeted,
|
||||
Action<string> displayMessage,
|
||||
Func<uint, SpellMetadata, bool>? targetCompatible = null,
|
||||
Func<uint, bool>? hasRequiredComponents = null,
|
||||
Action? incrementBusy = null,
|
||||
Func<bool>? canSend = null,
|
||||
Func<uint, SpellMetadata, bool>? targetCompatibleSilent = null)
|
||||
{
|
||||
_spellbook = spellbook ?? throw new ArgumentNullException(nameof(spellbook));
|
||||
_selectedObject = selectedObject ?? throw new ArgumentNullException(nameof(selectedObject));
|
||||
_localPlayerId = localPlayerId ?? throw new ArgumentNullException(nameof(localPlayerId));
|
||||
_stopCompletely = stopCompletely ?? throw new ArgumentNullException(nameof(stopCompletely));
|
||||
_sendUntargeted = sendUntargeted ?? throw new ArgumentNullException(nameof(sendUntargeted));
|
||||
_sendTargeted = sendTargeted ?? throw new ArgumentNullException(nameof(sendTargeted));
|
||||
_displayMessage = displayMessage ?? throw new ArgumentNullException(nameof(displayMessage));
|
||||
_targetCompatible = targetCompatible ?? ((_, _) => true);
|
||||
_targetCompatibleSilent = targetCompatibleSilent ?? _targetCompatible;
|
||||
_hasRequiredComponents = hasRequiredComponents ?? (_ => true);
|
||||
_incrementBusy = incrementBusy ?? (() => { });
|
||||
_canSend = canSend ?? (() => true);
|
||||
}
|
||||
|
||||
public uint? LastRequestedSpellId { get; private set; }
|
||||
public uint? LastRequestedTargetId { get; private set; }
|
||||
|
||||
public bool IsTargetReady(uint spellId)
|
||||
{
|
||||
if (!_spellbook.Knows(spellId)
|
||||
|| !_spellbook.TryGetMetadata(spellId, out SpellMetadata spell))
|
||||
return false;
|
||||
if (spell.IsSelfTargeted || spell.IsUntargeted || spell.TargetMask == 0u)
|
||||
return true;
|
||||
return _selectedObject() is uint target and not 0u
|
||||
&& _targetCompatibleSilent(target, spell);
|
||||
}
|
||||
|
||||
public CastRequestResult Cast(uint spellId)
|
||||
{
|
||||
if (!_spellbook.Knows(spellId) || !_spellbook.TryGetMetadata(spellId, out SpellMetadata spell))
|
||||
{
|
||||
_displayMessage("You do not know that spell.");
|
||||
return CastRequestResult.UnknownSpell;
|
||||
}
|
||||
|
||||
if (!_hasRequiredComponents(spellId))
|
||||
{
|
||||
_displayMessage("You do not have all of this spell's components.");
|
||||
return CastRequestResult.MissingComponents;
|
||||
}
|
||||
|
||||
uint? target;
|
||||
bool untargeted;
|
||||
if (spell.IsSelfTargeted)
|
||||
{
|
||||
uint playerId = _localPlayerId();
|
||||
target = playerId == 0 ? null : playerId;
|
||||
untargeted = target is null;
|
||||
}
|
||||
else if (spell.IsUntargeted || spell.TargetMask == 0)
|
||||
{
|
||||
target = null;
|
||||
untargeted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
target = _selectedObject();
|
||||
untargeted = false;
|
||||
if (target is null or 0)
|
||||
{
|
||||
_displayMessage("You must select a suitable target.");
|
||||
return CastRequestResult.NoTarget;
|
||||
}
|
||||
if (!_targetCompatible(target.Value, spell))
|
||||
return CastRequestResult.IncompatibleTarget;
|
||||
}
|
||||
|
||||
if (!_canSend())
|
||||
{
|
||||
_displayMessage("You cannot cast a spell right now.");
|
||||
return CastRequestResult.Unavailable;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_stopCompletely();
|
||||
LastRequestedSpellId = spellId;
|
||||
LastRequestedTargetId = target;
|
||||
if (untargeted)
|
||||
_sendUntargeted(spellId);
|
||||
else
|
||||
_sendTargeted(target!.Value, spellId);
|
||||
// FreeHandsAndCastSpell @ 0x00566EF0 increments the shared UI
|
||||
// busy reference only after Event_Cast has been emitted. The
|
||||
// matching server UseDone event decrements it.
|
||||
_incrementBusy();
|
||||
}
|
||||
catch
|
||||
{
|
||||
LastRequestedSpellId = null;
|
||||
LastRequestedTargetId = null;
|
||||
throw;
|
||||
}
|
||||
return CastRequestResult.Sent;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
LastRequestedSpellId = null;
|
||||
LastRequestedTargetId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public enum CastRequestResult
|
||||
{
|
||||
Sent,
|
||||
UnknownSpell,
|
||||
NoTarget,
|
||||
IncompatibleTarget,
|
||||
MissingComponents,
|
||||
Unavailable,
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Combat;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
|
|
@ -93,7 +93,7 @@ public static class FixtureProvider
|
|||
case CombatUiController.LayoutId:
|
||||
{
|
||||
var combat = new CombatState();
|
||||
var attacks = new CombatAttackController(
|
||||
var attacks = new RuntimeCombatAttackState(
|
||||
combat,
|
||||
canStartAttack: () => true,
|
||||
sendAttack: (_, _) => true,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using AcDream.App.Combat;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -46,7 +46,7 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
private readonly UiButton _autoTarget;
|
||||
private readonly UiButton _keepInView;
|
||||
private readonly CombatState _combat;
|
||||
private readonly CombatAttackController _attacks;
|
||||
private readonly RuntimeCombatAttackState _attacks;
|
||||
private readonly Func<GameplaySettings> _gameplay;
|
||||
private readonly Action<GameplaySettings> _setGameplay;
|
||||
private readonly Action<bool> _setWindowVisible;
|
||||
|
|
@ -64,7 +64,7 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
UiButton autoTarget,
|
||||
UiButton keepInView,
|
||||
CombatState combat,
|
||||
CombatAttackController attacks,
|
||||
RuntimeCombatAttackState attacks,
|
||||
Func<GameplaySettings> gameplay,
|
||||
Action<GameplaySettings> setGameplay,
|
||||
CombatUiLabels labels,
|
||||
|
|
@ -125,7 +125,7 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
public static CombatUiController? Bind(
|
||||
ImportedLayout layout,
|
||||
CombatState combat,
|
||||
CombatAttackController attacks,
|
||||
RuntimeCombatAttackState attacks,
|
||||
Func<GameplaySettings> gameplay,
|
||||
Action<GameplaySettings> setGameplay,
|
||||
CombatUiLabels labels,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using AcDream.Core.Combat;
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -13,7 +14,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <summary>
|
||||
/// Retail gmSpellcastingUI binding for the authored magic page inside combat
|
||||
/// LayoutDesc 0x21000073. Favorites remain server-persisted; casting emits one
|
||||
/// request through <see cref="SpellCastingController"/>.
|
||||
/// request through <see cref="RuntimeSpellCastState"/>.
|
||||
/// </summary>
|
||||
public sealed class SpellcastingUiController : IRetainedPanelController
|
||||
{
|
||||
|
|
@ -37,7 +38,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
];
|
||||
|
||||
private readonly Spellbook _spellbook;
|
||||
private readonly SpellCastingController _casting;
|
||||
private readonly RuntimeSpellCastState _casting;
|
||||
private readonly SelectionState _selection;
|
||||
private readonly ClientObjectTable _objects;
|
||||
private readonly Func<uint> _playerGuid;
|
||||
|
|
@ -69,7 +70,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
private SpellcastingUiController(
|
||||
ImportedLayout layout,
|
||||
Spellbook spellbook,
|
||||
SpellCastingController casting,
|
||||
RuntimeSpellCastState casting,
|
||||
ClientObjectTable objects,
|
||||
Func<uint> playerGuid,
|
||||
Func<uint, uint> resolveSpellIcon,
|
||||
|
|
@ -158,7 +159,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
public static SpellcastingUiController? Bind(
|
||||
ImportedLayout layout,
|
||||
Spellbook spellbook,
|
||||
SpellCastingController casting,
|
||||
RuntimeSpellCastState casting,
|
||||
ClientObjectTable objects,
|
||||
Func<uint> playerGuid,
|
||||
Func<uint, uint> resolveSpellIcon,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using AcDream.Core.Net;
|
|||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Spells;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.Content;
|
||||
using AcDream.UI.Abstractions;
|
||||
using AcDream.UI.Abstractions.Panels.Chat;
|
||||
|
|
@ -44,13 +45,13 @@ public sealed record RadarRuntimeBindings(
|
|||
|
||||
public sealed record CombatRuntimeBindings(
|
||||
CombatState State,
|
||||
CombatAttackController Attacks,
|
||||
RuntimeCombatAttackState Attacks,
|
||||
Func<GameplaySettings> Gameplay,
|
||||
Action<GameplaySettings> SetGameplay);
|
||||
|
||||
public sealed record MagicRuntimeBindings(
|
||||
Spellbook Spellbook,
|
||||
SpellCastingController Casting,
|
||||
RuntimeSpellCastState Casting,
|
||||
ClientObjectTable Objects,
|
||||
Func<uint> PlayerGuid,
|
||||
IReadOnlyDictionary<uint, SpellComponentDescriptor> Components,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,20 @@ using AcDream.Runtime.Gameplay;
|
|||
|
||||
namespace AcDream.Runtime;
|
||||
|
||||
public readonly record struct RuntimeCombatAttackSnapshot(
|
||||
long Revision,
|
||||
AttackHeight RequestedHeight,
|
||||
float DesiredPower,
|
||||
float PowerBarLevel,
|
||||
bool BuildInProgress,
|
||||
bool RequestInProgress,
|
||||
float RequestedPower);
|
||||
|
||||
public readonly record struct RuntimeSpellCastSnapshot(
|
||||
long Revision,
|
||||
uint LastRequestedSpellId,
|
||||
uint LastRequestedTargetId);
|
||||
|
||||
public readonly record struct RuntimeActionSnapshot(
|
||||
long SelectionRevision,
|
||||
uint SelectedObjectId,
|
||||
|
|
@ -14,7 +28,9 @@ public readonly record struct RuntimeActionSnapshot(
|
|||
long InteractionRevision,
|
||||
InteractionModeKind InteractionMode,
|
||||
uint InteractionSourceObjectId,
|
||||
RuntimeInteractionTransactionSnapshot InteractionTransactions);
|
||||
RuntimeInteractionTransactionSnapshot InteractionTransactions,
|
||||
RuntimeCombatAttackSnapshot CombatAttack = default,
|
||||
RuntimeSpellCastSnapshot Magic = default);
|
||||
|
||||
public interface IRuntimeActionView
|
||||
{
|
||||
|
|
|
|||
|
|
@ -112,6 +112,19 @@ public interface IRuntimeCombatCommands
|
|||
RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
RuntimeCombatCommand command);
|
||||
|
||||
RuntimeCommandResult ExecuteAttack(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in Gameplay.RuntimeCombatAttackInput command);
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeMagicCommand(uint SpellId);
|
||||
|
||||
public interface IRuntimeMagicCommands
|
||||
{
|
||||
RuntimeCommandResult Execute(
|
||||
RuntimeGenerationToken expectedGeneration,
|
||||
in RuntimeMagicCommand command);
|
||||
}
|
||||
|
||||
public interface IRuntimeMovementCommands
|
||||
|
|
@ -251,6 +264,8 @@ public interface IGameRuntimeCommands
|
|||
|
||||
IRuntimeCombatCommands Combat { get; }
|
||||
|
||||
IRuntimeMagicCommands Magic { get; }
|
||||
|
||||
IRuntimeMovementCommands Movement { get; }
|
||||
|
||||
IRuntimeChatCommands Chat { get; }
|
||||
|
|
|
|||
|
|
@ -7,16 +7,17 @@ public readonly record struct RuntimeEventStamp(
|
|||
|
||||
public enum RuntimeCommandDomain
|
||||
{
|
||||
Session,
|
||||
Selection,
|
||||
Combat,
|
||||
Movement,
|
||||
Chat,
|
||||
Portal,
|
||||
InventoryState,
|
||||
Spellbook,
|
||||
Character,
|
||||
Social,
|
||||
Session = 0,
|
||||
Selection = 1,
|
||||
Combat = 2,
|
||||
Movement = 3,
|
||||
Chat = 4,
|
||||
Portal = 5,
|
||||
InventoryState = 6,
|
||||
Spellbook = 7,
|
||||
Character = 8,
|
||||
Social = 9,
|
||||
Magic = 10,
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeLifecycleDelta(
|
||||
|
|
@ -176,7 +177,17 @@ public sealed class RuntimeTraceRecorder : IRuntimeEventObserver
|
|||
$"{checkpoint.Actions.InteractionTransactions.AwaitingAppraisalId:X8}:" +
|
||||
$"{checkpoint.Actions.InteractionTransactions.CurrentAppraisalId:X8}:" +
|
||||
$"{checkpoint.Actions.InteractionTransactions.OutboundCount}:" +
|
||||
$"{checkpoint.Actions.InteractionTransactions.PendingPickupToken};" +
|
||||
$"{checkpoint.Actions.InteractionTransactions.PendingPickupToken}:" +
|
||||
$"{checkpoint.Actions.CombatAttack.Revision}:" +
|
||||
$"{(int)checkpoint.Actions.CombatAttack.RequestedHeight}:" +
|
||||
$"{BitConverter.SingleToInt32Bits(checkpoint.Actions.CombatAttack.DesiredPower):X8}:" +
|
||||
$"{BitConverter.SingleToInt32Bits(checkpoint.Actions.CombatAttack.PowerBarLevel):X8}:" +
|
||||
$"{checkpoint.Actions.CombatAttack.BuildInProgress}:" +
|
||||
$"{checkpoint.Actions.CombatAttack.RequestInProgress}:" +
|
||||
$"{BitConverter.SingleToInt32Bits(checkpoint.Actions.CombatAttack.RequestedPower):X8}:" +
|
||||
$"{checkpoint.Actions.Magic.Revision}:" +
|
||||
$"{checkpoint.Actions.Magic.LastRequestedSpellId:X8}:" +
|
||||
$"{checkpoint.Actions.Magic.LastRequestedTargetId:X8};" +
|
||||
$"portal={checkpoint.Portal.Generation}:{(int)checkpoint.Portal.Kind}:" +
|
||||
$"{checkpoint.Portal.DestinationCell:X8}:{checkpoint.Portal.IsReady}")));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,10 +1,29 @@
|
|||
using System.Diagnostics;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.Combat;
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
internal interface ICombatAttackOperations
|
||||
public enum RuntimeInputActivation
|
||||
{
|
||||
Press,
|
||||
Release,
|
||||
}
|
||||
|
||||
public enum RuntimeCombatAttackCommand
|
||||
{
|
||||
LowAttack,
|
||||
MediumAttack,
|
||||
HighAttack,
|
||||
DecreasePower,
|
||||
IncreasePower,
|
||||
AbortForMovement,
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeCombatAttackInput(
|
||||
RuntimeCombatAttackCommand Command,
|
||||
RuntimeInputActivation Activation);
|
||||
|
||||
public interface IRuntimeCombatAttackOperations
|
||||
{
|
||||
bool CanStartAttack();
|
||||
void PrepareAttackRequest();
|
||||
|
|
@ -15,7 +34,8 @@ internal interface ICombatAttackOperations
|
|||
bool AutoRepeatAttack { get; }
|
||||
}
|
||||
|
||||
internal sealed class DelegateCombatAttackOperations : ICombatAttackOperations
|
||||
internal sealed class DelegateRuntimeCombatAttackOperations
|
||||
: IRuntimeCombatAttackOperations
|
||||
{
|
||||
private readonly Func<bool> _canStartAttack;
|
||||
private readonly Action _prepareAttackRequest;
|
||||
|
|
@ -25,7 +45,7 @@ internal sealed class DelegateCombatAttackOperations : ICombatAttackOperations
|
|||
private readonly Func<bool> _playerReadyForAttack;
|
||||
private readonly Func<bool> _autoRepeatAttack;
|
||||
|
||||
public DelegateCombatAttackOperations(
|
||||
public DelegateRuntimeCombatAttackOperations(
|
||||
Func<bool> canStartAttack,
|
||||
Func<AttackHeight, float, bool> sendAttack,
|
||||
Action? prepareAttackRequest,
|
||||
|
|
@ -56,8 +76,8 @@ internal sealed class DelegateCombatAttackOperations : ICombatAttackOperations
|
|||
|
||||
/// <summary>
|
||||
/// Owns retail's basic-combat attack request and power-bar state machine.
|
||||
/// UI buttons and keyboard actions both enter through <see cref="PressAttack"/>
|
||||
/// and <see cref="ReleaseAttack"/>; the wire send remains a host callback.
|
||||
/// Graphical controls and no-window hosts both enter through typed Runtime
|
||||
/// commands; the wire send remains a host callback.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Retail references: <c>ClientCombatSystem::Begin</c> (0x0056A460),
|
||||
|
|
@ -68,7 +88,7 @@ internal sealed class DelegateCombatAttackOperations : ICombatAttackOperations
|
|||
/// <c>SetRequestedAttackHeight</c> (0x0056C8F0), and
|
||||
/// <c>AbortAutomaticAttack</c> (0x0056AE90).
|
||||
/// </remarks>
|
||||
public sealed class CombatAttackController : IDisposable
|
||||
public sealed class RuntimeCombatAttackState : IDisposable
|
||||
{
|
||||
public const double AttackPowerUpSeconds = CombatInputPlanner.AttackPowerUpSeconds;
|
||||
public const double DualWieldPowerUpSeconds = CombatInputPlanner.DualWieldPowerUpSeconds;
|
||||
|
|
@ -76,7 +96,7 @@ public sealed class CombatAttackController : IDisposable
|
|||
public const float DesiredPowerStep = 1f / 6f;
|
||||
|
||||
private readonly CombatState _combat;
|
||||
private readonly ICombatAttackOperations _operations;
|
||||
private readonly IRuntimeCombatAttackOperations _operations;
|
||||
private readonly Func<double> _now;
|
||||
|
||||
private bool _buildInProgress;
|
||||
|
|
@ -91,7 +111,7 @@ public sealed class CombatAttackController : IDisposable
|
|||
private float _latestPowerBarLevel;
|
||||
private bool _disposed;
|
||||
|
||||
public CombatAttackController(
|
||||
public RuntimeCombatAttackState(
|
||||
CombatState combat,
|
||||
Func<bool> canStartAttack,
|
||||
Func<AttackHeight, float, bool> sendAttack,
|
||||
|
|
@ -103,7 +123,7 @@ public sealed class CombatAttackController : IDisposable
|
|||
Func<double>? now = null)
|
||||
: this(
|
||||
combat,
|
||||
new DelegateCombatAttackOperations(
|
||||
new DelegateRuntimeCombatAttackOperations(
|
||||
canStartAttack,
|
||||
sendAttack,
|
||||
prepareAttackRequest,
|
||||
|
|
@ -115,9 +135,9 @@ public sealed class CombatAttackController : IDisposable
|
|||
{
|
||||
}
|
||||
|
||||
internal CombatAttackController(
|
||||
public RuntimeCombatAttackState(
|
||||
CombatState combat,
|
||||
ICombatAttackOperations operations,
|
||||
IRuntimeCombatAttackOperations operations,
|
||||
Func<double>? now = null)
|
||||
{
|
||||
_combat = combat ?? throw new ArgumentNullException(nameof(combat));
|
||||
|
|
@ -134,6 +154,7 @@ public sealed class CombatAttackController : IDisposable
|
|||
public bool AttackRequestInProgress => _attackRequestInProgress;
|
||||
public float RequestedAttackPower => _requestedAttackPower;
|
||||
public bool BuildInProgress => _buildInProgress;
|
||||
public bool IsDisposed => _disposed;
|
||||
|
||||
/// <summary>The level retail publishes to the embedded combat meter.</summary>
|
||||
public float PowerBarLevel => _buildInProgress
|
||||
|
|
@ -142,38 +163,43 @@ public sealed class CombatAttackController : IDisposable
|
|||
|
||||
public event Action? StateChanged;
|
||||
|
||||
public bool HandleInputAction(InputAction action, ActivationType activation)
|
||||
public bool HandleCommand(in RuntimeCombatAttackInput command)
|
||||
{
|
||||
AttackHeight? height = action switch
|
||||
AttackHeight? height = command.Command switch
|
||||
{
|
||||
InputAction.CombatLowAttack => AttackHeight.Low,
|
||||
InputAction.CombatMediumAttack => AttackHeight.Medium,
|
||||
InputAction.CombatHighAttack => AttackHeight.High,
|
||||
RuntimeCombatAttackCommand.LowAttack => AttackHeight.Low,
|
||||
RuntimeCombatAttackCommand.MediumAttack => AttackHeight.Medium,
|
||||
RuntimeCombatAttackCommand.HighAttack => AttackHeight.High,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (height is not null)
|
||||
{
|
||||
if (activation == ActivationType.Press)
|
||||
if (command.Activation == RuntimeInputActivation.Press)
|
||||
PressAttack(height.Value);
|
||||
else if (activation == ActivationType.Release)
|
||||
else if (command.Activation == RuntimeInputActivation.Release)
|
||||
ReleaseAttack();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (activation != ActivationType.Press)
|
||||
if (command.Activation != RuntimeInputActivation.Press)
|
||||
return false;
|
||||
|
||||
if (action == InputAction.CombatDecreaseAttackPower)
|
||||
if (command.Command == RuntimeCombatAttackCommand.DecreasePower)
|
||||
{
|
||||
StepDesiredPower(-1);
|
||||
return true;
|
||||
}
|
||||
if (action == InputAction.CombatIncreaseAttackPower)
|
||||
if (command.Command == RuntimeCombatAttackCommand.IncreasePower)
|
||||
{
|
||||
StepDesiredPower(1);
|
||||
return true;
|
||||
}
|
||||
if (command.Command == RuntimeCombatAttackCommand.AbortForMovement)
|
||||
{
|
||||
AbortAutomaticAttack();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -183,20 +209,6 @@ public sealed class CombatAttackController : IDisposable
|
|||
/// call <c>HandleNewForwardMovement</c>. Jump has its own equivalent cancel
|
||||
/// in <c>ClientCombatSystem::CommenceJump</c> (0x0056AF90).
|
||||
/// </summary>
|
||||
public void HandleMovementInput(InputAction action, ActivationType activation)
|
||||
{
|
||||
if (activation != ActivationType.Press)
|
||||
return;
|
||||
|
||||
if (action is InputAction.MovementForward
|
||||
or InputAction.MovementBackup
|
||||
or InputAction.MovementRunLock
|
||||
or InputAction.MovementJump)
|
||||
{
|
||||
AbortAutomaticAttack();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetDesiredPower(float power)
|
||||
{
|
||||
float value = Math.Clamp(power, 0f, 1f);
|
||||
94
src/AcDream.Runtime/Gameplay/RuntimeCombatModeState.cs
Normal file
94
src/AcDream.Runtime/Gameplay/RuntimeCombatModeState.cs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
public enum RuntimeCombatModeRequestStatus
|
||||
{
|
||||
Inactive,
|
||||
Rejected,
|
||||
Sent,
|
||||
}
|
||||
|
||||
public readonly record struct RuntimeCombatModeRequestResult(
|
||||
RuntimeCombatModeRequestStatus Status,
|
||||
CombatMode Mode,
|
||||
string? Notice = null);
|
||||
|
||||
public interface IRuntimeCombatModeOperations
|
||||
{
|
||||
bool IsInWorld { get; }
|
||||
IReadOnlyList<ClientObject> GetOrderedEquipment();
|
||||
void NotifyExplicitCombatModeRequest();
|
||||
void SendChangeCombatMode(CombatMode mode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Presentation-independent owner for retail's explicit combat-mode command.
|
||||
/// App supplies ordered equipment, transport, and the auto-wield cancellation
|
||||
/// edge; Runtime owns the exact default selection and local state transition.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Port of <c>ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310</c>
|
||||
/// and <c>ClientCombatSystem::ToggleCombatMode @ 0x0056C8C0</c>.
|
||||
/// </remarks>
|
||||
public sealed class RuntimeCombatModeState
|
||||
{
|
||||
private readonly CombatState _combat;
|
||||
private readonly IRuntimeCombatModeOperations _operations;
|
||||
|
||||
public RuntimeCombatModeState(
|
||||
CombatState combat,
|
||||
IRuntimeCombatModeOperations operations)
|
||||
{
|
||||
_combat = combat ?? throw new ArgumentNullException(nameof(combat));
|
||||
_operations = operations
|
||||
?? throw new ArgumentNullException(nameof(operations));
|
||||
}
|
||||
|
||||
public RuntimeCombatModeRequestResult Toggle()
|
||||
{
|
||||
if (!_operations.IsInWorld)
|
||||
{
|
||||
return new RuntimeCombatModeRequestResult(
|
||||
RuntimeCombatModeRequestStatus.Inactive,
|
||||
_combat.CurrentMode);
|
||||
}
|
||||
|
||||
// Every explicit user request supersedes auto-wield settlement,
|
||||
// including a request GetDefaultCombatMode later rejects.
|
||||
_operations.NotifyExplicitCombatModeRequest();
|
||||
|
||||
CombatMode currentMode = _combat.CurrentMode;
|
||||
CombatMode nextMode;
|
||||
if (currentMode != CombatMode.NonCombat)
|
||||
{
|
||||
nextMode = CombatMode.NonCombat;
|
||||
}
|
||||
else
|
||||
{
|
||||
DefaultCombatModeDecision decision =
|
||||
CombatInputPlanner.GetDefaultCombatModeDecision(
|
||||
_operations.GetOrderedEquipment());
|
||||
if (decision.IncompatibleHeldItem is { } held)
|
||||
{
|
||||
string notice =
|
||||
$"You can't enter combat mode while wielding the {held.GetAppropriateName()}";
|
||||
return new RuntimeCombatModeRequestResult(
|
||||
RuntimeCombatModeRequestStatus.Rejected,
|
||||
currentMode,
|
||||
notice);
|
||||
}
|
||||
|
||||
nextMode = CombatInputPlanner.ToggleMode(
|
||||
currentMode,
|
||||
decision.Mode);
|
||||
}
|
||||
|
||||
_operations.SendChangeCombatMode(nextMode);
|
||||
_combat.SetCombatMode(nextMode);
|
||||
return new RuntimeCombatModeRequestResult(
|
||||
RuntimeCombatModeRequestStatus.Sent,
|
||||
nextMode);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,13 @@ using AcDream.Core.Combat;
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Selection;
|
||||
|
||||
namespace AcDream.App.Combat;
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
public interface IRuntimeCombatTargetOperations
|
||||
{
|
||||
bool AutoTarget { get; }
|
||||
uint? SelectClosestTarget();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owns combat-target lifecycle around the shared selection state.
|
||||
|
|
@ -14,25 +20,23 @@ namespace AcDream.App.Combat;
|
|||
/// makes the selected combat object unavailable and enters that same notice
|
||||
/// path.
|
||||
/// </remarks>
|
||||
public sealed class CombatTargetController : IDisposable
|
||||
public sealed class RuntimeCombatTargetState : IDisposable
|
||||
{
|
||||
private readonly CombatState _combat;
|
||||
private readonly SelectionState _selection;
|
||||
private readonly Func<bool> _autoTarget;
|
||||
private readonly Func<uint?> _selectClosestTarget;
|
||||
private readonly IRuntimeCombatTargetOperations _operations;
|
||||
private bool _disposed;
|
||||
public bool IsDisposed => _disposed;
|
||||
|
||||
public CombatTargetController(
|
||||
public RuntimeCombatTargetState(
|
||||
CombatState combat,
|
||||
SelectionState selection,
|
||||
Func<bool> autoTarget,
|
||||
Func<uint?> selectClosestTarget)
|
||||
IRuntimeCombatTargetOperations operations)
|
||||
{
|
||||
_combat = combat ?? throw new ArgumentNullException(nameof(combat));
|
||||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||
_autoTarget = autoTarget ?? throw new ArgumentNullException(nameof(autoTarget));
|
||||
_selectClosestTarget = selectClosestTarget
|
||||
?? throw new ArgumentNullException(nameof(selectClosestTarget));
|
||||
_operations = operations
|
||||
?? throw new ArgumentNullException(nameof(operations));
|
||||
|
||||
_selection.Changed += OnSelectionChanged;
|
||||
}
|
||||
|
|
@ -64,10 +68,10 @@ public sealed class CombatTargetController : IDisposable
|
|||
{
|
||||
if (transition.SelectedObjectId is not null
|
||||
|| transition.Reason == SelectionChangeReason.SessionReset
|
||||
|| !_autoTarget()
|
||||
|| !_operations.AutoTarget
|
||||
|| !CombatInputPlanner.SupportsTargetedAttack(_combat.CurrentMode))
|
||||
return;
|
||||
|
||||
_selectClosestTarget();
|
||||
_operations.SelectClosestTarget();
|
||||
}
|
||||
}
|
||||
161
src/AcDream.Runtime/Gameplay/RuntimeSpellCastState.cs
Normal file
161
src/AcDream.Runtime/Gameplay/RuntimeSpellCastState.cs
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
using System;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
public interface IRuntimeSpellCastOperations
|
||||
{
|
||||
uint LocalPlayerId { get; }
|
||||
bool CanSend { get; }
|
||||
bool HasRequiredComponents(uint spellId);
|
||||
bool IsTargetCompatible(
|
||||
uint targetId,
|
||||
SpellMetadata spell,
|
||||
bool showMessage);
|
||||
void StopCompletely();
|
||||
void SendUntargeted(uint spellId);
|
||||
void SendTargeted(uint targetId, uint spellId);
|
||||
void DisplayMessage(string message);
|
||||
void IncrementBusy();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime owner for retail cast intent. It validates the canonical spellbook
|
||||
/// and selection, stops movement, and emits exactly one targeted or untargeted
|
||||
/// request. ACE owns every gameplay result after that boundary.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Port of <c>ClientMagicSystem::CastSpell</c> (0x00568040) and
|
||||
/// <c>FreeHandsAndCastSpell</c> (0x00566EF0). This controller deliberately
|
||||
/// does not consume mana/components or start local effects.
|
||||
/// </remarks>
|
||||
public sealed class RuntimeSpellCastState
|
||||
{
|
||||
private readonly Spellbook _spellbook;
|
||||
private readonly SelectionState _selection;
|
||||
private readonly IRuntimeSpellCastOperations _operations;
|
||||
|
||||
public RuntimeSpellCastState(
|
||||
Spellbook spellbook,
|
||||
SelectionState selection,
|
||||
IRuntimeSpellCastOperations operations)
|
||||
{
|
||||
_spellbook = spellbook ?? throw new ArgumentNullException(nameof(spellbook));
|
||||
_selection = selection ?? throw new ArgumentNullException(nameof(selection));
|
||||
_operations = operations ?? throw new ArgumentNullException(nameof(operations));
|
||||
}
|
||||
|
||||
public uint? LastRequestedSpellId { get; private set; }
|
||||
public uint? LastRequestedTargetId { get; private set; }
|
||||
public event Action? StateChanged;
|
||||
|
||||
public bool IsTargetReady(uint spellId)
|
||||
{
|
||||
if (!_spellbook.Knows(spellId)
|
||||
|| !_spellbook.TryGetMetadata(spellId, out SpellMetadata spell))
|
||||
return false;
|
||||
if (spell.IsSelfTargeted || spell.IsUntargeted || spell.TargetMask == 0u)
|
||||
return true;
|
||||
return _selection.SelectedObjectId is uint target and not 0u
|
||||
&& _operations.IsTargetCompatible(
|
||||
target,
|
||||
spell,
|
||||
showMessage: false);
|
||||
}
|
||||
|
||||
public CastRequestResult Cast(uint spellId)
|
||||
{
|
||||
if (!_spellbook.Knows(spellId) || !_spellbook.TryGetMetadata(spellId, out SpellMetadata spell))
|
||||
{
|
||||
_operations.DisplayMessage("You do not know that spell.");
|
||||
return CastRequestResult.UnknownSpell;
|
||||
}
|
||||
|
||||
if (!_operations.HasRequiredComponents(spellId))
|
||||
{
|
||||
_operations.DisplayMessage(
|
||||
"You do not have all of this spell's components.");
|
||||
return CastRequestResult.MissingComponents;
|
||||
}
|
||||
|
||||
uint? target;
|
||||
bool untargeted;
|
||||
if (spell.IsSelfTargeted)
|
||||
{
|
||||
uint playerId = _operations.LocalPlayerId;
|
||||
target = playerId == 0 ? null : playerId;
|
||||
untargeted = target is null;
|
||||
}
|
||||
else if (spell.IsUntargeted || spell.TargetMask == 0)
|
||||
{
|
||||
target = null;
|
||||
untargeted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
target = _selection.SelectedObjectId;
|
||||
untargeted = false;
|
||||
if (target is null or 0)
|
||||
{
|
||||
_operations.DisplayMessage(
|
||||
"You must select a suitable target.");
|
||||
return CastRequestResult.NoTarget;
|
||||
}
|
||||
if (!_operations.IsTargetCompatible(
|
||||
target.Value,
|
||||
spell,
|
||||
showMessage: true))
|
||||
return CastRequestResult.IncompatibleTarget;
|
||||
}
|
||||
|
||||
if (!_operations.CanSend)
|
||||
{
|
||||
_operations.DisplayMessage("You cannot cast a spell right now.");
|
||||
return CastRequestResult.Unavailable;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_operations.StopCompletely();
|
||||
LastRequestedSpellId = spellId;
|
||||
LastRequestedTargetId = target;
|
||||
if (untargeted)
|
||||
_operations.SendUntargeted(spellId);
|
||||
else
|
||||
_operations.SendTargeted(target!.Value, spellId);
|
||||
// FreeHandsAndCastSpell @ 0x00566EF0 increments the shared UI
|
||||
// busy reference only after Event_Cast has been emitted. The
|
||||
// matching server UseDone event decrements it.
|
||||
_operations.IncrementBusy();
|
||||
}
|
||||
catch
|
||||
{
|
||||
LastRequestedSpellId = null;
|
||||
LastRequestedTargetId = null;
|
||||
throw;
|
||||
}
|
||||
StateChanged?.Invoke();
|
||||
return CastRequestResult.Sent;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
bool changed = LastRequestedSpellId is not null
|
||||
|| LastRequestedTargetId is not null;
|
||||
LastRequestedSpellId = null;
|
||||
LastRequestedTargetId = null;
|
||||
if (changed)
|
||||
StateChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
public enum CastRequestResult
|
||||
{
|
||||
Sent,
|
||||
UnknownSpell,
|
||||
NoTarget,
|
||||
IncompatibleTarget,
|
||||
MissingComponents,
|
||||
Unavailable,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue