Combo State Machine
Combo systems map naturally onto Altruist's generic State Machine core. A combo state usually needs three things:
- timing windows such as
damage,input, andcancel - optional duration scaling per entity, such as attack speed
- optional motion shaping during the attack
Altruist's generic FSM already gives you those pieces through state windows, OnStateEnter, and StateMotionProfile.
Combo Context
public sealed class ComboContext : IStateContext
{
public ITypelessWorldObject Entity { get; set; } = null!;
public float TimeInState { get; set; }
public float StateDuration { get; set; }
public string CurrentStateTag { get; set; } = "";
public IReadOnlyDictionary<string, StateWindow>? ActiveWindows { get; set; }
public object? CurrentStateData { get; set; }
public StateMotionProfile? CurrentStateMotion { get; set; }
public float PreviousProgress { get; set; }
public float AttackSpeedMultiplier { get; set; } = 1f;
public bool QueuedNextAttack { get; set; }
public bool DamageAppliedThisSwing { get; set; }
}
Example Combo Definition
[StateBehavior("sword_combo")]
public sealed class SwordComboDefinition
{
[State("attack1", Initial = true)]
public string? Attack1(ComboContext ctx, float dt)
{
if (!ctx.DamageAppliedThisSwing && ctx.WindowEntered("damage"))
{
DealDamage(ctx);
ctx.DamageAppliedThisSwing = true;
}
if (ctx.InWindow("input") && ctx.QueuedNextAttack)
return "attack2";
if (ctx.Progress >= 1f)
return "recover";
return null;
}
[StateEnter("attack1")]
public void Attack1Enter(ComboContext ctx)
{
ctx.DamageAppliedThisSwing = false;
}
}
Build-Time Configuration
var def = new StateMachineBuilder<ComboContext>()
.RegisterHandlers<SwordComboDefinition>()
.ConfigureState("attack1")
.Duration(0.55f)
.Window("damage", 0.35f, 0.55f)
.Window("input", 0.60f, 1.00f)
.Window("cancel", 0.80f, 1.00f)
.Motion(new StateMotionProfile
{
MovementThrottle = 0.20f,
MotionScale = 1f,
X = new StateMotionCurve
{
Keys =
[
new StateMotionKey { TimeN = 0f, Value = 0f },
new StateMotionKey { TimeN = 0.4f, Value = 40f },
new StateMotionKey { TimeN = 1f, Value = 0f },
]
}
})
.ConfigureState("recover")
.Duration(0.20f)
.OnStateEnter((ctx, _, baseDuration) =>
{
ctx.StateDuration = baseDuration / Math.Max(0.5f, ctx.AttackSpeedMultiplier);
})
.Build();
How to Read the Pattern
damagewindow decides when hit application is validinputwindow decides when combo chaining is acceptedcancelwindow decides when another system may interrupt or branchOnStateEnterscales authored timing per entityStateMotionProfilelets you describe forward lunge or throttle without baking it into every handler
This keeps combo logic declarative: authored timing and motion live in state metadata, while the handler focuses on the transition rules.