acdream/src/AcDream.Runtime/Gameplay/RuntimeCombatModeState.cs
Erik 20df9d155d 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>
2026-07-26 11:56:40 +02:00

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);
}
}