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, and cancel
  • 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

  • damage window decides when hit application is valid
  • input window decides when combo chaining is accepted
  • cancel window decides when another system may interrupt or branch
  • OnStateEnter scales authored timing per entity
  • StateMotionProfile lets 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.