> ## Documentation Index
> Fetch the complete documentation index at: https://runelite.zip/llms.txt
> Use this file to discover all available pages before exploring further.

# Combat, prayer, and magic

> Combat settings, prayer convergence, the dwarf multicannon, spell casting, and tick decisions.

Combat writes split across three action classes: `CombatActions` for combat settings, `PrayerActions` for prayers, and `MagicActions` for spells. Target discovery belongs to the query helpers; attack dispatch goes through `NPCActions.interact(...)`.

## Combat settings

All methods return `InteractionResult`. Attack-style and auto-retaliate changes require the Combat tab to be visible; a missing prerequisite returns `WIDGET_HIDDEN` targeting the tab and never selects it. The special-attack orb and quick-prayer orb operations are tab-independent.

| Method                                       | Behavior                                                                                     |
| -------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `CombatActions.toggleSpec()`                 | Clicks the visible special-attack orb; `COMBAT_SPEC_UNAVAILABLE` when the orb is unavailable |
| `CombatActions.setAttackStyle(AttackStyle)`  | Clicks the combat-tab button for the style; `setAttackStyle(null)` returns `TARGET_NULL`     |
| `CombatActions.toggleAutoRetaliate(boolean)` | Idempotently converges auto-retaliate                                                        |

```java theme={null}
InteractionResult result = CombatActions.toggleAutoRetaliate(true);
if (result.failed() && result.getStatus() != InteractionStatus.PACED) {
    log.debug("Auto-retaliate update failed: {}", result.getMessage());
}
```

<Warning>
  Check the resulting widget state, not only the action result. These widget mappings respond to client revision changes; a successful dispatch is not proof of convergence.
</Warning>

## Prayer

The core writes are idempotent and converge toward a requested state:

| Method                               | Behavior                                          |
| ------------------------------------ | ------------------------------------------------- |
| `PrayerActions.enable(Prayer)`       | Enables one prayer                                |
| `PrayerActions.disable(Prayer)`      | Disables one prayer                               |
| `PrayerActions.set(Prayer, boolean)` | Converges one prayer                              |
| `PrayerActions.setOnly(Prayer...)`   | Converges toward exactly the requested active set |
| `PrayerActions.disableAll()`         | Converges toward no active prayers                |

```java theme={null}
InteractionResult result = PrayerActions.setOnly(
    Prayer.PROTECT_FROM_MELEE,
    Prayer.PIETY
);
if (result.getStatus() == InteractionStatus.PACED) {
    return; // retry from the next tick
}
```

`setOnly(...)` and `disableAll()` may need multiple ticks: each call queues at most the next required toggle. Re-evaluate and call again until the observed state matches. A null prayer returns `TARGET_NULL`; an unmapped, missing, or hidden component returns `PRAYER_WIDGET_NOT_FOUND`.

Prayer changes require the Prayer tab only when the observed state still differs from the request.

### Quick prayers

| Method                               | Behavior                                                            |
| ------------------------------------ | ------------------------------------------------------------------- |
| `openQuickPrayers()`                 | Opens the quick-prayer setup interface                              |
| `selectQuickPrayer(Prayer, boolean)` | Selects or clears one prayer while setup is open                    |
| `setQuickPrayers(Prayer...)`         | Converges the configured selection in one call (bypasses the pacer) |
| `toggleQuickPrayers()`               | Toggles the quick-prayer orb                                        |

Open the setup before calling `setQuickPrayers(...)`, and toggle the orb only after observing the configured set.

## Dwarf multicannon

`CannonActions` covers the cannon lifecycle. State reads use rev240 object-ID evidence (`DWARF_MULTICANNON1`, `BROKEN_MULTICANNON`, and the three build-stage objects); writes are paced.

| Method                                             | Behavior                                                                                 |
| -------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `isPlaced()` / `isBroken()` / `isPartiallyBuilt()` | Scene-state reads                                                                        |
| `hasSetupKit()`                                    | All four kit parts are in inventory                                                      |
| `place()`                                          | Uses the cannon base; fails closed when a cannon is already placed or the kit is missing |
| `load()`                                           | Interacts with the placed cannon                                                         |
| `pickUp()`                                         | Fires Pick-up on the placed cannon                                                       |
| `repair()`                                         | Fires Repair on a broken cannon                                                          |

Cannonball ammunition state is deliberately unmodeled: rev240 evidence exposes no ammunition varbit and IDs are never invented. Re-observe `isPlaced()`/`isBroken()` after each interaction.

## Magic

`MagicActions` handles result-aware spell interactions. Cast, selection, and deselection require the Magic tab to be visible; otherwise they return `WIDGET_HIDDEN` targeting `tab/MAGIC` without changing tabs.

### Reads

`canCast`, `getSpellBook`, `isAutoCasting`, `isSpellSelected()`, and `isSpellSelected(Spell)` report spell state. Rune-pouch contents come from the `sdk.query.RunePouch` reads.

### Selection and targeted casting

| Method                                                 | Target                                          |
| ------------------------------------------------------ | ----------------------------------------------- |
| `selectSpell(Spell)`                                   | Selects the spell widget                        |
| `deselect()`                                           | Clears the current selection                    |
| `cast(Spell)`                                          | Untargeted cast                                 |
| `cast(Spell, NPC / Player / TileObject / ground item)` | Casts on a resolved target                      |
| `castOnInventoryItem(Spell, Widget)`                   | Advances the shared inventory-spell transaction |

```java theme={null}
Optional<Widget> item = Inventory.search().withId(ItemID.YEW_LONGBOW).first();
if (item.isPresent()) {
    InteractionResult cast = MagicActions.castOnInventoryItem(
        Spell.HIGH_LEVEL_ALCHEMY,
        item.get()
    );
}
```

Passing `null` to `selectSpell` returns `TARGET_NULL`. Missing spell widgets yield `SPELL_WIDGET_NOT_FOUND`; hidden widgets yield `SPELL_NOT_CASTABLE`.

### The inventory-spell transaction

`castOnInventoryItem` is built for repeated calls from a tick-driven controller. It owns one synchronized transaction shared by all consumers:

1. With no selection, it requires the Magic tab and selects the exact requested spell. The transaction never opens a closed tab.
2. Once the exact spell is selected, it validates the inventory widget and queues one mouse-click metadata packet followed by one widget-on-widget packet.
3. RuneLite owns the transition back to Inventory; the transaction never sends an explicit tab action.
4. While the selection stays unresolved after target dispatch, further requests return `PACED` without resending anything. A competing spell or item cannot take over the transaction.
5. A different selected spell or non-spell widget is cleared and reported as `TARGET_STALE`. A selection unresolved for five seconds is cleared the same way, and recovery begins with a fresh selection cycle.

`DISPATCHED` means the action was accepted for dispatch; it does not prove completion. Observe the domain postcondition (an inventory decrement, XP gain, or produced output) before counting the spell as cast.

Spell names resolve through `MagicActions.resolveSpellInfo(name)`, backed by the `Api.actions.Spell` catalog. See the [scripting patterns](/guides/scripting-patterns#pattern-spell-resolution) for the resolution rules.

## Tick decisions

Use `TickDecisionList` when a plugin must evaluate ordered, once-per-tick combat policies. Call it from `onGameTick`; never inside a `TaskPipeline` or any faster loop.

* The list suppresses duplicate evaluation within one tick and preserves declaration order.
* A decision returning `true` reserves the remainder of the tick; `false` lets the next decision run.
* Each decision returns without sleeping and dispatches at most one meaningful action.

```java theme={null}
private TickDecisionList combatDecisions;

@Override
protected void startUp() {
    combatDecisions = TickDecisionList.of(
            new PotionDecision()
                    .addPotion(superStrength, () ->
                            client.getBoostedSkillLevel(Skill.STRENGTH)
                                    - client.getRealSkillLevel(Skill.STRENGTH) < 3),
            new EatDecision(client),
            attackDecision);
}

@Subscribe
public void onGameTick(GameTick event) {
    combatDecisions.onTick(client.getTickCount());
}
```

`lastDecisionName()` reports which decision blocked the last evaluated tick.

### Eating

`EatDecision` reads the current tick plus real and boosted hitpoints from the injected `Client`. The default policy requires 20 missing hitpoints and covers sharks (20), lobsters (14), swordfish (14), jugs of wine (8), blighted manta ray (22), blighted anglerfish (10), and karambwan (45 primary, 18 combo). It clears an active widget selection before eating, records its cooldown only after `InventoryActions.use(..., "Eat")` accepts, and blocks another eat for three game ticks. When primary and combo food both fit the deficit, it dispatches the primary food and attempts the combo on the next evaluation.

Supply custom healing maps, thresholds, and a policy gate through the configurable constructor:

```java theme={null}
Map<Integer, Integer> food = new LinkedHashMap<>();
food.put(ItemID.MANTA_RAY, 22);

Map<Integer, Integer> combo = new LinkedHashMap<>();
combo.put(ItemID.COOKED_KARAMBWAN, 18);

TickDecision eat = new EatDecision(client, food, combo, 18, () -> !bankOpen && safeToEat());
```

Map iteration order does not select food: the decision scans current inventory order and picks the first food whose heal does not exceed the missing hitpoints.

### Potions and variants

`ItemVariant` groups interchangeable doses in resolution order, preferred dose first:

```java theme={null}
ItemVariant superStrength = new ItemVariant(
        ItemID.SUPER_STRENGTH4,
        ItemID.SUPER_STRENGTH3,
        ItemID.SUPER_STRENGTH2,
        ItemID.SUPER_STRENGTH1);
```

`PotionDecision` evaluates rules in the order you add them. A rule whose condition is false does nothing; a rule whose condition is true resolves the first owned variant and dispatches `InventoryActions.use(id, "Drink")`, ending potion evaluation for that tick. Conditions read state (boost deltas, prayer points, run energy) and stay read-only.

### Ordering

Place survival before damage. A typical combat list:

1. Configure quick prayers.
2. Move into the combat area.
3. Drink potions.
4. Flick or swap prayer.
5. Eat.
6. Loot.
7. Set combat style.
8. Attack.

Never infer completion from an accepted dispatch; re-read combat, inventory, animation, and skill state on a later tick.

## Workflow ownership

Food thresholds, target selection, retries, and death or loot transitions belong in `CombatWorkflowBuilder` or your own state machine, not in these action methods. See [workflow builders](/guides/workflow-builders) for the orchestration layer. In paced unit tests, call `ActionPacer.reset()` in `@Before`.
