A rule-based automation addon for Final Fantasy XI (Windower). Define conditional "gambits" that automatically execute actions when a set of conditions are met — similar to the AI programming systems found in some other RPGs. Designed for multi-boxing, support automation, and intelligent behavior chains.
- Place the
Gambitfolder in your Windoweraddons/directory. - Load the addon:
//lua load Gambit - To auto-load, add
lua load Gambitto yourWindower/scripts/init.txt.
//gambit start Enable gambits
//gambit stop Pause all gambits
//gb start Shorthand versions work too
//gb stop
//gb aura List active aura suppressions
//gb aura reset Clear all aura suppressions
//gb txt <speaker> <text...>
Gambits begin processing as soon as start is issued. Use stop to pause without unloading.
//gb txt injects (Speaker) text as if it arrived in party chat — test
chat-driven gambits without a second character (e.g.
//gb txt Bob keep rolling chaos).
Gambits are defined in Lua files inside the stacks/ folder. The addon searches for a config file in this priority order (first match wins):
| Priority | Filename |
|---|---|
| 1 | CharacterName_JOB.lua (e.g. Bob_COR.lua) |
| 2 | CharacterName-JOB.lua |
| 3 | CharacterName_Corsair.lua (full job name) |
| 4 | CharacterName-Corsair.lua |
| 5 | CharacterName.lua |
| 6 | COR.lua (job-only template) |
| 7 | Corsair.lua |
| 8 | default.lua |
Start by copying stacks/test_gambits.lua and renaming it to match your character and job.
-- stacks/MyChar_WHM.luaregistered_gambits=flatten{
-- Put your gambits here
}The only required export is the registered_gambits table. No includes are
needed: every condition and helper is injected by the engine as a bare global
name (is_engaged, tp_above, chat_match_buff, handle_cures, gambit,
when...). The full registry also remains reachable as Gambits.* for
back-compat.
Every gambit is built around a trigger type that determines when it fires.
Every gambit is a condition list plus an action. Fires when all listed conditions pass (AND logic).
gambit{
condition1,
condition2,
condition3,
action=use_command("Savage Blade", "t"),
}Conditions are evaluated left to right; evaluation stops at the first failure.
action is required — a gambit without one fails at load, not mid-fight.
(Gambits.multi_condition_trigger.cond({...}, action) is the underlying
implementation and still works.)
Earlier trigger modules (hp_below_trigger, hpp_below_trigger,
tp_trigger, ja_recast_ready, ma_recast_ready, chat_trigger) predate
the realization that everything is a condition list + action. They still work
via Gambits.*, but each is expressed more flexibly as a gambit{}:
| Legacy trigger | gambit{} equivalent |
|---|---|
hp_below_trigger(500, act) | gambit{ hp_below(500), action = act } |
hpp_below_trigger(70, act) | gambit{ hpp_below(70), action = act } |
tp_trigger(1000, act) | gambit{ tp_above(1000), action = act } (or when "tp >= 1000") |
ja_recast_ready("X", act) | gambit{ ja_recast_ready("X"), action = act } |
ma_recast_ready("X", act) | gambit{ ma_recast_ready("X"), action = act } |
chat_trigger("msg", act) | gambit{ chat_match("msg"), action = act } |
timed_input (repeating timer commands) has no condition equivalent and
remains available: Gambits.timed_input.cond(command, start_delay, repeat_delay).
Conditions are bare global names — no imports. The name is the condition
module's registry key with any trailing _cond dropped (tp_above_cond →
tp_above, chat_match_cond → chat_match).
gambit{
is_engaged(),
tp_above(1000),
action=use_command("Savage Blade", "t"),
}See the Conditions Reference for all available conditions.
For literal commands use use_command; to execute the current decision (see below) use do_action. Both are available globally (no require needed).
use_command(command, target)| Target value | In-game target |
|---|---|
"me" or "self" | <me> |
"t" or "target" | <t> |
nil | Default target for the spell/ability |
use_command("Cure IV", "me") -- /ma "Cure IV" <me>use_command("Savage Blade", "t") -- /ws "Savage Blade" <t>use_command("Berserk", nil) -- /ja "Berserk"do_action() -- perform the decided action on the decided targetqueue_commands(B"buff_casts") -- run a match-time-decided command list, one per GCDThe decision is how Multi-Condition Triggers pass data forward. Selector conditions fill decision = {action, kind, target} — the action name, what kind of action it is ("spell", "ja", "ws", "item"), and who it is aimed at. Later conditions read the decision, and do_action() executes it.
Example: Cast the appropriate buff when asked in chat
gambit{
-- Matches any known buff name in chat and sets the decision to the-- best available spell for it, targeted at the speaker. Pass nil to-- listen to any speaker, or pass "Bob" to only listen to Bob.chat_match_buff(nil),
-- Checks that the decided action can be performed right now (recast-- ready, enough MP, correct job level, not silenced, in range, etc.)-- With no argument, can_use always checks the decision.can_use(),
-- Perform the decided action on the decided target (the speaker)action=do_action(),
}Dynamic arguments: every condition argument may be a literal or a function(params) -> value, evaluated at check time. Three helpers cover the common cases:
| Helper | Reads | Example |
|---|---|---|
G"name" | a global set with set_global_condition | decide_spell(G"geo_spell") |
D"field" | a field of the current decision | buff_active(D"action", 3) |
B"key" | chat-command captures (chat_command<name> slots) | set_global_condition("geo_spell", B"buff") |
Decision-setting conditions:
| Condition | What it decides |
|---|---|
chat_match_buff | Best available spell for the buff mentioned; target = speaker |
chat_match_heal | Best available heal spell mentioned; target = speaker |
decide_spell | Set the action to a specific spell |
decide_target | Set the target ("me", "t", a name, or G"var") |
decide_item | Set the action to an item (e.g. decide_item("Remedy", "me")) |
decide_leaders_target | Target = leader's current target |
decide_geo_target | Geo-style target: leader's target for offense, leader for buffs |
decide_ability | Set the action to a job ability (target left for whoever filled it) |
decide_rune | Set the action to the rune JA for an element |
decide_highest_tier | Set the action to the highest castable tier of a buff |
decide_unclaimed_target | Target = nearest unclaimed mob from a name list |
decision_target_is / decision_target_is_not | Gate on who the decided target is |
A gambit's condition list is an implicit AND. For OR and NOT, wrap conditions in combinators — each returns a normal condition, so they nest freely (and/or/not are Lua keywords, hence the names; all_/any_/and_/or_ are aliases of all/any):
-- "the leader is fighting": engaged, OR staging a never-engaged bosslocalleader_fighting=any{
leader_is_engaged(G"leader"),
leader_target_is(G"leader", S{"Aminon"}),
}
gambit{
buff_not_active("Copy Image (3)", 1),
any{
is_engaged(),
all{
check_global_equals("pull_mode", true, false),
not_(leader_is_engaged(G"leader")),
},
},
action=use_command("Utsusemi:Ni", "me"),
}Rules: use combinators for gates only (keep decide_* selectors in the flat list); all{}/any{} short-circuit left to right, so put cheap conditions first. Edge-triggered conditions compose correctly: any{} commits state only for the child that passed, not_() never commits the wrapped condition.
when "expr" builds a condition from a comparison over scalar game words. The
string is compiled once at stack load and evaluated every tick; an unknown
word or syntax error fails at load with the known-word list, so a typo can
never reach a fight.
Vocabulary: tp, hp, hpp, mp, mpp, pet_hpp, target_hpp — scalars only, by
design. Boolean world-state (engagement, pet existence...) stays in named
conditions, composed in the gambit list or with all{}/any{}/not_.
and/or/not inside a string are expression syntax over scalars.
gambit{ is_engaged(), when"mp <= 500 and tp >= 1000", action=... },
gambit{ when"tp >= 500 and hpp <= 60", action=... },
any{
when"hpp <= 30",
all{ is_engaged(), when"mpp <= 10" },
}Exact equivalents of the named threshold conditions (mind the boundaries —
the named library is inconsistent): tp_above(1000) = when "tp >= 1000"
and the *_below vitals are inclusive (hpp_below(60) =
when "hpp <= 60"), while pet/target hpp are strict
(pet_hpp_below(50) = when "pet_hpp < 50"). pet_hpp/target_hpp read as
false when the pet or target is absent, matching the named conditions.
Some mobs project auras: a debuff re-applied every second or two that no cure can remove (Erase completes with "no effect"). Without protection the cleanse gambits would burn every GCD against it. The engine detects and suppresses these automatically:
- Party members: when your status cure completes and removes NOTHING it could address, every surviving addressable debuff on that target is convicted. Casts that remove something never convict, so multi-effect Erase works down real debuffs first and the aura costs exactly ONE wasted cast. Yagrush AoE cures convict several members from a single splash; two convicted members make it party-wide.
- Self: your own buff packet shows expiry timestamps -- an effect whose expiry keeps advancing without ever dropping is convicted with zero casts.
- A convicted (member, debuff) reads as absent to every cleanse gambit and the downtime sweep. Explicit chat heal requests bypass suppression.
- Doom is exempt: Cursna's doom removal is chance-based, so doom surviving a cast is normal attrition — it never convicts and Cursna keeps being cast for as long as doom is up.
- Lifecycle: suppression holds while the debuff stays present (15s
grace for people moving out of range), re-earns itself every 90s with one
probe, clears on zone change, and announces once per detection ("Aura
detected: ..." -- set the global
aura_announceto false to silence). //gb auralists active suppressions;//gb aura resetclears them.
Gambits can react to what players and monsters DO -- spells landing, job abilities and weaponskills firing, monsters readying TP moves and starting casts. Action packets are normalized onto a short-lived queue with the same consumption rules as chat: a matched packet is consumed even when the response was blocked -- no retries -- and unheard packets are dropped after 5 seconds. Reactions only get the action slot on ticks that chat and state gambits left free, so on a busy tick (GCD, mid-cast) a packet can be heard a few seconds after the event.
| Matcher | Fires when |
|---|---|
on_cast(what, opts) | a party member's spell LANDED |
on_ability(what, opts) | a party member's job ability went off ("voke" resolves via ja_requests) |
on_weaponskill(what, opts) | a party member's weaponskill landed |
on_mob_ability(what, opts) | a monster's TP move LANDED |
on_readies(what, opts) | a monster BEGAN READYING a TP move -- the stun/defense hook |
on_mob_starts_casting(what, opts) | a monster STARTED CASTING a spell -- the interrupt hook (interrupted casts never match) |
The what argument, all matchers: nil matches anything of that kind;
a set or list (S{"Shock", "Thunder IV"}) matches exactly those
names, nothing else; a string tries the family tables first
("dia" = every Dia tier, "fire" = every single-target Fire tier) and
falls back to the literal name. When an exact spell is what you mean, use
the set form.
opts.by narrows the actor: player matchers default to any party member
(trusts included); pass "me", a name, or a set. Mob matchers accept a
mob name or set.
Every matcher fills the decision target (by id) plus B"actor" /
B"what" scratch, so a decide_spell / decide_ability + can_use +
do_action completes the response. LANDED matchers target whoever the
packet says was hit; the START matchers (on_readies,
on_mob_starts_casting) target the acting mob instead, so a stun
response needs no decide_target:
-- when Joe lands any Dia tier on a mob, Light Shot that mobgambit{
on_cast("dia", { by="Joe" }),
decide_ability("Light Shot"),
can_use(),
action=do_action(),
},
-- when Kam'lanaut starts casting Dispelga, Stun himgambit{
on_mob_starts_casting("Dispelga", { by="Kam'lanaut" }),
decide_ability("Stun"),
can_use(),
action=do_action(),
},Repeated gambit families (presets, toggles, WS-by-target rules...) live as data tables at the top of a stack, expanded into gambits at one position in the list. Two engine helpers make this work:
flatten{...}— buildsregistered_gambitsfrom a list whose entries are either single gambits or lists of gambits; lists expand in place, so priority stays positional.expand(table, factory)— one gambit per data row. Positional rows{a, b, c}callfactory(a, b, c); keyed rows{ja = "Pflug"}and plain strings pass through whole asfactory(row).
localtoggles= {
{ "joe do hastes", "hastes", true },
{ "joe stop hastes", "hastes", false },
}
localfunctiontoggle(phrase, global, value)
returngambit{
chat_match(phrase, nil),
action=set_global_condition(global, value),
}
endregistered_gambits=flatten{
some_gambit,
expand(toggles, toggle), -- all toggles expand here, in table orderanother_gambit,
}Adding a row to the table is the whole edit — no new gambit to write. When a
family's priorities interleave with other gambits (WHM's cleanses), call the
factory inline at each position instead of using one expand(). See
stacks/GEO.lua (sets/profiles), stacks/WAR.lua (WS rules), and
stacks/WHM.lua (both styles) for worked examples.
All of these are also expressible as when-strings (see Scalar Checks).
| Condition | Description |
|---|---|
hp_below(amount) | Player HP <= amount |
hpp_below(percent) | Player HP% <= percent |
mp_below(amount) | Player MP <= amount |
mpp_below(percent) | Player MP% <= percent |
tp_above(amount) | Player TP >= amount |
tp_below(amount) | Player TP <= amount |
target_hpp_below(percent) | Current target HP% < percent |
pet_hpp_below(percent) | Pet HP% < percent |
pet_hpp_above(percent) | Pet HP% > percent |
| Condition | Description |
|---|---|
buff_active(buff_name, count) | Player has buff (count = stacks, default 1) |
buff_not_active(buff_name, count) | Player does NOT have buff |
buff_active_on_party_member(name, buff_name, count) | Party member has buff |
buff_not_active_on_party_member(name, buff_name, count) | Party member lacks buff |
is_buff_missing_on_party_member(names_set, buff_name) | Any listed member lacks the buff; decides that member as target |
debuff_active_on_player(debuff_set, player_name) | Named player has one of the debuffs; decides the cure |
debuff_active_on_party_member(debuff_set) | Any party member has one of the debuffs; decides member + cure |
| Condition | Description |
|---|---|
ja_recast_ready(ability_name) | Job ability is off cooldown (charge-aware for Quick Draw) |
ma_recast_ready(spell_name) | Spell is off cooldown |
can_use(action, cast_while_moving) | Can the action be performed now: level, recast, MP, status effects, movement, range, and zone restrictions (item kind checks inventory). With no argument it checks the current decision. |
can_use_ability(ability_name) | Job ability usable now (knows it, recast/charges ready). With no argument it checks the current decision. |
| Condition | Description |
|---|---|
is_engaged() | Player is in combat (and the target is claimed) |
is_not_moving() | Player is not moving |
is_in_range(spell_name) | Target is in range for spell |
is_target_valid() | The decided target is in range of the decided spell |
leader_is_engaged(leader) | Leader is in combat (pass a name or G"leader") |
leader_is_not_engaged(leader) | Leader is NOT in combat |
| Condition | Description |
|---|---|
target_is(name_or_set, invert) | Target name matches (pass a set S{...} for multiple names; set invert=true to negate) |
leader_target_is(leader, name_or_set) | Leader's target matches |
target_name_changed(watched_name, uuid) | Named player's target changed since this gambit last fired (edge trigger; unique uuid per gambit, shared uuid = shared edge) |
leader_target_changed(leader, uuid) | Leader's target changed (by index) since this gambit last fired |
leader_target_name_changed(leader, uuid) | Leader's target changed (by name) since this gambit last fired |
| Condition | Description |
|---|---|
has_pet() | Player has an active pet |
does_not_have_pet() | Player has no pet |
pet_in_range_to_target(leader, range, use_leader_target) | Pet is within range of the (leader's) target |
pet_not_in_range_to_target(leader, range, use_leader_target) | Pet is NOT within range |
| Condition | Description |
|---|---|
chat_match(text, speaker) | Exact chat match. Pass nil for speaker to match anyone. |
chat_command(pattern, speaker) | Whole-command pattern: literal words + <name> captures into B"name" (chat_command("set entrust <buff>")). Typed captures canonicalize and validate the word: <buff> (gd.buffs + picker), <spell> (the "cast" offense grammar), <ja> (gd.ja_requests keywords); <name:type> gives a typed capture its own key ("swap <first:buff> <second:buff>"). Any other <name> captures the raw word. Parsed and validated at load. |
chat_match_buff(speaker) | Chat contains a known buff name; decides the best spell. Bare requests are distance-aware: the self-AoE tier when the requester stands within 10 (cast on self), the single-target tier when farther. "buff " / "buff t" explicit targets always use the single-target tier. |
chat_match_buffs(speaker) | ADDRESSED multi-buff request ("joe shell protect str"): every word must resolve as a buff keyword (joins allowed), two-buff minimum, each aimed at the speaker with the same distance rule; uncastable words get one combined /p line. Pair with chat_strip_my_name(true) and action = queue_commands(B"buff_casts"). |
chat_match_heal(speaker) | Chat contains a known heal spell |
chat_match_ja(speaker) | Chat contains a ja_requests keyword ("light shot", "voke"); enemy JAs target the speaker's current target, player JAs target the speaker. Only characters that have the ability respond. |
chat_strip_my_name(required) | Address router: strips this character's name when the message starts with it ("joe haste" → "haste" on Joe only), so a request can be routed to one character. Place FIRST, before chat matchers. Pass-through by default (bare requests still work for everyone); chat_strip_my_name(true) passes only when addressed. |
chat_match_offense(speaker) | "cast <key>" → offensive spell at the speaker's current target (recast/MP-aware tier pick). Keys: family word = highest tier (cast fire, cast firaga, cast blizzara, cast dia); force a tier with fire3/fireiii/f3, fg1–fg3 (+fg4 = Firaja), br1–br3 (GEO -ra), dia2 etc. The "cast" gate keeps bare words ("dia", "sleep") on their status-cure meaning. |
| Condition | Description |
|---|---|
main_weapon_type(type) | Main hand weapon type matches ("Sword", "Great Axe", ...) |
main_weapon_name(name) | Main hand weapon name matches exactly |
Useful for coordinating behavior across multiple gambits.
-- In an action, set a global variable:set_global_condition("mode", "healing")()
-- In a condition, check it:check_global_equals("mode", "healing") -- true when equalcheck_global_equals("mode", "healing", true) -- true when NOT equal (inverted)Ensures a gambit only fires once per combat encounter. Requires a unique UUID string to track state.
once_per_fight("550e8400-e29b-41d4-a716-446655440000")Generate any unique string — you can use an online UUID generator or just make up a unique identifier.
An intelligent healing system for WHM/support characters. Rather than casting a fixed spell, it surveys the party, picks the optimal target and cure tier, and handles AoE vs. single-target decisions automatically.
registered_gambits=flatten{
handle_cures(),
}Behavior:
- AoE cure (Curaga) when multiple party members are critically low
- Critical members (<= 55%) always outrank non-critical ones; ties break to lowest absolute HP
- Alliance members are cured single-target only, and only when critical
- Falls back to
Full Curewhen caster MP is below 80 - Requires WHM main job or subjob for Curaga access
Set the global cure_alliance to false for party-only healing (unset defaults to alliance critical-only):
set_global_condition("cure_alliance", false)()A chat-driven song system for Bard. Party members issue commands in party chat to queue songs, which the BRD character then casts automatically. Handles Pianissimo for off-self targets, dummy song insertion, and automatic re-cast timing.
registered_gambits=flatten{
-- Pass the name of a global variable that holds the leader's name (or nil for anyone).-- Only that player's chat commands will be processed.handle_bard_songs("leaderName"),
}The leader_var argument is the name of a global variable (set via set_global_condition) that contains the leader's character name or a set of names. Pass nil to respond to any speaker.
These globals control behavior and should be set in your config or via a gambit before using bard songs:
| Variable | Type | Description |
|---|---|---|
useJA | boolean | Whether ja in a sing command actually uses Nightingale/Troubadour/Marcato |
use1Hr | boolean | Whether 1hr in a sing command actually uses Clarion Call + Soul Voice |
hasHonorMarch | boolean | Set to true if the BRD has access to Honor March (instrument-gated) |
hasAria | boolean | Set to true if the BRD has access to Aria of Passion |
do_dummy_songs | boolean | Whether to insert dummy songs between real songs to push old buffs off |
dummy_song_one | string | Spell name for the 2nd-slot dummy song |
dummy_song_two | string | Spell name for the 3rd-slot dummy song |
dummy_song_three | string | Spell name for the 4th-slot dummy song |
All commands are spoken in party chat.
Sing songs once:
sing [songs...] [target] [ja] [1hr]
Queues up to 5 songs for a single cast cycle.
Keep singing (auto-repeat):
keep singing [songs...] [target] [ja] [1hr]
Queues songs and automatically re-casts them before they expire. The system calculates recast timing from song duration + gear bonus. Max 4 songs (5 with 1hr).
Stop all songs:
clear songs
Clears both the active cast queue and any repeating song cycles.
Check what's queued:
which songs
Prints each active repeating song cycle to party chat, including what songs are in it, the target, and time until next cast.
| Parameter | Description |
|---|---|
Song keyword (e.g. march, minuet) | The buff type to sing. Automatically selects the highest available tier. Case-insensitive. |
Character name (e.g. Bob) | Cast songs on that player instead of self (uses Pianissimo automatically). |
ja | Prepend Nightingale, Troubadour, and Marcato before singing (requires useJA = true). |
1hr | Use Clarion Call and Soul Voice before singing (requires use1Hr = true). |
Multiple songs and modifiers can be combined in any order:
keep singing march minuet ja Bob
→ Use Nightingale/Troubadour/Marcato, then Pianissimo + highest March tier on Bob, then Pianissimo + highest Minuet tier on Bob. Auto-repeats.
sing ballad madrigal 1hr
→ Clarion Call + Soul Voice, then Ballad and Madrigal on self. One time only.
When a BRD needs to overwrite old songs (e.g. to land 2× March), dummy songs fill the intermediate slots to push the old buff off the target's song list. Set do_dummy_songs = true and configure the dummy spell names:
set_global_condition("do_dummy_songs", true)()
set_global_condition("dummy_song_one", "Army Paeon")()
set_global_condition("dummy_song_two", "Army Paeon II")()
set_global_condition("dummy_song_three", "Army Paeon III")()A chat-driven roll system for Corsair: party members request rolls in party chat, and the COR executes the full sequence — Crooked Cards, Phantom Roll, Double-Up decisions, and Snake Eye — automatically.
registered_gambits=flatten{
-- Same convention as handle_bard_songs: pass the NAME of a global-- variable holding the leader's name (or a set of names) to only obey-- that player, or nil to obey anyone. Own commands are always accepted.handle_roll(nil),
}All commands are spoken in party chat.
roll <roll> [<roll2>] [cc] one-shot: roll one or two rolls
keep rolling <roll> [<roll2>] [cc] auto-repeat: re-roll whenever a
tracked roll buff drops
clear rolls stop repeating and flush the queue
- Roll keywords resolve through
gd.rolls(gambits/gambit_defines.lua), which knows every roll plus aliases: job shorthand (drk= Chaos,whm= Healer's,rng= Hunter's) and effect words (att= Chaos,acc= Hunter's,exp= Corsair's,cure= Healer's). Each entry carries the roll's lucky/unlucky numbers. ccqueues Crooked Cards before the first roll (skipped gracefully if on cooldown).- A request is ignored while the asked-for buffs are already up or a roll sequence is mid-flight.
keep rolling chaos hunters cc
→ Crooked Cards, Chaos Roll (+ Double-Ups), then Hunter's Roll (+ Double-Ups), re-rolling each whenever its buff drops.
Decided per result from the roll's lucky/unlucky numbers:
| Result | Action |
|---|---|
| lucky or 11 | stop (11 is the max; doubling risks bust) |
| 10, Snake Eye ready | Snake Eye → Double-Up (guaranteed 11) |
| below 6 | Double-Up (cannot bust from < 6) |
| unlucky, Snake Eye ready | Snake Eye to escape → Double-Up |
| anything else | stop; a bust just advances to the next roll |
Double-Up and Snake Eye are gated on the "Double-Up Chance" window: if the window expires with follow-ups still queued, the slot finalizes at the total it landed on — at most one wasted attempt, never a retry loop.
Rolling pauses (and resumes when clear) under silence, sleep, terror, petrify, stun, charm, mute, Omerta, amnesia, and impairment, while mounted, and while moving. Two textboxes show the pending action queue and the repeat status per roll.
registered_gambits=flatten{
gambit{ is_engaged(), ja_recast_ready("Berserk"), action=use_command("Berserk", "me") },
gambit{ is_engaged(), ja_recast_ready("Warcry"), action=use_command("Warcry", "me") },
gambit{ is_engaged(), ja_recast_ready("Aggressor"), action=use_command("Aggressor", "me") },
}registered_gambits=flatten{
gambit{
hpp_below(50),
action=use_command("Cure IV", "me"),
},
}registered_gambits=flatten{
handle_roll(nil),
}Then in party chat: keep rolling chaos hunters cc — or test without a
party from the COR's own console: //gb txt Bob roll corsair. See
Corsair Roll Automation for the
full command set and Double-Up strategy.
registered_gambits=flatten{
gambit{
chat_match_buff(nil),
can_use(),
action=do_action(),
},
}registered_gambits=flatten{
-- Only use Garland of Bliss to build Aftermath: Lv.3 (don't waste if already up)gambit{
is_engaged(),
tp_above(3000),
buff_not_active("Aftermath: Lv.3", 1),
action=use_command("Garland of Bliss", "t"),
},
}registered_gambits=flatten{
gambit{
is_engaged(),
tp_above(100),
is_in_range("Box Step"),
target_is(S{ "BossA", "BossB", "BossC" }, false),
ja_recast_ready("Box Step"),
once_per_fight("a1b2c3d4-e5f6-7890-abcd-ef1234567890"),
action=use_command("Box Step", "t"),
},
}- Order matters: Conditions are checked left to right. Put cheap/fast checks (like
is_engaged) before expensive ones (like range checks). - GCD: After an action fires, the cooldown before gambits run again depends on what was used. The addon intercepts the outgoing packet to set the delay: spell casts use ~3.1 seconds, job abilities and weaponskills use ~1.2 seconds.
- Multiple gambits: The addon processes the
registered_gambitslist in order. The first gambit whose conditions all pass will execute, then the GCD kicks in. - UUIDs for
once_per_fight: Each uniqueonce_per_fightcondition needs its own UUID so different gambits can independently track their per-fight state. - Testing: Use the commented-out examples in
stacks/test_gambits.luaas a reference when building new gambits.