1 Examples
Flaig edited this page 2026-10-02 16:52:21 +02:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Examples

Copy-paste Script macros. Unless noted, they expect one token controlled (source) and one targeted (target).

Attack with a recoil

await TokenMove.between(source, target)
  .on("source").lunge({ tokens: 0.8 })
  .on("target").at("impact").offset({ direction: "backward", tokens: 0.3, duration: 250, ease: "easeOutBounce" })
  .play();

The attacker lunges toward the target; the target recoils at the moment of impact; both return.

Heavy attack with a wind-up

await TokenMove.between()
  .on("source")
    .offset({ direction: "backward", tokens: 0.4, duration: 300, ease: "easeOutQuad" })
    .wait(150)
    .offset({ direction: "forward", tokens: 1.2, duration: 250, ease: "easeInQuad" })
    .mark("impact")
    .restore({ duration: 500, ease: "easeInOutCubic" })
  .on("target").at("impact")
    .offset({ direction: "backward", tokens: 0.8, duration: 300, ease: "easeOutQuart" })
    .wait(200)
    .restore({ duration: 400 })
  .play();

between() without arguments uses the controlled and the targeted token.

Dodge to a random side

await TokenMove.between()
  .on("target")
  .offset({ direction: ["left", "right"], tokens: 0.8, duration: 180, ease: "easeOutQuart" })
  .wait(250)
  .restore({ duration: 300 })
  .play();

Shake and wobble

await TokenMove.on(target).shake({ times: 8, tokens: 0.08 }).play();

await TokenMove.on(source)
  .offset({ direction: ["left", "right"], tokens: 0.15, duration: 60, repeats: 5, yoyo: true, ease: "linear" })
  .play();

Random distances and speeds

await TokenMove.on(source)
  .offset({ tokens: [0.3, 1.2], duration: [150, 450], direction: "random" })
  .restore({ duration: 200 })
  .play();

Ranges are rolled once per play(), on the client that starts it, so everyone sees the same result.

Face the target

// Temporary: turn toward the target while lunging, turn back afterwards
await TokenMove.between().on("source").offset({ tokens: 0.6, face: true, duration: 300 }).play();

// Saved: face the target and keep the rotation
await TokenMove.on(source).face(target, { duration: 300, persist: true }).play();

Knockback that stops at walls

await TokenMove.between(source, target, { walls: true })
  .on("source").lunge()
  .on("target").at("impact")
    .moveBy({ away: source, squares: 3, duration: 450, ease: "easeOutQuart", action: "displace" })
  .play();

The target is pushed three squares away and stays there. action: "displace" records it as forced movement rather than walking.

Click to move (Sequencer crosshair)

Needs Sequencer.

const pos = await Sequencer.Crosshair.show({ gridHighlight: true });
if (pos) {
  await TokenMove.on(source, { walls: true })
    .moveTo(pos, { face: true, duration: 800, ease: "easeInOutCubic" })
    .play();
}

The token turns toward the clicked spot, moves there (snapped to the grid, stopped by walls) and stays.

Dash to the target

await TokenMove.between()
  .on("source").moveTo("target", { adjacent: true, face: true, duration: 600, ease: "easeInQuad" })
  .mark("hit")
  .on("target").at("hit").shake({ times: 5 })
  .play();

The attacker rushes to the space next to the target and stays; the target shakes on arrival.

Area knockback

const radius = 3 * canvas.grid.size;
const others = canvas.tokens.placeables.filter(t =>
  t !== source && Math.hypot(t.center.x - source.center.x, t.center.y - source.center.y) <= radius);

const anim = new TokenMove({ walls: true });
anim.on(source).shake({ times: 4, tokens: 0.05 });
for (const token of others) {
  anim.on(token).wait(150).moveBy({ away: source, squares: 2, duration: 400, ease: "easeOutQuart", action: "displace" });
}
await anim.play();

Every token within three squares is pushed two squares away from the caster.

Several tokens on one timeline

await TokenMove.between()
  .on("source").offset({ tokens: 0.5, duration: 500 })                       // source:    0– 500 ms
  .on("target").offset({ direction: "backward", tokens: 0.5, duration: 500 }) // target:    0– 500 ms
  .on("source").restore({ duration: 500 })                                   // source:  500–1000 ms
  .on("target").at("end").restore({ duration: 500 })                         // target: 1000–1500 ms
  .on("source").at(1500).shake({ times: 4 })                                 // source: 1500 ms–
  .play();

Custom preset

if (!("hop" in TokenMove.prototype)) {
  TokenMove.register("hop", (tm, { height = 0.4, times = 1 } = {}) => {
    for (let i = 0; i < times; i++) {
      tm.offset({ relativeTo: "world", direction: 180, tokens: height, duration: 120, ease: "easeOutQuad" })
        .offset({ relativeTo: "world", direction: 0, tokens: height, duration: 120, ease: "easeInQuad" });
    }
  });
}
await TokenMove.between().on("source").hop({ times: 3 }).on("target").at("end").hop().play();

Custom easing

const { registerEasing, easings } = game.modules.get("tokenmove").api;
if (!easings.includes("stepped")) registerEasing("stepped", t => Math.floor(t * 4) / 4);

await TokenMove.on(source)
  .offset({ relativeTo: "world", direction: 270, tokens: 2, duration: 800, ease: "stepped" })
  .offset({ relativeTo: "world", direction: 180, tokens: 1, duration: 600, ease: t => t * t * t })
  .play();

Custom easings are sampled when synced, so other players see the same curve without registering it.

Play only when something is targeted

const anim = TokenMove.on(source).shake().playIf(() => game.user.targets.size > 0);
await anim.play();   // → { completed: true, skipped: true } when nothing is targeted

Emergency stop

TokenMove.stop();   // every token on the scene, on every client

Tokens snap back to their saved positions.

Checking your setup

const mod = game.modules.get("tokenmove");
ChatMessage.create({
  whisper: [game.user.id],
  content: [
    `TokenMove ${mod?.active ? "active" : "inactive"} · v${mod?.version}`,
    `socketlib: ${!!game.modules.get("socketlib")?.active} · Sequencer: ${!!game.modules.get("sequencer")?.active}`,
    `source: ${TokenMove.source?.name ?? "none"} · target: ${TokenMove.target?.name ?? "none"}`,
    `easings: ${mod?.api?.easings.length}`
  ].join("<br>")
});