Documentation Index
Fetch the complete documentation index at: https://v5.rpgjs.dev/llms.txt
Use this file to discover all available pages before exploring further.
Client Visuals
Client visuals let the server trigger a named visual macro while the rendering
details live on the client.
They do not replace existing APIs such as playSound(), flash(),
showComponentAnimation(), or sprite animations. They are a lightweight way to
group those existing client-side primitives when one gameplay moment needs
several visual or audio reactions at once.
Why use client visuals?
Without client visuals, a hit may require several server packets:
target.flash()
target.showHit("-25")
target.showComponentAnimation("hit-spark", { scale: 1.2 })
player.playSound("hit")
With client visuals, the server sends one compact packet:
player.clientVisual("hit", {
targetId: target.id,
damage: 25,
})
The client decides how hit is rendered. This keeps visual orchestration close
to the renderer, avoids spreading presentation details through server gameplay
code, and reduces bandwidth when several visual operations should happen
together.
When to prefer client visuals
Use client visuals when:
- one gameplay moment triggers multiple visual or audio operations
- the visual sequence is client presentation only
- you want to customize rendering without changing server gameplay code
- you want to reduce several visual packets to one server event
Prefer the direct APIs when you only need one operation:
player.playSound("door-open")
player.flash()
map.showComponentAnimation("explosion", { x: 100, y: 120 }, { scale: 2 })
Client visuals should not own gameplay authority. Damage, states, cooldowns,
movement, rewards, and combat results should still be decided by the server.
Register a client visual
Register visuals in a client module with clientVisuals:
import { defineModule, RpgClient } from "@rpgjs/client";
import HitSpark from "./hit-spark.ce";
export default defineModule<RpgClient>({
componentAnimations: [
{
id: "hit-spark",
component: HitSpark,
},
],
clientVisuals: {
hit({ target, data }, helpers) {
helpers.flash(target, {
type: "tint",
tint: "red",
duration: 120,
});
helpers.showHit(target, `-${data.damage}`);
helpers.component("hit-spark", target, {
scale: data.critical ? 1.4 : 1,
});
helpers.sound(data.critical ? "critical-hit" : "hit");
},
},
});
The handler receives:
target, source, and object, resolved from targetId, sourceId, and
objectId
position, resolved from position or { x, y }
data, the serializable payload sent by the server
helpers, a small wrapper around existing client primitives
You can also pass target, source, or object instead of the *Id fields
when the payload already contains an object id string. The *Id names are
usually clearer for server payloads.
The first argument passed to helpers.component() is a component animation id.
Register that id in the same client module with componentAnimations, or in
another loaded client module, before using it from clientVisuals.
Trigger from a player
player.clientVisual() sends the visual only to that player’s client:
player.clientVisual("hit", {
targetId: enemy.id,
damage: 25,
critical: false,
});
This is useful for private feedback such as UI confirmation, personal rewards,
or effects only one player should see.
Trigger from a map
map.clientVisual() broadcasts the visual to all players currently on the map:
map.clientVisual("explosion", {
position: { x: 320, y: 180 },
power: 2,
});
And the client can render it at a position:
export default defineModule<RpgClient>({
clientVisuals: {
explosion({ position, data }, helpers) {
helpers.component("explosion", position, {
scale: data.power,
});
helpers.sound("explosion", { volume: 0.8 });
helpers.shake({ intensity: 4, duration: 180 });
},
},
});
Available helpers
helpers.getObject(id)
helpers.flash(target, options)
helpers.showHit(target, text)
helpers.component(id, targetOrPosition, params)
helpers.sound(id, options)
helpers.animation(target, animationName, options)
helpers.shake(options)
These helpers call the same rendering capabilities RPGJS already exposes on the
client. Client visuals only group them behind a named client-side function.
target, source, and object are resolved client objects. You can pass them
directly to helpers, or pass an object id string to helpers that accept a target.
If a target cannot be resolved, the helper quietly does nothing.
Use helpers.component(id, target, params) to display a registered component
animation on an object. Use helpers.component(id, position, params) to display
it at a map position:
helpers.component("explosion", { x: 320, y: 180 }, { scale: 2 })
Use helpers.animation(target, animationName, options) for sprite animations.
Pass graphic when the animation should use a different graphic, and repeat
when it should play more than once:
helpers.animation(target, "attack", { repeat: 1 })
helpers.animation(target, "cast", { graphic: "mage", repeat: 2 })
Payload rules
The payload sent by the server must be serializable:
player.clientVisual("reward", {
targetId: player.id,
itemId: "potion",
amount: 3,
});
Do not send functions, class instances, or rendering components from the
server. Register rendering details in clientVisuals and send only IDs, numbers,
strings, booleans, arrays, and plain objects.
Complete example
This example registers a hit spark component on the client, groups the spark,
flash, hit text, and sound in one client visual, then triggers that visual from
server gameplay code.
// client/module.ts
import { defineModule, RpgClient } from "@rpgjs/client";
import HitSpark from "./hit-spark.ce";
export default defineModule<RpgClient>({
componentAnimations: [
{
id: "hit-spark",
component: HitSpark,
},
],
clientVisuals: {
hit({ target, data }, helpers) {
helpers.flash(target, { type: "tint", tint: "red", duration: 120 });
helpers.showHit(target, `-${data.damage}`);
helpers.component("hit-spark", target, {
scale: data.critical ? 1.4 : 1,
});
helpers.sound(data.critical ? "critical-hit" : "hit");
},
},
});
// server/combat.ts
target.hp -= damage;
player.clientVisual("hit", {
targetId: target.id,
damage,
critical,
});
The server keeps the gameplay result authoritative. The client only decides how
the hit visual is presented.