api.timeline
The map's Timeline from an action: the running timed effects, and starting, changing and ending one.
Part of the Action API · Function reference · Since Action API 0.6 (v0.104.0)
Signature
api.timeline.list(): Promise<TimedEffect[]>
api.timeline.createEffect(input: EffectInput): Promise<TimedEffect>
api.timeline.editEffect(id: string, patch: Partial<EffectInput>): Promise<TimedEffect>
api.timeline.endEffect(id: string): Promise<TimedEffect>
api.timeline.startBuff(buff: Buff, opts?: { tokenId?, url?, color?, parent? }): Promise<TimedEffect>
api.timeline.endEffects(filter: { tokenId?: string; category: string | string[] }): Promise<{ ended, refused }> // 0.8
Asynchronous — await each.
interface EffectInput {
name: string;
duration: number | { fn: string }; // rounds (0 or more), -1 for indefinite, or a function
description?: string;
url?: string; // defaults to this action's document (the power it came from)
color?: string; // '#rrggbb'; defaults to the next distinct hue on the map
startTick?: number; // defaults to now
tokenId?: string; // the token the effect is on
positivity?: 'positive' | 'neutral' | 'negative';
effects?: BuffEffect[]; // 0.8: what it does to the bearer's numbers while it runs
category?: string; // 0.8: Magic, Curse, Poison… — what cleansing ends
links?: { type: 'drawing' | 'token' | 'effect'; id: string }[]; // 0.8: 'effect' — ends when that one does
}
Durations
A number is rounds: the effect ends at the same place in initiative
that many rounds later (10 — ten rounds; 0 — the end of this turn's
round position; -1 — indefinite, until ended by hand). A function
decides at every tick:
duration: { fn: '(round, whoseTurn) => round >= 5' }
It runs in the platform's script sandbox — a small allowlisted language,
not full JavaScript — with round (the current round) and whoseTurn (the
token acting: { id, name, initiative, requestRoll }), and ends the effect
when it returns true. whoseTurn.requestRoll('1d20+2') asks whoever plays
that token to roll; the platform pauses the function until the roll is
answered and then runs it again with the result in hand — { total }, or
null if they declined — so write it as a plain call, not an await:
duration: { fn: '(round, whoseTurn) => { if (whoseTurn.id !== "TOKEN_ID") return false; const r = whoseTurn.requestRoll("1d20+3", { label: "Wisdom save" }); return r !== null && r.total >= 15; }' }
A function that throws leaves the effect running and tells whoever started it what went wrong, once.
Buffs
api.timeline.startBuff(buff, { tokenId?, url?, color?, parent? }) starts a
buff on the Timeline. Its duration becomes the effect's:
- a timed buff counts rounds by its unit (a minute is ten rounds, an hour six hundred);
- concentration and save ends become functions that decide (the platform supplies the shape; the action may write its own);
- a buff whose duration already names an effect (
type: 'effect',effectId) returns that effect.
The slice carries the buff's name and description.
Since Action API 0.8 (v0.117.0) a buff does something. Its effects
change the bearer's numbers while the effect runs. Its positivity colours
the bearer's Mini-Me chip, and its category is what cleansing ends.
{
name: 'Shield of Faith',
category: 'Magic',
positivity: 'positive',
duration: { type: 'concentration' },
effects: [{ kind: 'attribute', target: 'ac', op: 'add', value: 2 }],
}
An attribute effect has a target from the system's vocabulary (ac,
ac.base, ability.str, speed.walk, save.*…) and one of five
operations. They are applied in stages, so the order they arrive in never
matters:
op |
Does | Several resolve by |
|---|---|---|
set |
replaces the base (Mage Armor: ac.base = '13 + @dex.mod') |
the highest wins |
add |
adds (Shield of Faith: ac +2) |
all apply |
percent |
adds a percent of the base (Haste: speed.* +100) |
percents sum, then apply once |
floor |
at least (Barkskin: ac 16) |
the highest wins |
ceiling |
at most (Restrained: speed.* 0), applied last |
the lowest wins |
A condition effect, { kind: 'condition', conditionId: 'paralyzed' },
puts that condition on the bearer for as long as the buff runs.
The system decides stacking. In 5e, two castings of the same spell do not stack: the most potent applies.
parent (an effect, or its id) ties the buff to another effect, and it
ends when that one does. A concentration spell passes the caster's
concentration effect, so dropping concentration ends the buff on every
target:
const conc = await api.timeline.concentrate({ name: 'Bless', duration: 10 });
for (const t of targets) await api.timeline.startBuff(bless, { tokenId: t.id, parent: conc });
Cleansing
api.timeline.endEffects({ tokenId?, category }) ends every running effect
on a token whose category matches. The token defaults to the acting one.
category is a string or a list, matched case-insensitively:
await api.timeline.endEffects({ tokenId: target.id, category: 'Magic' }); // Dispel Magic
await api.timeline.endEffects({ tokenId: target.id, category: ['Poison', 'Disease'] }); // Lesser Restoration
Each end is the server's to allow, as with endEffect. An effect the server
refuses is listed in refused with its reason, and the call does not throw.
What a derived power does (v0.104.2)
A System 8 power action the platform derives from its document writes this
call itself when the document has a duration: one createEffect per token
in targets (or target), or on token when the range is personal and
nothing was aimed at; duration is the rounds the document counts, -1
for concentration / encounter / sustained / permanent, and for a
save that ends (Str ends) a function built at run time from the bearer's
id and the power roll — the save DC in System 8:
// What Entangle (circle 2, concentration, Str ends) sends for a target whose roll to beat was 14.
{ fn: '(round, whoseTurn) => { if (!whoseTurn || whoseTurn.id !== "gnoll-1") return false; const r = whoseTurn.requestRoll("1d20+str"); return r !== null && r.total >= 14; }' }
The first effect takes the palette's colour and the rest reuse it, so one casting is one hue. See The Timeline.
Example
// Bless, ten rounds, on the target; the slice links back to the spell.
const bless = await api.timeline.createEffect({ name: 'Bless', duration: 10, tokenId: target.id });
// Dispel Magic: end the first magical effect on the target.
const running = await api.timeline.list();
const onTarget = running.find((e) => e.tokenId === target.id);
if (onTarget) await api.timeline.endEffect(onTarget.id);
Permissions, and a map
The server's rules apply as they do to a hand on the Timeline strip: anyone who may play on the map starts an effect; the one who started it, or the GM, changes or ends it. Each call needs an open map — the impersonated-token bar and the map are where actions run — and throws with a plain sentence without one.