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>
94 lines
3 KiB
C#
94 lines
3 KiB
C#
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);
|
|
}
|
|
}
|