Table of contents
- Examples
- Attack with a recoil
- Heavy attack with a wind-up
- Dodge to a random side
- Shake and wobble
- Random distances and speeds
- Face the target
- Knockback that stops at walls
- Click to move (Sequencer crosshair)
- Dash to the target
- Area knockback
- Several tokens on one timeline
- Custom preset
- Custom easing
- Play only when something is targeted
- Emergency stop
- Checking your setup
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>")
});