Drive the fight module from a script: switch it on or off, change its profile and speed, read the fight under way, follow every fight's events, and take its decisions yourself at the placement, ready and each action of a turn, with the rules engine checking every action.
asterobot:fight lets your script work with the bot's fight module, which plays its fights as its fight profile says. The guide describes the module for players: The fight module.
import {
enable, disable, enabled,
setProfile, profile, setSpeed, speed, setFights, fights, state,
onFight, offFight, onDecision, offDecision,
} from "asterobot:fight";
These are the module's only exports. There's no fight object grouping them.
| Export | Returns | What it does |
|---|---|---|
enable(), disable() |
Nothing | Switch the module on or off while the script runs |
enabled() |
A boolean | Whether the module is on, all settings considered |
setProfile() |
Nothing | Play another profile while the script runs |
profile() |
An object | The profile in effect |
setSpeed(), speed() |
Nothing, a string or a timing object | Set or read the speed |
setFights(), fights() |
Nothing, an array | Set or read the kinds of fight it plays |
state() |
An object or undefined |
The fight under way |
onFight(), offFight() |
A handle, nothing | Follow every fight's events |
onDecision(), offDecision() |
A handle, nothing | Take the module's decisions |
Every function is synchronous: none returns a promise.
Overrides
enable(), disable(), setProfile(), setSpeed() and setFights() override the settings the person saved on the bot's Fight tab. The tab then says what the script overrides, and the person's settings play again once the script stops.
- They last while the script runs. A reload starts a new script: its first override replaces the old script's, and until then the old script's stay. A reload that fails to start leaves the running script's.
- The switch takes effect at the next turn. Switched during a fight, the module starts or stops playing at the next turn's start, so one turn is never played by two. Before the first turn, and for the profile and the speed, the change applies at once.
- On a relayed session the switch stays the person's.
enable()anddisable()change nothing there, andenabled()says what's in effect. The profile, the speed and the kinds of fight still apply. - A profile the script sets plays every fight, against players too, whatever Profile in PvP fights the tab sets.
enable() and disable()
enable() switches the module on and disable() off, as the switch on the Fight tab does. enabled() returns true when the module is on, all settings considered: the saved switch, or the script's.
import { disable } from "asterobot:fight";
// This package plays its fights itself.
disable();
setProfile()
setProfile(nameOrProfile) plays another profile while the script runs.
| Argument | Plays |
|---|---|
| A profile's name | "Idle", a profile of the person's pool, or a package's, such as "@some-author:fighters/air-cra" |
"" |
The default profile |
| An object | A profile your script writes, in the fight profile format |
An object is read as JSON.stringify writes it, then checked as a saved profile is. Its extends is merged in, one level as always. A malformed profile throws a TypeError naming the field, and its warnings go to the bot's console. A name no profile has, or a saved profile that can't be played, throws an Error.
import { setProfile } from "asterobot:fight";
setProfile({
version: 1,
name: "Careful",
extends: "@some-author:fighters/air-cra",
risk: 20,
rules: [{ when: { "self.life": { below: "30%" } }, do: { retreat: { to: "cover" } } }],
});
profile()
profile() returns the profile in effect as a plain object in the format, the profile it extends already merged in, so without extends. The weapon is written "weapon". The object is a copy: change it, then set it back.
import { profile, setProfile } from "asterobot:fight";
// Keep everything, but walk no more.
const current = profile();
setProfile({ ...current, parts: { ...current.parts, movement: false } });
A field this Asterobot doesn't read isn't in the copy.
setSpeed() and speed()
setSpeed(speed) sets the speed while the script runs: "slow", "normal" or "turbo", or a custom timing object, as the Custom speed of the Fight tab sets it:
setSpeed({
actions: { min: 0 }, // no wait between actions
cast: { min: 300, max: 800 }, // 300 to 800 ms before each spell, at random
endTurn: { min: 1000 }, // 1 s before ending the turn
answerAtOnce: true, // answer the server at once, as "turbo" does
});
| Field | What it is |
|---|---|
actions |
The wait before every decision without one of its own: each move, spell and end of turn, the placement and ready. Required. |
placement, ready, move, cast, endTurn |
The wait before a decision of that kind. Absent waits actions. |
answerAtOnce |
true answers the server at once, as "turbo" does: the fight map's load, each action's acknowledgement and the turn-ready answers. Absent or false answers once the game client would have played what it answers. |
A wait is { min, max } in milliseconds, each from 0 to 60000: min, or a random wait from min to max when max is above it. A wait never costs the turn: a decision goes out 2 seconds before the turn ends at the latest. The waits only change when the module acts, never what it decides.
Any other value, a field a timing doesn't have, or a wait out of bounds throws a TypeError. speed() returns the speed in effect: the preset's name, or the timing object for a custom speed. Speed says what each one does, and what "turbo" and answering at once risk.
setFights() and fights()
setFights(kinds) sets the kinds of fight the module plays while the script runs, as the Fights it plays checkboxes of the Fight tab do. An empty array plays none. In a fight of another kind, or of a type whose meaning isn't known yet, the module is off, as with disable(), from the moment the game says what the fight is, just after its map's load. An unknown kind throws a TypeError. fights() returns the kinds in effect: ["monsters"] unless the person or a script chose others.
| Kind | Fights |
|---|---|
"monsters" |
Fights against monsters, quest fights where an NPC fights beside the character included, and breaches |
"aggressions" |
A player attacking another |
"duels" |
Duels between players |
"kolizeum" |
Kolossium fights |
"taxCollectorsAndPrisms" |
Fights for a perceptor (tax collector), a prism or an alliance's territory |
import { setFights } from "asterobot:fight";
// Defend against aggressions while farming.
setFights(["monsters", "aggressions"]);
Starting a fight against players is your script's job, as for monsters: accepting a duel, registering for the Kolossium. Against players, the module reckons each enemy hits bare-handed, since it doesn't know their spells yet: it plays, but takes more risks than against monsters.
state()
state() returns the fight under way, as the module follows it from the server's messages, or undefined outside one. Every call returns a new snapshot.
| Property | Type | Value |
|---|---|---|
characterId |
BigInt | The bot's character |
round |
number | The fight's round, 0 before it starts |
turn |
BigInt | The fighter whose turn it is; absent during the placement |
fighters |
array | Every fighter, the dead ones included |
Each fighter:
| Property | Type | Value |
|---|---|---|
id |
BigInt | Its id in the fight: negative for monsters and summons |
kind |
string | "character", "sidekick" or "creature" |
ally |
boolean | Whether it's on the character's side, the character included |
team |
number | Its team: fighters of one team share it |
alive |
boolean | Whether it's alive |
cell |
number | Its cell |
invisible |
boolean | Whether the server hides it from the other team: cell is then where it was last seen |
level |
number | Its level |
life, maxLife, shield |
number | Its life, its most life, its shield points |
ap, mp |
number | The AP and MP it has left |
states |
array of numbers | The states it's in, by spell_states id |
breed |
number | A character's class, by breeds id |
monsterId, grade |
number | A creature's monster, by monsters id, and its grade |
summonerId |
BigInt | The fighter that summoned it, for a summon |
import { state } from "asterobot:fight";
const fight = state();
const mastogob = fight?.fighters.find((f) => f.monsterId === 2854 && f.alive);
onFight()
onFight(handler) calls handler with each event of every fight the bot is in, whether the module plays it or not, until offFight(handle). It returns the handle.
type |
When | Also has |
|---|---|---|
"started" |
The bot entered a fight, or got one back after a reconnection | |
"turnStarted" |
A fighter's turn started | fighter (BigInt), round |
"actionPlayed" |
The module played an action: only the module's own | fighter, round, action, spell, cell, reasons, scripted |
"turnEnded" |
A fighter's turn ended | fighter, round |
"ended" |
The fight ended, or the session did first | round, summary |
actionPlayed describes what the module sent, as the Turn log keeps it:
| Property | Value |
|---|---|
fighter |
The character, or for a turn's action the summon it controls whose turn it is |
action |
"placement", "ready", "move", "cast" or "endTurn" |
spell |
The spell cast: its id, or "weapon" |
cell |
The cell cast on, walked to, or placed on |
reasons |
score, the score of the turn this action starts, terms, what the score counted by term, and options, the best options weighed, { spell, cell, score }. Absent for the placement, ready, and an end of turn a rule asked for. |
scripted |
true when a decision hook decided it |
ended's summary has rounds, turns (the character's), actions (the module's), alive (whether the character was alive at the end) and duration, the fight's length in milliseconds as the server gave it, absent when the session ended first.
import { onFight } from "asterobot:fight";
import { botInfo } from "asterobot:bot";
onFight((event) => {
if (event.type === "ended") botInfo(`Fight over in ${event.summary.rounds} rounds, alive: ${event.summary.alive}`);
});
Handlers run in the order they were registered. What a handler returns is ignored, and an async handler is fine. A handler that throws stops the script, as an on() handler does.
An event reaches the script at the moment the module follows it: before your protocol handlers hear the message it comes from, on every PC. actionPlayed comes when the module sends the action.
onDecision()
onDecision(hook) asks hook at each of the module's decisions, until offDecision(handle). It returns the handle. The module decides first, as its profile says, then asks your hooks, and plays your answer instead when the rules engine accepts it.
The hook receives { stage, fight, options }:
stage |
When | options, best first |
The hook can answer |
|---|---|---|---|
"placement" |
Before the fight, when the profile places the character | The placement cells, { cell, score } |
{ cell }: one of the options |
"ready" |
Before the fight, when the profile says the character is ready | None | { wait: true } |
"action" |
Before each action of a turn the module plays, the character's and those of the summons it controls | The best turns the AI found for the rest of the turn, { actions, score, terms } |
{ option: i }, { actions: [...] } or { endTurn: true } |
fight is what state() returns. The challenges' stage comes with the challenges, which the module doesn't choose yet.
Answers
{ cell }places the character on that cell. It must be one of the options: the character's own cell, or one of its team's placement cells nobody stands on.{ wait: true }holds the character's ready. The hooks are asked again each time the fight changes, such as an ally joining, and the placement countdown still starts the fight when it runs out.{ option: i }plays the first action ofoptions[i]. Then the module decides again: the AI searches again, and the hooks are asked again.{ actions: [...] }plays the actions one after the other, without asking again between them. Return every action and{ endTurn: true }to play the whole turn.{ endTurn: true }ends the turn.
Each action is one of:
| Action | Does |
|---|---|
{ move: cell } |
Walks to the cell |
{ cast: spell, cell } |
Casts the spell on the cell: its id, or "weapon" for the weapon or bare hands |
{ cast: spell, target } |
Casts the spell on a fighter's cell: target is an id of fight.fighters |
{ endTurn: true } |
Ends the turn |
Each turn of options lists its actions the same way, ending with { endTurn: true }, so { actions: options[i].actions } plays a whole turn the AI found.
undefined, null and a promise are no answer: the next hook is asked, then the AI decides. A hook is never awaited.
import { onDecision } from "asterobot:fight";
onDecision(({ stage, fight, options }) => {
if (stage !== "action") return;
// Open on the Royal Mastogob with Explosive Arrow when it's there.
const mastogob = fight.fighters.find((f) => f.monsterId === 2854 && f.alive);
if (mastogob && fight.round === 1) return { actions: [{ cast: 32445, target: mastogob.id }] };
// Otherwise prefer the best turn that doesn't move.
const still = options.findIndex((o) => !o.actions.some((a) => "move" in a));
return still >= 0 ? { option: still } : undefined;
});
What the module checks
The rules engine checks each action before it's sent, as it checks the AI's: the AP, the range, the line of sight, the cooldowns, the cells a walk can reach with its tackle. The module also never sends an action the server refused earlier in the turn. When it can't play an answer, or a hook throws, runs past the time limit or answers something of another shape, the AI's decision is played and the bot's console says why.
A list of actions stops early, and the module decides again with the AI and the hooks:
- at an action the rules engine or the server refuses;
- when the fight isn't what the engine predicted after the action before: a fighter that died or lived otherwise, one on another cell, or the fighter acting with other AP or MP left. Life isn't compared, since the roll is the server's.
The console says why, with The fight module stops the script's actions:.
Timing
- The module waits for your answer as long as it takes. The question goes through the script's queue like every message, so on a slow PC it's answered later, never skipped: such a PC may lose a turn, but it never decides otherwise. When no hook is registered, nothing is asked and nothing waits.
- What a hook decides goes out after your frames. An answer takes its turn among what your script sends, as a movement does: a hook that calls
send(X)and then returns a cast putsXon the wire first. What the AI decides alone is sent directly. - The speed still applies. Each action goes out after the wait the speed sets before it, as the AI's do.
The Turn log marks the decisions your hooks made By the script, and actionPlayed has scripted: true.
While Asterobot reads declarations
When Asterobot reads a package's settings, steps and conditions, it runs the top level of its modules without a bot (see Declarations). There's no fight then: state() returns undefined, onFight() and onDecision() return a handle that never fires, and offFight() and offDecision() do nothing. The other functions throw, since they read or change a bot's module.
Errors
| Error | Type | Cause |
|---|---|---|
fight.setProfile: fight: malformed profile: <field>: <reason> |
TypeError |
The profile object can't be played. The format's errors list each reason. |
fight.setProfile: fightprofiles: no profile has this name: "<name>" |
Error |
No profile has that name. For a package's profile, the reason follows, such as no version of <package> is installed. |
fight.setProfile: fight: malformed profile: <field>: <reason>, for a name |
Error |
The saved profile of that name can't be played |
fight.setProfile takes a profile's name or a profile object |
TypeError |
The argument is neither a string nor an object |
fight.setSpeed: fight: the speed must be slow, normal, turbo or custom, not "<speed>" |
TypeError |
Another speed |
fight.setSpeed: fight: a custom speed needs its timing, each wait from 0 to 60000 ms |
TypeError |
"custom" passed as a string: pass the timing object |
fight.setSpeed: fight: a custom speed needs its timing, each wait from 0 to 60000 ms, not <ms> |
TypeError |
A wait out of bounds |
fight.setSpeed: json: unknown field "<field>" |
TypeError |
A field a timing doesn't have |
fight.setSpeed takes "slow", "normal", "turbo" or a timing object |
TypeError |
The argument is neither a string nor an object |
fight.onFight handler must be callable, fight.onDecision handler must be callable |
TypeError |
The handler isn't a function |
maximum behavior handlers reached |
TypeError |
256 handlers are registered already, protocol handlers included |
These lines appear on the bot's Console and don't stop the script:
| Line | Cause |
|---|---|
fight decision hook failed: <error>: the AI decides |
A hook threw, or ran for more than 5 s |
fight decision hook answered <stage>: not an answer this stage takes: <shapes>: the AI decides |
A hook answered an object of another shape |
fight decision hook returned a Promise, which counts as no answer: a hook answers synchronously, so it can't be async |
An async hook, or one returning a promise. Said once per script. |
The script's decision can't be played: the fight module decides instead |
The rules engine refused the answer, with the reason. Said once a turn. |
The fight module stops the script's actions: <reason> |
A list of actions stopped early |
Nothing plays this fight unless the bot's script does: the server disconnects a character left idle for a few minutes |
On a Full socket bot, the module is off for the fight, or doesn't play its kind |
A hook that fails again and again is reported at the 1st, 10th, 100th failure in a row, ending with (10 times) and so on.
Fights explains how scripts and the module share a fight, and what a script sends when it plays fights itself.