# Cloudflare server in production Source: https://v5.rpgjs.dev/advanced/cloudflare-server-production Run an authoritative RPGJS MMORPG with Signe rooms and Cloudflare Durable Objects. # Cloudflare server in production `@rpgjs/server/cloudflare` runs the same framework-agnostic server module as the Node adapter. Each Signe namespace and room id is routed deterministically to a Durable Object. WebSockets remain attached during hibernation, while persisted room state lets RPGJS rebuild a new in-memory instance when the object wakes. If this is your first deployment, complete the shared project and publisher setup in [Put an MMORPG online](/guide/deploy-mmorpg) first. This page serves the built browser client and the room Worker from the same public Cloudflare URL. ## Mental model The Worker is the public HTTP and WebSocket entry point. It routes each RPGJS room id to one deterministic Durable Object: ```txt theme={null} Worker /parties/main ├─ lobby-1 → one Durable Object ├─ map-port → one Durable Object └─ map-marsh → one Durable Object ``` Players connected to the same room share the same authoritative simulation. Different map rooms do not share in-memory state, which is why world topology updates must be sent to every affected map room. RPGJS uses Cloudflare's hibernatable WebSocket path. While a room is idle, Cloudflare can remove its JavaScript instance from memory without disconnecting its WebSockets. In-memory physics, chunks, events, and world managers are then lost. RPGJS persists the authoritative map source and world topology first, and rebuilds those transient resources when the Durable Object starts again. Do not treat class properties as durable storage. See Cloudflare's [WebSocket hibernation lifecycle](https://developers.cloudflare.com/durable-objects/best-practices/websockets/) for the underlying runtime behavior. ## 1. Install Wrangler Install the current Wrangler CLI in your starter project: ```bash theme={null} npm install --save-dev wrangler@latest ``` The repository sample pins an older Wrangler release for its GLIBC 2.31 test host. A new project on a current operating system should use Wrangler 4 or later. ## 2. Create the Worker entry Create `src/entries/cloudflare.ts`: ```ts theme={null} import { RpgServerDurableObject, createRpgServerWorker, } from "@rpgjs/server/cloudflare"; import ServerModule from "../server"; export { RpgServerDurableObject }; interface Env extends Record { RPGJS_ROOMS: DurableObjectNamespace; RPGJS_MAP_UPDATE_TOKEN: string; ASSETS: { fetch(request: Request): Promise }; } const rooms = createRpgServerWorker(ServerModule, { binding: "RPGJS_ROOMS", partiesPath: "/parties/main", }); export default { fetch(request: Request, env: Env, ctx: ExecutionContext) { if (new URL(request.url).pathname.startsWith("/parties/")) { return rooms.fetch(request, env, ctx); } return env.ASSETS.fetch(request); }, }; ``` ## 3. Configure the Durable Object and browser assets Configure `RPGJS_ROOMS` as a SQLite Durable Object class in `wrangler.jsonc` and export `RpgServerDurableObject` from the Worker entry: ```jsonc theme={null} { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-rpgjs-game", "main": "src/entries/cloudflare.ts", "compatibility_date": "2026-07-22", "compatibility_flags": ["nodejs_compat"], "assets": { "directory": "./dist/client", "binding": "ASSETS", "not_found_handling": "single-page-application" }, "durable_objects": { "bindings": [ { "name": "RPGJS_ROOMS", "class_name": "RpgServerDurableObject" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["RpgServerDurableObject"] } ] } ``` Use a recent compatibility date and generate bindings with `wrangler types`. Keep the existing migration tags after deployment; add a new migration rather than rewriting an already deployed one. Cloudflare documents the available [Durable Object migration operations](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/). The binding name in `wrangler.jsonc`, the `Env` interface, and `createRpgServerWorker()` must all be exactly `RPGJS_ROOMS`. SQLite-backed Durable Objects persist authoritative room state between Worker instances. ## Maps and worlds are published externally The Worker does not read project files from a filesystem and the browser client is not trusted to configure authoritative maps. An editor, Vite development server, CI job, or administrative backend publishes them with the following server-only flow: ```txt theme={null} trusted publisher ├─ POST /parties/main/map-/map/update └─ POST /parties/main/map-/world//update ``` The endpoints have different responsibilities: | Endpoint | Responsibility | | - | - | | `POST /parties/main/map-/map/update` | Replace one authoritative map revision, including its events, collisions, streaming data, and dimensions. | | `POST /parties/main/map-/world//update` | Replace the world topology used by that map room for automatic map transitions. | Both endpoints accept the same administration token: ```txt theme={null} x-rpgjs-map-update-token: ``` Set the Worker secret interactively: ```bash theme={null} wrangler secret put RPGJS_MAP_UPDATE_TOKEN ``` `createRpgServerWorker()` fails closed for map and world updates when this secret is missing. A map room can be created by a trusted update before its first player connects. RPGJS awaits the Durable Object storage write before acknowledging either update. `createStudioMapUpdatePayload()` includes runtime-ready `worldUpdates` metadata. The RPGJS Vite publisher and the Studio seed command consume it automatically: they publish the selected map, then fan the topology out to every map room in the world. If you build a custom publisher or call the HTTP API directly, you must reproduce both steps. Sending only `/map/update` does not refresh the world manager of a player who is currently on another map. ## Local Vite and Wrangler workflow Configure the Vite plugin with a remote development server: ```ts theme={null} rpgjs({ server: ServerModule, devServer: { target: "http://127.0.0.1:8787", mapIds: ["town"], mapUpdateToken: process.env.RPGJS_MAP_UPDATE_TOKEN, }, }); ``` Vite proxies HTTP and WebSocket traffic under `/parties` to Wrangler. It uses a server-only publisher to send the configured maps on startup and after HMR. If the resolved payload contains `worldUpdates`, the same publisher also calls the world update endpoint for every referenced map room. See `samples/cloudflare-mmorpg` for the complete setup. For sources that need asynchronous preparation, use `resolveMapPayload`. This is particularly important for Studio because the publisher must fetch and normalize the complete v2 map before the room compiles its private authoritative state and public chunks: ```ts theme={null} import { createStudioMapUpdatePayload } from "@rpgjs/studio/server"; rpgjs({ server: ServerModule, devServer: { target: "http://127.0.0.1:8787", mapIds: ["studio-map-id"], mapUpdateToken: process.env.RPGJS_MAP_UPDATE_TOKEN, resolveMapPayload: ({ mapId }) => createStudioMapUpdatePayload(mapId, { projectId: "studio-project-id", startMapId: mapId, }), }, }); ``` The callback runs in Vite's Node process, not in the browser. Do not use a `VITE_`-prefixed secret. The browser receives only nearby render and physics chunks; event rules, project data, the database, and the complete collision model remain in the Durable Object. Renderable media URLs remain public. The Studio playground contains a local Wrangler entry, integration test, fixture, and authenticated seed command: ```bash theme={null} cd playground/games/studio cp .env.example .env.local cp .dev.vars.example .dev.vars pnpm dev:cloudflare pnpm seed:cloudflare ``` `RPG_TYPE=mmorpg` alone keeps the normal Node.js room hosted by Vite and does not require `RPGJS_MAP_UPDATE_TOKEN`. The playground's `dev:cloudflare` and `build:cloudflare` scripts additionally set `RPGJS_SERVER_ADAPTER=cloudflare`; only that adapter enables remote publication and requires the shared secret. It also documents the equivalent `curl` request and how to publish a real Studio project and map. Studio MMORPG streaming accepts v2 maps; v1 maps remain available to the standalone loader. ## First production deployment The following sequence deploys the browser client and Worker, configures the secret, publishes the starter map, and verifies the live game. 1. Authenticate and confirm which Cloudflare account Wrangler uses: ```bash theme={null} npx wrangler login npx wrangler whoami ``` 2. Build the MMORPG client and validate the Worker configuration: ```bash theme={null} npm run build:mmorpg npx wrangler types ./src/worker-configuration.d.ts npx wrangler deploy --dry-run ``` 3. Deploy the Worker. Wrangler prints its public `workers.dev` URL: ```bash theme={null} npx wrangler deploy ``` 4. Create the administration secret through Wrangler's interactive prompt. Do not put the value on the command line: ```bash theme={null} npx wrangler secret put RPGJS_MAP_UPDATE_TOKEN ``` 5. Create an uncommitted `.env.publisher` file on your trusted development or CI machine. Use the URL printed by `wrangler deploy` and the same secret: ```dotenv theme={null} RPGJS_PUBLISH_TARGET=https://my-rpgjs-game..workers.dev RPGJS_MAP_UPDATE_TOKEN=the-secret-entered-in-wrangler RPGJS_MAP_IDS=simplemap ``` 6. Run the publisher created in the beginner guide: ```bash theme={null} node --env-file=.env.publisher --import tsx src/entries/publish-maps.ts ``` A `Published map: simplemap` message confirms that the map and its world updates were accepted. The Durable Object persists them before acknowledging the request. 7. Inspect logs while opening the public URL in two independent browsers: ```bash theme={null} npx wrangler tail ``` For later releases, build and deploy the Worker first, then run the publisher whenever map source or world topology changes. Never rewrite the deployed `v1` migration tag; append a new migration if a future Durable Object class changes. ## Direct HTTP publication Prefer the Vite publisher, the Studio seed command, or a trusted backend because they perform the world fan-out. To inspect the HTTP contract manually, a complete publication uses the same secret for both requests: ```bash theme={null} curl --fail-with-body \ -X POST \ -H 'content-type: application/json' \ -H 'x-rpgjs-map-update-token: ' \ --data-binary @prepared-map.json \ https:///parties/main/map-port/map/update curl --fail-with-body \ -X POST \ -H 'content-type: application/json' \ -H 'x-rpgjs-map-update-token: ' \ --data-binary @prepared-world.json \ https:///parties/main/map-port/world//update ``` `prepared-world.json` can be either `{ "id": "", "maps": [...] }` or the maps array itself. Repeat the second request for every map id contained in that world. A successful `2xx` response means the target room updated its live world manager and persisted the topology. ## Production checklist These commands use the top-level Worker configuration shown above. If you add a named Wrangler environment, append the same `--env ` option to every secret, deploy, and tail command. 1. Authenticate Wrangler with `npx wrangler login` or configure the appropriate CI API token. 2. Build the MMORPG client and Worker entry. 3. Verify the binding and the `new_sqlite_classes` migration in `wrangler.jsonc`. 4. Deploy the Worker and note its public URL: ```bash theme={null} npx wrangler deploy ``` 5. Configure the secret for that Worker through the interactive prompt: ```bash theme={null} npx wrangler secret put RPGJS_MAP_UPDATE_TOKEN ``` 6. Point the trusted publisher at the deployed Worker URL and publish the start map. For Studio worlds, confirm that it reports successful world publication for every map room. 7. Open two clients, change maps in both directions, and inspect production logs with `npx wrangler tail`. Never expose `RPGJS_MAP_UPDATE_TOKEN` in browser code, a `VITE_` variable, a committed `.env` file, or a public build artifact. ## Troubleshooting | Symptom | Cause and check | | - | - | | `401 Unauthorized` | The publisher token does not match the Worker secret. Check `.env.local` against `.dev.vars` locally, or the secret in the selected production environment. | | `503 Missing required Worker secret` | `RPGJS_MAP_UPDATE_TOKEN` is not configured for the Worker environment receiving the request. | | The new map loads only after restarting Wrangler | The publisher sent `/map/update` but did not fan out `/world/:id/update`, or an older Worker bundle is still running. | | The player is blocked at a world boundary | Inspect the current map room's topology. Both source and target map ids, dimensions, and runtime world coordinates must be present. | | A room works until hibernation | Required state was kept only in memory. Republish the map and world with a Worker version that persists both before acknowledgement. | | Local behavior differs from production | Confirm the same adapter, token, project id, map id, and Wrangler environment are used, then inspect `wrangler tail`. | ## Empty maps When the final connection leaves a map, RPGJS stops its 60 Hz simulation loop. Persisted state remains in the Durable Object. On the next connection, RPGJS restores the state and rebuilds transient physics and event resources without simulating elapsed empty-room time. # Custom Gameplay Rooms Source: https://v5.rpgjs.dev/advanced/custom-gameplay-rooms Build synchronized, map-independent RPGJS rooms with custom CanvasEngine scenes. # Custom Gameplay Rooms Custom gameplay rooms are server-authoritative sessions that keep RPGJS players, state synchronization, actions, GUI messages, and session transfer without loading a Tiled map or starting map physics. They fit turn-based battles, card tables, lobbies, matchmaking screens, and other non-spatial game modes. Maps remain unchanged. Use `player.changeMap()` for maps and `player.changeRoom()` for custom rooms. ## Runtime model | Concern | Owner | | - | - | | Players, authorization, actions, and persistent room state | Server | | Synchronized `players` and public `state` projections | RPGJS room synchronization | | Scene layout, input, animations, and local effects | Client CanvasEngine component | | Map loading, movement, collisions, and map physics | Disabled while the custom scene is active | The same API works in standalone and MMORPG modes. In standalone mode, the server room runs in the browser alongside the client. In MMORPG mode, it runs only in the server bundle and remains authoritative. Keep reusable features in separate server and client entry points so server-only code is not imported by the MMORPG browser bundle. ## Define the server room Create a room class, declare a unique `kind` and path, then register it in the server entry: ```ts theme={null} import { createServer, provideServerModules, provideServerRooms, RpgGameplayRoom, RpgRoom, RpgRoomAction, type RpgPlayer, } from "@rpgjs/server" type BattleState = { turn: number lastPlayerId: string | null } @RpgRoom({ kind: "battle", path: "battle-{id}", persistState: true, initialState: () => ({ turn: 1, lastPlayerId: null }), }) export class BattleRoom extends RpgGameplayRoom { @RpgRoomAction("battle.nextTurn") nextTurn(player: RpgPlayer) { this.state.update((state) => ({ turn: state.turn + 1, lastPlayerId: player.id, })) } } export default createServer({ providers: [ provideServerModules([/* your server modules */]), provideServerRooms([BattleRoom]), ], }) ``` The path placeholders are supplied by the server during a transfer. Parameter values cannot contain `/`, `?`, or `#`. Duplicate kinds and duplicate paths fail during bootstrap instead of routing players unpredictably. `RpgGameplayRoom` provides: * `players`, containing synchronized `RpgPlayer` instances; * `state`, a server-authoritative writable signal; * `id`, `params`, and `descriptor`, identifying the resolved room instance; * decorated actions through `@RpgRoomAction()`; * `broadcast()`, `on()`, and `off()` for ephemeral messages; * the standard RPGJS database, save, GUI, and player snapshot behavior. The base class intentionally does not load `RpgEvent` instances. Represent non-spatial entities in the room's serializable state, or synchronize a feature-owned model when a game mode needs more than players. For a reusable module, export the room class or a `rooms` array from a dedicated `/server` entry point. The application should only install that public result: ```ts theme={null} import { provideServerRooms } from "@rpgjs/server" import { battleRooms } from "@my-game/battle/server" export default createServer({ providers: [provideServerRooms(battleRooms)], }) ``` ## Register the client scene Each custom server kind needs one CanvasEngine scene adapter on the client. The root may be a composed `.ce` component and may use `DOMContainer` for HTML UI. `battle-scene.ce`: ```html theme={null} ``` Register that component from the client entry point: ```ts theme={null} import { provideClientScenes, startGame, } from "@rpgjs/client" import BattleScene from "./battle-scene.ce" type BattleState = { turn: number lastPlayerId: string | null } startGame({ providers: [ provideClientScenes([{ kind: "battle", component: BattleScene, onEnter(room, descriptor) { console.log(`Entered ${descriptor.name}`, room.state()) }, onChanges(room) { console.log("Battle state changed", room.state()) }, }]), // Your normal RPGJS client and connection providers. ], }) ``` The component receives the synchronized `room` and its server-provided `descriptor`. When a custom scene is active, RPGJS unmounts the map viewport, lighting, map entities, prediction, streaming, projectiles, and map physics. GUI components remain mounted above the custom scene. Export scenes from a dedicated `/client` entry point when building a reusable module. The application then installs only `provideClientScenes(battleScenes)`; it does not need to know the component tree or socket protocol used internally. `onBeforeEnter` runs while the previous scene is still mounted, `onEnter` runs after the new room connection opens, `onChanges` receives synchronized packets, and `onLeave` runs before another room replaces the scene. Use `onLeave` to remove feature-owned socket listeners and controllers. Use the CanvasEngine component unmount lifecycle for component-local effects, animation loops, DOM listeners, and input handlers. RPGJS clears the synchronized room signals when the scene changes, but it cannot dispose listeners created by the feature itself. ## Public and private state `RpgGameplayRoom.state` is synchronized to every connection in the room. Store only data that every participant and spectator may inspect there. Do not place hidden hands, enemy skills, private inventory contents, secrets, or authorization data in the shared state. Send ephemeral private projections to one player with `player.emit()`. Store durable player-owned data in synchronized player properties and use `$syncWithClient: false` for server-only properties. Every client command must be validated again by a `@RpgRoomAction()` method or another server-side handler; the custom CanvasEngine scene never owns gameplay authority. ## Transfer players Only the server selects the destination: ```ts theme={null} await player.changeRoom({ kind: "battle", params: { id: "encounter-42" }, }) // Return through the unchanged map API. await player.changeMap("town", { x: 320, y: 480 }) ``` The transfer token preserves the RPGJS player identity and snapshot across the room boundary. `player.getCurrentRoom()` returns the active lobby, map, or custom room; `player.getCurrentMap()` is `null` in a map-independent room. Store the return map and position in server-owned match or player state before calling `changeRoom()`. Returning to spatial gameplay must use `changeMap()` so the normal map loading, streaming, movement, and physics lifecycle is restored. Use the generic hooks when behavior should apply to every room: ```ts theme={null} export default { player: { canChangeRoom(player, destination) { return destination.kind !== "battle" || player.level >= 5 }, onJoinRoom(player, room) {}, onLeaveRoom(player, room) {}, }, room: { onJoin(player, room) {}, onLeave(player, room) {}, }, } ``` The existing `canChangeMap`, `onJoinMap`, `onLeaveMap`, and map hooks continue to run for maps. The generic room hooks run alongside them. ## Failure behavior RPGJS rejects an unknown room kind, missing or unsafe path parameters, duplicate server registrations, duplicate client scene kinds, and custom transfers without a matching client scene. These errors are surfaced through the normal client connection-error hook. Register the same room kinds on both runtimes. A server room without a matching client scene cannot be rendered; a client scene without a matching server room cannot be selected by an authoritative transfer. # Latency Simulation Source: https://v5.rpgjs.dev/advanced/latency-simulation Guide for Latency Simulation in RPGJS. # Latency Simulation RPG-JS provides built-in latency simulation capabilities to test how your game behaves under various network conditions. This feature allows you to simulate realistic network delays between the server and clients. ## Overview Latency simulation adds configurable delays to outgoing messages from the server to clients, helping you: * Test game responsiveness under poor network conditions * Validate client-side prediction and reconciliation systems * Ensure smooth gameplay even with high latency * Debug synchronization issues ## Configuration ### Environment Variables You can configure latency simulation using environment variables: ```bash theme={null} # Enable latency simulation RPGJS_ENABLE_LATENCY=true # Set minimum and maximum latency in milliseconds RPGJS_LATENCY_MIN_MS=50 RPGJS_LATENCY_MAX_MS=200 # Optional: Only apply latency to messages containing specific text RPGJS_LATENCY_FILTER=sync ``` ### Programmatic Configuration You can also configure latency simulation programmatically: ```typescript theme={null} import { PartyConnection } from '@rpgjs/vite'; // Enable latency simulation with 100-300ms range PartyConnection.configureLatency(true, 100, 300); // Enable latency only for synchronization messages PartyConnection.configureLatency(true, 50, 150, 'sync'); // Disable latency simulation PartyConnection.configureLatency(false, 0, 0); ``` ## Usage Examples ### Basic Latency Simulation ```typescript theme={null} // In your server configuration PartyConnection.configureLatency(true, 100, 300); // All outgoing messages will now have 100-300ms random delays room.broadcast({ type: 'player_move', data: playerData }); ``` ### Selective Latency ```typescript theme={null} // Only apply latency to synchronization messages PartyConnection.configureLatency(true, 50, 150, 'sync'); // This message will be delayed (contains 'sync') room.broadcast({ type: 'sync', data: worldState }); // This message will be sent immediately (no 'sync' in content) room.broadcast({ type: 'chat', message: 'Hello!' }); ``` ### Combined with Packet Loss You can use latency simulation together with packet loss simulation: ```typescript theme={null} // Configure both packet loss and latency PartyConnection.configurePacketLoss(true, 0.1); // 10% packet loss PartyConnection.configureLatency(true, 100, 300); // 100-300ms latency // Messages will first be delayed, then potentially dropped room.broadcast({ type: 'update', data: gameState }); ``` ## Monitoring The server logs will show latency simulation activity: ``` [LATENCY SIMULATION] Connection conn_123: Delaying message by 156.7ms [MESSAGE DATA] {"type":"sync","data":{"players":[...]}} ``` ## Best Practices ### 1. Realistic Latency Values Use realistic latency ranges based on your target network conditions: * **Local/LAN**: 1-10ms * **Good Internet**: 20-100ms * **Poor Internet**: 100-500ms * **Mobile/3G**: 200-1000ms ### 2. Selective Application Use filters to apply latency only to specific message types: ```typescript theme={null} // Apply high latency to world updates PartyConnection.configureLatency(true, 200, 500, 'world_update'); // Apply low latency to critical messages PartyConnection.configureLatency(true, 10, 50, 'critical'); ``` ### 3. Testing Different Scenarios Test various latency scenarios: ```typescript theme={null} // Test with very high latency PartyConnection.configureLatency(true, 500, 1000); // Test with variable latency (simulates unstable connections) PartyConnection.configureLatency(true, 50, 300); // Test with consistent low latency PartyConnection.configureLatency(true, 20, 30); ``` ### 4. Client-Side Considerations When latency simulation is enabled, ensure your client: * Implements proper client-side prediction * Uses interpolation for smooth movement * Handles message ordering correctly * Provides visual feedback for network conditions ## API Reference ### PartyConnection.configureLatency() Configures latency simulation settings. **Parameters:** * `enabled` (boolean): Whether to enable latency simulation * `minMs` (number): Minimum latency in milliseconds * `maxMs` (number): Maximum latency in milliseconds * `filter` (string, optional): Only apply latency to messages containing this string ### PartyConnection.getLatencyStatus() Returns current latency simulation configuration. **Returns:** ```typescript theme={null} { enabled: boolean; minMs: number; maxMs: number; filter: string; } ``` ## Troubleshooting ### Messages Not Being Delayed 1. Check if latency simulation is enabled: `PartyConnection.getLatencyStatus()` 2. Verify the filter string matches your message content 3. Ensure `maxMs` is greater than 0 ### Performance Issues 1. High latency values may affect server performance 2. Consider using filters to limit which messages are delayed 3. Monitor server logs for excessive delay messages ### Client Disconnections 1. Very high latency may cause client timeouts 2. Adjust client-side timeout settings accordingly 3. Consider implementing heartbeat mechanisms # MMORPG entries Source: https://v5.rpgjs.dev/advanced/mmorpg-entries Organize client, server, and adapter entry points for an MMORPG project, with an Express example. # MMORPG entries When `RPG_TYPE=mmorpg`, RPGJS now treats your project as a multi-entry application: * a browser client * a framework-agnostic server module * optional hosting adapters such as Express Use this layout when you want to keep your game server portable across: * Vite development * Express in production * another Node host such as Fastify or Hono * a custom deployment target later `RPG_TYPE` defaults to `rpg` when it is not set. If this is your first online deployment, start with [Put an MMORPG online](/guide/deploy-mmorpg), which covers local verification, map publication, Node/Docker, and Cloudflare. ## Recommended structure Keep your game logic in `src/server.ts`, and put host-specific bootstraps in `src/entries`. ```txt theme={null} src/ client.ts server.ts standalone.ts entries/ express.ts ``` Role of each file: * `src/client.ts`: browser entry for `mmorpg` * `src/server.ts`: RPGJS server module, without `express()`, `listen()`, or platform code * `src/entries/express.ts`: Node entry that mounts `src/server.ts` in Express * `src/standalone.ts`: browser entry for `RPG_TYPE=rpg` ## Configure `rpgjs()` Declare the MMORPG entries explicitly in your Vite config. ```ts theme={null} import { defineConfig } from "vite" import { rpgjs, tiledMapFolderPlugin } from "@rpgjs/vite" import vue from "@vitejs/plugin-vue" import serverModule from "./src/server" export default defineConfig({ plugins: [ vue(), tiledMapFolderPlugin({ sourceFolder: "./src/tiled", publicPath: "/map", buildOutputPath: "assets/data", // Keep authoritative Tiled sources out of the browser build. allowedExtensions: [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"], }), ...rpgjs({ server: serverModule, entryPoints: { rpg: "./src/standalone.ts", mmorpg: { client: "./src/client.ts", server: "./src/server.ts", adapters: { express: "./src/entries/express.ts", }, }, }, }), ], }) ``` ## Keep `src/server.ts` agnostic `src/server.ts` should export your RPGJS server module and nothing else. It should not: * create an HTTP server * call `listen()` * import Express directly * depend on a specific host runtime Example: ```ts theme={null} import { createServer, provideServerModules } from "@rpgjs/server" import mainServerModule from "./modules/server" export default createServer({ providers: [ provideServerModules([mainServerModule]), ], }) ``` This file is the canonical server entry for your game. Vite development can use it directly, and production adapters can import it without duplicating game setup. ## Express adapter example Put your Express bootstrap in `src/entries/express.ts`. ```ts theme={null} import express from "express" import { createServer } from "node:http" import { dirname, join, resolve } from "node:path" import { fileURLToPath } from "node:url" import { WebSocketServer } from "ws" import { createRpgServerTransport } from "@rpgjs/server/node" import serverModule from "../server" const app = express() const httpServer = createServer(app) const wsServer = new WebSocketServer({ noServer: true }) const transport = createRpgServerTransport(serverModule) const currentDir = dirname(fileURLToPath(import.meta.url)) const clientDistDir = resolve(currentDir, "../client") const clientIndexFile = join(clientDistDir, "index.html") app.use("/parties", async (req, res, next) => { await transport.handleNodeRequest(req, res, next, { mountedPath: "/parties", }) }) app.use(express.static(clientDistDir)) app.get(/.*/, (_req, res) => { res.sendFile(clientIndexFile) }) httpServer.on("upgrade", (request, socket, head) => { void transport.handleUpgrade(wsServer, request, socket, head) }) httpServer.listen(3000) ``` This adapter is responsible for: * serving the built client from `dist/client` * forwarding HTTP requests under `/parties` * forwarding WebSocket upgrades to the RPGJS transport ## Build output Run the MMORPG build with: ```bash theme={null} RPG_TYPE=mmorpg vite build ``` RPGJS produces two output folders: ```txt theme={null} dist/ client/ index.html assets/ server/ server.js express.js ``` Meaning of each output: * `dist/client`: browser assets containing client and explicitly shared modules * `dist/server/server.js`: built version of your agnostic `src/server.ts` * `dist/server/express.js`: built version of your Express adapter The MMORPG client build does not include modules imported only by `src/server.ts`. Conversely, client-only modules and components are not part of the server entry. Files imported by both entry graphs are shared code and may be present in both outputs. Do not import a server module, a server adapter, or a combined module index from the client entry graph. Any file reachable from browser code can be included in `dist/client`. Keep secrets in the deployment environment, never in source code that a browser build can reach. ### Keep source maps server-side Map assets follow the same entry boundary as modules. In MMORPG mode, install the server side of a map provider in `src/server.ts`; its compiler reads the complete map and streams only nearby render and prediction chunks. The client side applies those chunks but does not fetch the source document. For Tiled, expose image files through `tiledMapFolderPlugin()` and omit `.tmx` and `.tsx` from `allowedExtensions`. Event definitions, object properties, collision rules and the raw tile layer stay in the map room. The same provider module works with a Node.js transport or a Cloudflare Durable Object. Start the built Express server with: ```bash theme={null} node dist/server/express.js ``` ## Development flow During development, Vite still uses the server module you pass to `rpgjs({ server })`. That means: * `src/server.ts` stays the single source of truth for your game server * your adapter files are only needed when you want a specific production host * you do not need a separate `server-dev.ts` ## Add another host later To support another runtime, keep `src/server.ts` unchanged and add another adapter file: * `src/entries/fastify.ts` * `src/entries/hono.ts` * `src/entries/custom.ts` Then declare it in `entryPoints.mmorpg.adapters`. ```ts theme={null} mmorpg: { client: "./src/client.ts", server: "./src/server.ts", adapters: { express: "./src/entries/express.ts", fastify: "./src/entries/fastify.ts", }, } ``` The build will emit one file per adapter in `dist/server`. ## Related pages * For the complete beginner deployment path, see [/guide/deploy-mmorpg](/guide/deploy-mmorpg) * For the Node transport API and protected map updates, see [/advanced/node-server-production](/advanced/node-server-production) * For a Cloudflare Durable Object deployment, see [/advanced/cloudflare-server-production](/advanced/cloudflare-server-production) # Node server in production Source: https://v5.rpgjs.dev/advanced/node-server-production Run RPGJS without Vite in production, secure map updates, and mount the transport in a Node HTTP server. # Node server in production `@rpgjs/server/node` lets you run the RPGJS server runtime without Vite. If this is your first deployment, complete the shared setup in [Put an MMORPG online](/guide/deploy-mmorpg) first. This page then takes the starter through a Docker deployment. The container serves the browser client, HTTP room routes, and WebSockets from the same public origin. If you want to structure an MMORPG project with a framework-agnostic `src/server.ts` plus host-specific entries such as Express, read [/advanced/mmorpg-entries](/advanced/mmorpg-entries) first. Use it when you want to mount the server in your own Node stack: * Express * Fastify * Hono on Node * a custom `http.createServer()` setup ## Dev vs production In development with `@rpgjs/vite`, Vite acts as a trusted map publisher. It builds the map payload and calls the same administration endpoint used by an editor or deployment pipeline. Gameplay clients never publish map definitions. In production, map updates must come from a trusted backend source. To protect `/map/update`, set `RPGJS_MAP_UPDATE_TOKEN`. When this environment variable is set: * gameplay clients cannot update maps * trusted backend code must send the token * you can use `transport.updateMap()` or call the HTTP endpoint yourself ## 1. Install the Node adapter dependencies From the starter project: ```bash theme={null} npm install express ws npm install --save-dev @types/express @types/ws ``` `express` and `ws` must be production dependencies because the built adapter imports them when the container starts. ## 2. Create the transport Create `src/entries/express.ts`. This version fails at startup if the map update token is missing, stores room state on a configurable SQLite path, serves the built client, and exposes a health check for the container host. ```ts theme={null} import http from "node:http" import express from "express" import { dirname, join, resolve } from "node:path" import { fileURLToPath } from "node:url" import { WebSocketServer } from "ws" import { createRpgServerTransport, createSqliteNodeRoomStorage, } from "@rpgjs/server/node" import ServerModule from "../server" const mapUpdateToken = process.env.RPGJS_MAP_UPDATE_TOKEN if (!mapUpdateToken) { throw new Error("RPGJS_MAP_UPDATE_TOKEN is required") } const app = express() const server = http.createServer(app) const wsServer = new WebSocketServer({ noServer: true }) const currentDir = dirname(fileURLToPath(import.meta.url)) const clientDistDir = resolve(currentDir, "../client") const clientIndexFile = join(clientDistDir, "index.html") const port = Number(process.env.PORT ?? 3000) const transport = createRpgServerTransport(ServerModule, { initializeMaps: false, mapUpdateToken, storage: createSqliteNodeRoomStorage({ databasePath: process.env.RPGJS_ROOM_DB ?? "./data/rooms.sqlite", }), }) app.get("/healthz", (_req, res) => { res.status(200).send("ok") }) app.use("/parties", (req, res, next) => { void transport.handleNodeRequest(req, res, next, { mountedPath: "/parties", }) }) app.use(express.static(clientDistDir)) app.get(/.*/, (_req, res) => { res.sendFile(clientIndexFile) }) server.on("upgrade", (request, socket, head) => { void transport.handleUpgrade(wsServer, request, socket, head) }) server.listen(port, "0.0.0.0", () => { console.log(`RPGJS server listening on port ${port}`) }) ``` ## 3. Build the MMORPG Configure the `entryPoints.mmorpg.adapters.express` entry and the `build:mmorpg` script shown in [Put an MMORPG online](/guide/deploy-mmorpg), then run: ```ts theme={null} mmorpg: { client: "./src/client.ts", server: "./src/server.ts", adapters: { express: "./src/entries/express.ts", }, } ``` ```bash theme={null} npm run build:mmorpg ``` The relevant output is: ```txt theme={null} dist/ client/ server/ server.js express.js ``` Verify that `dist/client` contains images and browser assets but no `.tmx` or `.tsx` source maps. ## 4. Create the Docker image Create `Dockerfile` at the project root: ```dockerfile theme={null} FROM node:22-bookworm-slim AS build WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci COPY . . RUN npm run build:mmorpg FROM node:22-bookworm-slim AS runtime ENV NODE_ENV=production ENV PORT=3000 ENV RPGJS_ROOM_DB=/data/rooms.sqlite WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --omit=dev COPY --from=build /app/dist ./dist RUN mkdir -p /data VOLUME ["/data"] EXPOSE 3000 CMD ["node", "dist/server/express.js"] ``` Create `.dockerignore`: ```gitignore theme={null} .git .env* data dist node_modules ``` Build and test the same image that you will deploy: ```bash theme={null} docker build -t my-rpgjs-game . ``` Create an uncommitted `.env.production` file: ```dotenv theme={null} RPGJS_MAP_UPDATE_TOKEN=replace-with-a-long-random-secret ``` Make sure `.env.production` is ignored by Git, then run: ```bash theme={null} docker volume create rpgjs-rooms docker run --rm \ --env-file .env.production \ -p 3000:3000 \ -v rpgjs-rooms:/data \ my-rpgjs-game ``` Open `http://localhost:3000/healthz`, then `http://localhost:3000`. ## 5. First production deployment Push the image to the registry supported by your container host. Configure the host with: * container port `3000` * `RPGJS_MAP_UPDATE_TOKEN` as a secret * a persistent volume mounted at `/data` * health check path `/healthz` * HTTPS with WebSocket upgrades enabled Use one container replica. The SQLite Node adapter coordinates one process; starting several independent replicas would create separate room state. Your host supplies the public URL, for example `https://game.example.com`. Create an uncommitted `.env.publisher` on the trusted development or CI machine: ```dotenv theme={null} RPGJS_PUBLISH_TARGET=https://game.example.com RPGJS_MAP_UPDATE_TOKEN=the-same-secret-configured-on-the-host RPGJS_MAP_IDS=simplemap ``` Publish the authoritative maps using the script from the beginner guide: ```bash theme={null} node --env-file=.env.publisher --import tsx src/entries/publish-maps.ts ``` Do this after the server is healthy on the first deployment and whenever a map changes. A `Published map: simplemap` message confirms that the server accepted and persisted the update. Then open the public URL in two independent browser sessions. ## Push trusted map updates from application code If your map data is produced inside the same trusted Node process, use `transport.updateMap()`. ```ts theme={null} await transport.updateMap("town", { id: "town", width: 3200, height: 2400, events: [], data: tiledXml, }) ``` `transport.updateMap("town", ...)` targets the room `map-town` automatically. ## Call `/map/update` from another trusted backend If your editor pipeline, admin API, or deploy step runs outside the game server process, call the endpoint directly with the token. Endpoint format: ```txt theme={null} POST /parties/main/map-/map/update ``` Example: ```ts theme={null} import { createMapUpdateHeaders } from "@rpgjs/server/node" await fetch("http://localhost:3000/parties/main/map-town/map/update", { method: "POST", headers: createMapUpdateHeaders(process.env.RPGJS_MAP_UPDATE_TOKEN), body: JSON.stringify({ id: "town", width: 3200, height: 2400, events: [], data: tiledXml, }), }) ``` You can also send the token as: * `x-rpgjs-map-update-token: ` * `Authorization: Bearer ` ## Recommended production flow 1. Start your Node server with `RPGJS_MAP_UPDATE_TOKEN` set. 2. Mount `transport.handleNodeRequest()` and `transport.handleUpgrade()`. 3. Keep `initializeMaps: false` in production. 4. From a trusted backend source, call `transport.updateMap()` or `POST /parties/main/map-/map/update`. 5. Let gameplay clients use only normal movement and game actions. The memory storage remains the default for tests and short-lived development servers. Configure SQLite for a production process so synchronized room state and sessions survive a restart. This Node adapter deliberately targets one process; horizontal coordination is a separate World/Shard deployment concern. Room SQLite storage is not the same as long-term player save storage. The starter's `LocalStorageSaveStorageStrategy` only works for standalone browser games. Configure a server-side save strategy before relying on save slots or account persistence in an MMORPG. ## Memory snapshots and SQLite ownership The memory provider can snapshot all party and room values for a test, a local tool, or an explicit short-lived backup: ```ts theme={null} import { createMemoryNodeRoomStorage } from "@rpgjs/server/node" const storage = createMemoryNodeRoomStorage() const room = await storage.getStorage("main", "map-town") await room.put("weather", { kind: "rain" }) const snapshot = storage.snapshot() storage.clear() storage.restore(snapshot) // Reacquire handles after clear() or restore(). const restoredRoom = await storage.getStorage("main", "map-town") console.log(await restoredRoom.get("weather")) ``` `snapshot()` returns an independent representation of the current data. `clear()` and `restore()` replace the provider's room registry, so reacquire room handles afterward. Memory data is process-local and disappears when the process exits unless the application persists the snapshot itself. For SQLite, pass exactly one database source: * `databasePath` lets RPGJS open and own the SQLite connection; * `database` reuses a compatible connection owned by the application. Passing neither source or both sources is invalid. A filesystem path is the simplest production configuration for the single-process Node adapter shown above. ## WebSocket session ids The Node transport uses the RPGJS room adapter and follows its session model: * `conn.id` is unique for each active WebSocket connection. * `conn.sessionId` is the stable private session id sent by the client. `provideMmorpg()` sends this stable id through PartySocket, so a browser refresh or a second tab can restore the same player session without replacing the first active WebSocket. When you need to address or exclude a single physical socket, use `conn.id`; when you need to inspect the restored player session, use `conn.sessionId`. ## Hono and other runtimes The same transport can be used outside Express: * use `transport.fetch()` when your framework exposes Fetch `Request`/`Response` * use `transport.handleNodeRequest()` when your framework gives Node `req`/`res` * use `transport.acceptWebSocket()` or `transport.handleUpgrade()` for WebSocket upgrades ## Security note Do not expose `/map/update` without a token in public MMORPG production deployments. `/map/update` is a trusted server-side operation. It can redefine map geometry, events, and world metadata. ## Development with Vite With `@rpgjs/vite`, you do not need this manual flow for local development. The Vite plugin creates the transport internally and performs the server-side map bootstrap automatically. # Packet Loss Simulation Source: https://v5.rpgjs.dev/advanced/packet-loss-simulation Guide for Packet Loss Simulation in RPGJS. # Packet Loss Simulation The RPG-JS Vite plugin includes a built-in packet loss simulation feature that allows you to test how your game behaves under poor network conditions. ## Overview This feature simulates real-world network conditions by randomly preventing the server from processing certain messages received from clients, simulating what happens when the server doesn't receive certain packets due to network issues. This is useful for: * Testing server-side message handling under poor network conditions * Validating game state synchronization when messages are lost * Ensuring your server handles missing client messages gracefully * Debugging network-related issues in multiplayer games ## Configuration ### Environment Variables You can configure packet loss simulation using environment variables: ```bash theme={null} # Enable packet loss simulation export RPGJS_ENABLE_PACKET_LOSS=true # Set packet loss rate (0.0 to 1.0, where 0.1 = 10% packet loss) export RPGJS_PACKET_LOSS_RATE=0.15 # Optional: Only simulate loss for messages containing specific text export RPGJS_PACKET_LOSS_FILTER="sync" ``` ### Programmatic Configuration You can also configure packet loss simulation programmatically: ```typescript theme={null} import { PartyConnection } from '@rpgjs/vite'; // Enable 20% packet loss for all incoming messages PartyConnection.configurePacketLoss(true, 0.2); // Enable 15% packet loss only for sync messages PartyConnection.configurePacketLoss(true, 0.15, 'sync'); // Disable packet loss simulation PartyConnection.configurePacketLoss(false, 0); // Get current status const status = PartyConnection.getPacketLossStatus(); console.log(status); // { enabled: true, rate: 0.2, filter: 'sync' } ``` ## Usage Examples ### Basic Setup ```typescript theme={null} // vite.config.ts import { defineConfig } from 'vite'; import { serverPlugin } from '@rpgjs/vite'; import startServer from './src/server'; export default defineConfig({ plugins: [ serverPlugin(startServer) ] }); ``` ### Testing Different Network Conditions ```typescript theme={null} // Test with 5% packet loss (good connection) PartyConnection.configurePacketLoss(true, 0.05); // Test with 25% packet loss (poor connection) PartyConnection.configurePacketLoss(true, 0.25); // Test with 50% packet loss (very poor connection) PartyConnection.configurePacketLoss(true, 0.5); // Test with 20% packet loss only for synchronization messages PartyConnection.configurePacketLoss(true, 0.2, 'sync'); // Test with 30% packet loss only for player movement messages PartyConnection.configurePacketLoss(true, 0.3, 'playerMove'); ``` ## Console Output When packet loss simulation is enabled, you'll see colored logs in your terminal: * **Green**: `[PACKET RECEIVED]` - Message will be processed by server * **Red**: `[PACKET LOSS]` - Message dropped (server won't process it) * **Yellow**: `[PACKET DATA]` - Content of the dropped message * **Purple**: `[PACKET LOSS SIMULATION]` - Configuration changes * **Cyan**: `[NETWORK SIMULATION]` - Startup status Example output: ``` [PACKET LOSS SIMULATION] Enabled with 15.0% loss rate (filtered: "sync") [NETWORK SIMULATION] Packet loss simulation: 15.0% loss rate (filter: "sync") [PACKET RECEIVED] Connection conn_123: Server will process this message [PACKET LOSS] Connection conn_123: Server won't receive this message (15.0% loss rate) [PACKET DATA] {"type":"sync","playerId":"player1","position":{"x":100,"y":200}}... ``` ## Best Practices 1. **Use realistic rates**: 5-15% packet loss is common in poor network conditions 2. **Test incrementally**: Start with low rates and gradually increase 3. **Use filters strategically**: Target specific message types (like 'sync' or 'playerMove') to test critical game systems 4. **Monitor server behavior**: Ensure your server handles missing messages gracefully 5. **Test reconnection**: Verify that clients can reconnect and sync state properly 6. **Disable in production**: Always disable packet loss simulation in production builds ## Packet Filtering You can use the `RPGJS_PACKET_LOSS_FILTER` environment variable or the filter parameter in `configurePacketLoss()` to only simulate packet loss for specific types of messages. This is useful for: * Testing only synchronization messages: `filter: 'sync'` * Testing only player movement: `filter: 'playerMove'` * Testing only battle updates: `filter: 'battle'` When a filter is set, only messages containing the filter string will be subject to packet loss simulation. All other messages will be processed normally. ## Implementation Details The packet loss simulation works by: 1. Intercepting all incoming WebSocket messages in the `ws.on("message")` handler 2. Generating a random number between 0 and 1 3. Comparing against the configured loss rate 4. Either processing the message normally or dropping it with a log message This simulation only affects messages sent from client to server, simulating what happens when the server doesn't receive certain packets due to network issues. Server-to-client messages are not affected. ## Troubleshooting ### Packet Loss Not Working 1. Verify environment variables are set correctly 2. Check that `RPGJS_ENABLE_PACKET_LOSS=true` 3. Ensure `RPGJS_PACKET_LOSS_RATE` is greater than 0 4. Look for the startup log message confirming configuration ### Too Much Packet Loss * Reduce the `RPGJS_PACKET_LOSS_RATE` value * Use values between 0.05 (5%) and 0.25 (25%) for realistic testing ### No Logs Appearing * Ensure your client is sending messages to the server * Check that WebSocket connections are established * Verify console output is not being filtered # provideLoadMap Source: https://v5.rpgjs.dev/advanced/provide-load-map Guide for provideLoadMap in RPGJS. # provideLoadMap The `provideLoadMap` function allows you to customize how maps are loaded and displayed on the client side. It enables you to provide a custom map component and define how map data is processed before rendering. ## Overview `provideLoadMap` is a client-side function that takes a callback function which receives a map ID and returns map data along with a custom component for rendering. This is particularly useful when you want to: * Use custom map formats (like Tiled TMX files) * Display maps with custom rendering components * Process map data before rendering * Add custom layers or effects to your maps ## Basic Usage ```ts theme={null} import { provideLoadMap } from '@rpgjs/client' import { createModule } from '@rpgjs/common' import MyMapComponent from './MyMapComponent.ce' export function provideCustomMap() { return createModule("CustomMap", [ provideLoadMap(async (mapId) => { // Load your map data const response = await fetch(`/maps/${mapId}.json`) const mapData = await response.json() return { data: mapData, // Raw map data component: MyMapComponent, // CanvasEngine component width: mapData.width, // Map width in pixels height: mapData.height, // Map height in pixels events: mapData.events // Optional: map events } }) ]) } ``` ## Return Object Properties The callback function must return an object with the following properties: ### Required Properties * **`data`** - The raw map data that will be passed to your component * **`component`** - A CanvasEngine component that will render the map ### Optional Properties * **`width`** - Map width in pixels (used for viewport calculations) * **`height`** - Map height in pixels (used for viewport calculations) * **`events`** - Map events data * **`id`** - Map identifier (defaults to the mapId parameter) * **`hitboxes`** - Array of collision hitboxes for the map * **`positions`** - Named positions, used by `player.changeMap("map", "name")` ## Creating a Map Component Your map component should be a CanvasEngine component that receives the map data through props: ```html theme={null} ``` ### Accessing Props Data In your component, use `defineProps()` to access the map data: ```ts theme={null} const { data } = defineProps() // The data is a signal, call it to get the actual value const mapData = data() ``` ## Event Layer Integration To display game events (NPCs, interactive objects, etc.), include the `EventLayerComponent` in your map component: ```html theme={null} ``` The `EventLayerComponent` automatically handles: * Player character rendering * NPC and event rendering * Character animations and interactions * Proper layering and sorting ## Composing the SceneMap If you want to add scene-level components without changing each map renderer, define a client `sceneMap.component` and wrap the built-in `SceneMap` component: ```ts theme={null} import { RpgClient, defineModule } from '@rpgjs/client' import MyScene from './my-scene.ce' export default defineModule({ sceneMap: { component: MyScene } }) ``` ```html theme={null} ``` Use `SceneMap` children for overlays or scene systems that should be mounted around the current map renderer. Use `EventLayerComponent` children when the custom elements must be sorted by `zIndex` with players and events. ### Adding Custom Elements to EventLayerComponent You can add custom elements inside `EventLayerComponent` that will be automatically sorted by zIndex with the rest of the elements: ```html theme={null} ``` ## Hitboxes Configuration The `hitboxes` property allows you to define collision areas for the map. Each hitbox can be either rectangular or polygonal: ### Rectangular Hitboxes ```ts theme={null} return { data: mapData, component: MyMapComponent, width: 2048, height: 1536, hitboxes: [ { id: "wall1", // Optional: unique identifier x: 100, // X position in pixels y: 50, // Y position in pixels width: 32, // Width in pixels height: 128 // Height in pixels }, { id: "obstacle1", x: 300, y: 200, width: 64, height: 64 } ] } ``` ### Polygonal Hitboxes ```ts theme={null} return { data: mapData, component: MyMapComponent, width: 2048, height: 1536, hitboxes: [ { id: "triangle1", // Optional: unique identifier points: [ // Array of [x, y] coordinates [100, 100], [150, 50], [200, 100] ] }, { id: "complex-shape", points: [ [400, 300], [450, 250], [500, 300], [450, 350] ] } ] } ``` ### Mixed Hitboxes You can combine both rectangular and polygonal hitboxes in the same array: ```ts theme={null} return { data: mapData, component: MyMapComponent, width: 2048, height: 1536, hitboxes: [ // Rectangular hitbox { id: "wall1", x: 100, y: 50, width: 32, height: 128 }, // Polygonal hitbox { id: "triangle1", points: [ [100, 100], [150, 50], [200, 100] ] } ] } ``` **Note:** If no `id` is provided, a unique identifier will be automatically generated for each hitbox. # Advanced Signe Extension Points Source: https://v5.rpgjs.dev/advanced/signe-extension-points Understand when direct Signe APIs are appropriate in an RPGJS plugin. # Advanced Signe Extension Points RPGJS uses Signe internally for dependency injection, reactivity, synchronization, and room coordination. Normal gameplay modules should use RPGJS-owned contracts such as `RpgWritableSignal`, `RpgProvider`, `RpgContext`, and the Node or Cloudflare adapters. ## Prefer the stable RPGJS surface Import stable provider and signal contracts from `@rpgjs/common`. This keeps game code independent of the internal Signe version: ```ts theme={null} import type { RpgContext, RpgFactoryProvider, RpgWritableSignal, } from "@rpgjs/common" interface MatchmakingService { queue: RpgWritableSignal } export const matchmakingProvider = { provide: "matchmaking", async useFactory(context: RpgContext): Promise { return context.get>("matchmaking-bootstrap") }, } satisfies RpgFactoryProvider ``` Provider factories may be asynchronous. Object providers must specify exactly one of `useValue`, `useClass`, `useFactory`, or `useExisting`. Use the gameplay synchronization APIs described in [Synchronization](/guide/synchronization) before reaching for low-level signals. Use [Custom Gameplay Rooms](/advanced/custom-gameplay-rooms) for map-independent synchronized sessions before defining a low-level room protocol. See [Migrate from v4 to v5](/migration/v4-to-v5) when replacing types re-exported by an early v5 beta. ## When direct Signe usage is appropriate Use Signe directly only when an advanced plugin needs behavior absent from the stable RPGJS surface, such as: * defining a low-level room protocol beyond the RPGJS gameplay-room contract; * consuming Signe-specific signal observables; * implementing reusable infrastructure whose public boundary stays independent of Signe. ```ts theme={null} import { signal } from "@signe/reactive"; import { sync } from "@signe/sync"; class PluginState { @sync() value = signal(0); } ``` When a plugin imports `@signe/*` directly: * declare every imported Signe package in the plugin's own dependencies; * pin a compatible version instead of relying on RPGJS transitive dependencies; * keep Signe-specific values behind the plugin boundary; * expose RPGJS-owned or plugin-owned structural types to game code. Direct Signe APIs follow Signe's compatibility lifecycle. They are not part of the stable RPGJS gameplay compatibility promise. A Signe type intentionally exposed by a future stable RPGJS API would instead be documented and versioned as an RPGJS public contract. # API overview Source: https://v5.rpgjs.dev/api Overview of the RPGJS API reference for player, map, client, and hooks. # API overview Use this section to browse the generated RPGJS reference by domain. ## Sections * [Player API](/api/player/index) * [Map API](/api/map/index) * [Client API](/api/client/index) * [Hooks reference](/hooks/client-engine-hooks) # Client API Source: https://v5.rpgjs.dev/api/client/index Reference for the client engine and client-side runtime helpers. # Client API This reference is generated from the JSDoc comments in `@rpgjs/client`. ## Pages * [Rpg Client Engine](/api/client/rpg-client-engine) * [Hotbar Activation](/api/client/hotbar-activation) # Rpg Client Engine Source: https://v5.rpgjs.dev/api/client/rpg-client-engine Reference for the `RpgClientEngine` class. # Rpg Client Engine Reference for the `RpgClientEngine` class. ## Members * [activeRoom](#activeroom) * [activeRoomSceneComponent](#activeroomscenecomponent) * [activeSceneKind](#activescenekind) * [addComponentAnimation](#addcomponentanimation) * [addEventComponentResolver](#addeventcomponentresolver) * [addSound](#addsound) * [addSpriteComponentBehind](#addspritecomponentbehind) * [addSpriteComponentInFront](#addspritecomponentinfront) * [cameraFollowRevision](#camerafollowrevision) * [cameraFollowSmoothMove](#camerafollowsmoothmove) * [cameraFollowTargetId](#camerafollowtargetid) * [clear](#clear) * [clearClientPredictionStates](#clearclientpredictionstates) * [connect](#connect) * [dashDefaults](#dashdefaults) * [disconnect](#disconnect) * [flash](#flash) * [getAvailableLocales](#getavailablelocales) * [getComponentAnimation](#getcomponentanimation) * [getCurrentRoom](#getcurrentroom) * [getLocale](#getlocale) * [getLocalePreference](#getlocalepreference) * [getSound](#getsound) * [getSoundVolume](#getsoundvolume) * [getSpriteComponent](#getspritecomponent) * [getSpriteSheet](#getspritesheet) * [interactions](#interactions) * [interruptCurrentPlayerMovement](#interruptcurrentplayermovement) * [mapShakeTrigger](#mapshaketrigger) * [music](#music) * [playClientVisual](#playclientvisual) * [playSound](#playsound) * [pointer](#pointer) * [processAction](#processaction) * [processDash](#processdash) * [registerClientVisual](#registerclientvisual) * [registerClientVisuals](#registerclientvisuals) * [registerSpriteComponent](#registerspritecomponent) * [resolveEventComponent](#resolveeventcomponent) * [sceneRoom](#sceneroom) * [setCameraFollow](#setcamerafollow) * [setKeyboardControls](#setkeyboardcontrols) * [setLocale](#setlocale) * [setSoundResolver](#setsoundresolver) * [setSoundVolume](#setsoundvolume) * [setSpritesheetResolver](#setspritesheetresolver) * [startTransition](#starttransition) * [stopAllSounds](#stopallsounds) * [stopSound](#stopsound) * [visualPause](#visualpause) ## activeRoom Authoritative descriptor of the active room. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `property` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} activeRoom ``` ## activeRoomSceneComponent CanvasEngine component selected for a custom gameplay room. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `property` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} activeRoomSceneComponent ``` ## activeSceneKind Active room kind; `map` keeps the historical map pipeline enabled. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `property` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} activeSceneKind ``` ## addComponentAnimation Add a component animation to the engine Component animations are temporary visual effects that can be displayed on sprites or objects, such as hit indicators, spell effects, or status animations. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} addComponentAnimation(componentAnimation: { component: any, id: string }) ``` ### Parameters * `componentAnimation`: `{ component: any, id: string }` ### Returns The added component animation configuration ### Examples ```ts theme={null} // Add a hit animation component engine.addComponentAnimation({ id: 'hit', component: HitComponent }); // Add an explosion effect component engine.addComponentAnimation({ id: 'explosion', component: ExplosionComponent }); ``` ## addEventComponentResolver Register a custom event component resolver. The last resolver returning a component wins. This lets later modules override earlier defaults without replacing the whole map scene. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} addEventComponentResolver(resolver: EventComponentResolver) ``` ### Parameters * `resolver`: `EventComponentResolver` ### Returns The registered resolver ## addSound Add a sound to the engine Adds a sound to the engine's sound cache. The sound can be: * A simple object with `id` and `src` properties * A Howler instance * An object with a `play()` method If the sound has a `src` property, a Howler instance will be created automatically. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} addSound(sound: any, id?: string): any ``` ### Parameters * `sound`: `any` * `id?`: `string` ### Returns The added sound ### Examples ```ts theme={null} // Simple sound object engine.addSound({ id: 'click', src: 'click.mp3' }); // With explicit ID engine.addSound({ src: 'music.mp3' }, 'background-music'); ``` ## addSpriteComponentBehind Add a component to render behind sprites Components added with this method will be displayed with a lower z-index than the sprite Supports multiple formats: 1. Direct component: `ShadowComponent` 2. Configuration object: `{ component: LightHalo, props: {...} }` 3. With dynamic props: `{ component: LightHalo, props: (object) => {...} }` 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }` Components with dependencies will only be displayed when all dependencies are resolved (!= undefined). The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} addSpriteComponentBehind(component: any) ``` ### Parameters * `component`: `any` ### Returns The added component or configuration ### Examples ```ts theme={null} // Add a shadow component behind all sprites engine.addSpriteComponentBehind(ShadowComponent); // Add a component with static props engine.addSpriteComponentBehind({ component: LightHalo, props: { radius: 30 } }); // Add a component with dynamic props and dependencies engine.addSpriteComponentBehind({ component: HealthBar, props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }), dependencies: (object) => [object.hp, object.param.maxHp] }); ``` ## addSpriteComponentInFront Add a component to render in front of sprites Components added with this method will be displayed with a higher z-index than the sprite Supports multiple formats: 1. Direct component: `HealthBarComponent` 2. Configuration object: `{ component: StatusIndicator, props: {...} }` 3. With dynamic props: `{ component: HealthBar, props: (object) => {...} }` 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }` Components with dependencies will only be displayed when all dependencies are resolved (!= undefined). The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} addSpriteComponentInFront(component: any | { component: any, props: (object: any) => any, dependencies?: (object: any) => any[] }) ``` ### Parameters * `component`: `any | { component: any, props: (object: any) => any, dependencies?: (object: any) => any[] }` ### Returns The added component or configuration ### Examples ```ts theme={null} // Add a health bar component in front of all sprites engine.addSpriteComponentInFront(HealthBarComponent); // Add a component with static props engine.addSpriteComponentInFront({ component: StatusIndicator, props: { type: 'poison' } }); // Add a component with dynamic props and dependencies engine.addSpriteComponentInFront({ component: HealthBar, props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }), dependencies: (object) => [object.hp, object.param.maxHp] }); ``` ## cameraFollowRevision Incremented for each camera follow command so repeated commands on the same target are applied * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `property` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} cameraFollowRevision ``` ## cameraFollowSmoothMove Camera follow transition options used by character components when the target changes * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `property` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} cameraFollowSmoothMove: CameraFollowSmoothMove ``` ## cameraFollowTargetId ID of the sprite that the camera should follow. null means follow the current player * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `property` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} cameraFollowTargetId ``` ## clear Clear all client resources and reset state This method should be called to clean up all client-side resources when shutting down or resetting the client engine. It: * Destroys the PIXI renderer * Stops all sounds * Cleans up subscriptions and event listeners * Resets scene map * Stops ping/pong interval * Clears prediction states ## Design This method is used primarily in testing environments to ensure clean state between tests. In production, the client engine typically persists for the lifetime of the application. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} clear(): void ``` ### Examples ```ts theme={null} // In test cleanup afterEach(() => { clientEngine.clear(); }); ``` ## clearClientPredictionStates Clear client prediction states for cleanup Removes old prediction states and input history to prevent memory leaks. Should be called when changing maps or disconnecting. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} clearClientPredictionStates() ``` ### Examples ```ts theme={null} // Clear prediction states when changing maps engine.clearClientPredictionStates(); ``` ## connect Open the initial MMORPG connection after a deferred account flow. Concurrent calls share the same attempt. Standalone and normal MMORPG starts call this automatically. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Member of: `RpgClientEngine` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} connect ``` ### Returns A promise resolved after the RPGJS server accepts the connection. ## dashDefaults Runtime defaults used by modules that specialize the built-in dash. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `property` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} dashDefaults: Partial ``` ## disconnect Close the current physical connection without destroying the rendered client. Account modules can return to a pre-connection GUI and call `connect()` again. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Member of: `RpgClientEngine` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} disconnect ``` ### Returns Nothing. ## flash Trigger a flash animation on a sprite This method allows you to trigger a flash effect on any sprite from client-side code. The flash can be configured with various options including type (alpha, tint, or both), duration, cycles, and color. ## Design The flash is applied directly to the sprite object using its flash trigger. This is useful for client-side visual feedback, UI interactions, or local effects that don't need to be synchronized with the server. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} flash(spriteId?: string, options?: { type?: 'alpha' | 'tint' | 'both'; duration?: number; cycles?: number; alpha?: number; tint?: number | string; }): void ``` ### Parameters * `spriteId?`: `string` * `options?`: `{ type?: 'alpha' | 'tint' | 'both'; duration?: number; cycles?: number; alpha?: number; tint?: number | string; }` ### Examples ```ts theme={null} // Flash the current player with default settings engine.flash(); // Flash a specific sprite with red tint engine.flash('sprite-id', { type: 'tint', tint: 0xff0000 }); // Flash with both alpha and tint for dramatic effect engine.flash(undefined, { type: 'both', alpha: 0.5, tint: 0xff0000, duration: 200, cycles: 2 }); // Quick damage flash on current player engine.flash(undefined, { type: 'tint', tint: 'red', duration: 150, cycles: 1 }); ``` ## getAvailableLocales Game catalogue locales only, in both standalone and MMORPG mode. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Member of: `RpgClientEngine` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} getAvailableLocales ``` ### Returns Registered game locales; defaultLocale if there are no catalogues. ### Examples ```ts theme={null} client.getAvailableLocales() // ['en', 'fr'] ``` ## getComponentAnimation Get a component animation by its ID Retrieves the EffectManager instance for a specific component animation, which can be used to display the animation on sprites or objects. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} getComponentAnimation(id: string): AnimationManager ``` ### Parameters * `id`: `string` ### Returns The EffectManager instance for the animation ### Examples ```ts theme={null} // Get the hit animation and display it const hitAnimation = engine.getComponentAnimation('hit'); hitAnimation.displayEffect({ text: "Critical!" }, player); ``` ## getCurrentRoom Return the active map scene or synchronized custom gameplay room. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} getCurrentRoom(): RpgClientMap | RpgClientRoom ``` ## getLocale Read the resolved client locale, reactively inside CanvasEngine computations. Works in standalone RPG and MMORPG modes. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Member of: `RpgClientEngine` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} getLocale ``` ### Returns The resolved game locale, never `auto`. ### Examples ```ts theme={null} client.getLocale() // 'fr' ``` ## getLocalePreference Current client preference in standalone RPG and MMORPG modes. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Member of: `RpgClientEngine` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} getLocalePreference ``` ### Returns Locale preference, reactive in client components. ### Examples ```ts theme={null} client.getLocalePreference() // 'auto' ``` ## getSound Get a sound by ID, using resolver if not found in cache This method first checks if the sound exists in the cache. If not found and a resolver is set, it calls the resolver to create the sound. The resolved sound is automatically cached for future use. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} getSound(id: string): any | Promise ``` ### Parameters * `id`: `string` ### Returns The sound if found or created, or undefined if not found and no resolver ### Examples ```ts theme={null} // Synchronous usage const sound = engine.getSound('my-sound'); // Asynchronous usage (when resolver returns Promise) const sound = await engine.getSound('dynamic-sound'); ``` ## getSoundVolume Read the persisted volume of one sound channel for the current project. Calls made inside a CanvasEngine computed value remain reactive. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Member of: `RpgClientEngine` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} getSoundVolume(channel: RpgAudioChannel): number ``` ### Parameters * `channel`: `RpgAudioChannel` ### Returns Volume between 0 and 1. ### Examples ```ts theme={null} const musicVolume = engine.getSoundVolume('music') ``` ## getSpriteComponent Get a reusable sprite component by id. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} getSpriteComponent(id: string) ``` ### Parameters * `id`: `string` ### Returns The CanvasEngine component, or undefined when missing ## getSpriteSheet Get a spritesheet by ID, using resolver if not found in cache This method first checks if the spritesheet exists in the cache. If not found and a resolver is set, it calls the resolver to create the spritesheet. The resolved spritesheet is automatically cached for future use. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} getSpriteSheet(id: string | number): any | Promise ``` ### Parameters * `id`: `string | number` ### Returns The spritesheet if found or created, or undefined if not found and no resolver ### Examples ```ts theme={null} // Synchronous usage const spritesheet = engine.getSpriteSheet('my-sprite'); // Asynchronous usage (when resolver returns Promise) const spritesheet = await engine.getSpriteSheet('dynamic-sprite'); ``` ## interactions Register client-only pointer behaviors for map sprites. Interactions remain local unless a behavior explicitly sends an action to the server. See the [client interactions guide](../../guide/interactions.md) for hover, selection, hit testing, drag-and-drop, overlays, and network rules. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `property` * Member of: `RpgClientEngine` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} interactions: RpgClientInteractions ``` ### Examples ```ts theme={null} engine.interactions.use('Guard', { cursor: 'pointer', click(ctx) { ctx.action('guard:talk', { eventId: ctx.target.id }) } }) ``` ## interruptCurrentPlayerMovement Stop local movement immediately and discard pending predicted movement. Use this before a blocking action such as an A-RPG attack, dialog, dash startup, or any client-side state where already buffered movement inputs must not be replayed after server reconciliation. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} interruptCurrentPlayerMovement(player?: any): boolean ``` ### Parameters * `player?`: `any` ### Returns `true` when a player was found and interrupted. ### Examples ```ts theme={null} engine.interruptCurrentPlayerMovement(); ``` ## mapShakeTrigger Trigger for map shake animation * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `property` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} mapShakeTrigger: ConfigurableTrigger ``` ## music Client-only controller for temporary looping music and map BGM crossfades. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `property` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} music ``` ## playClientVisual Play a registered client visual locally. This is also used by the websocket listener when the server calls `player.clientVisual()` or `map.clientVisual()`. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} playClientVisual(packet: ClientVisualPacket) ``` ### Parameters * `packet`: `ClientVisualPacket` ## playSound Play a sound by its ID This method retrieves a sound from the cache or resolver and plays it. If the sound is not found, it will attempt to resolve it using the soundResolver. Uses Howler.js for audio playback instead of native Audio elements. The existing API remains the single entry point for ordinary, channel-aware, and spatial sounds. Playback is client-owned in both standalone and MMORPG games; server calls only ask the receiving client to play a registered ID. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Member of: `RpgClientEngine` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} playSound(soundId: string, options?: RpgPlaySoundOptions): Promise ``` ### Parameters * `soundId`: `string` * `options?`: `RpgPlaySoundOptions` ### Returns Resolves after the sound has been resolved and started when available. ### Examples ```ts theme={null} // Play a sound synchronously engine.playSound('item-pickup'); // Play a sound with volume and loop engine.playSound('background-music', { volume: 0.5, loop: true }); // Play a sound asynchronously (when resolver returns Promise) await engine.playSound('dynamic-sound', { volume: 0.8 }); // Play a spatial sound without exposing CanvasEngine signals await engine.playSound('enemy-hit', { channel: 'sfx', position: { x: enemy.x(), y: enemy.y() }, listener: { x: player.x(), y: player.y() }, }); ``` ## pointer Read the latest pointer position tracked by the client canvas. World coordinates are suitable for action payloads and map interactions. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `property` * Member of: `RpgClientEngine` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} pointer: ClientPointerContext ``` ### Examples ```ts theme={null} const target = engine.pointer.world() if (target) engine.processAction('projectile:shoot', { target }) ``` ## processAction Send an action intent to the authoritative server. Client-provided data must be validated by the receiving player input handler or action. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Member of: `RpgClientEngine` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} processAction(action: RpgActionName | RpgActionInput, data?: any): void ``` ### Parameters * `action`: `RpgActionName` * `data?`: `any` ### Returns Nothing. ### Examples ```ts theme={null} engine.processAction('projectile:shoot', { target: engine.pointer.world(), source: 'map-click', }) ``` ## processDash Start a predicted dash for the current player and send it through the authoritative movement channel. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Member of: `RpgClientEngine` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} processDash(input?: Partial): Promise ``` ### Parameters * `input?`: `Partial` ### Returns A promise resolved after the dash input has been processed locally. ### Examples ```ts theme={null} await engine.processDash({ direction: { x: 1, y: 0 }, additionalSpeed: 10, duration: 220, cooldown: 600, }) ``` ## registerClientVisual Register a named client visual macro. Client visuals are small client-side functions that group existing visual primitives such as flash, sound, component animations, sprite animation, or map shake. The server sends only the visual name and a serializable payload. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} registerClientVisual(name: string, handler: ClientVisualHandler) ``` ### Parameters * `name`: `string` * `handler`: `ClientVisualHandler` ### Returns The registered handler ## registerClientVisuals Register several named client visual macros. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} registerClientVisuals(visuals: ClientVisualMap) ``` ### Parameters * `visuals`: `ClientVisualMap` ## registerSpriteComponent Register a reusable sprite component that can be addressed by the server. Server-side component definitions only carry the component id and serializable props. The client registry maps that id to the CanvasEngine component that performs the actual rendering. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} registerSpriteComponent(id: string, component: any) ``` ### Parameters * `id`: `string` * `component`: `any` ### Returns The registered component ### Examples ```ts theme={null} engine.registerSpriteComponent('guildBadge', GuildBadgeComponent); ``` ## resolveEventComponent Resolve the custom CanvasEngine component for an event, if any. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} resolveEventComponent(event: RpgClientEvent): EventComponentConfig | null ``` ### Parameters * `event`: `RpgClientEvent` ### Returns The component/config returned by the last matching resolver ## sceneRoom Synchronized state for the active non-map gameplay room. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `property` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} sceneRoom ``` ## setCameraFollow Set the camera to follow a specific sprite This method changes which sprite the camera viewport should follow. The camera can smoothly animate to the target sprite before continuous follow starts. ## Design The camera follow target is stored in a signal that is read by sprite components. Each sprite checks if it should be followed by comparing its ID with the target ID. When smoothMove options are provided, the transition is handled by pixi-viewport's animation plugin, then continuous follow is handled by CanvasEngine's viewport system. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} setCameraFollow(targetId: string | null, smoothMove?: CameraFollowSmoothMove): void ``` ### Parameters * `targetId`: `string | null` * `smoothMove?`: `CameraFollowSmoothMove` ### Examples ```ts theme={null} // Follow another player with default smooth animation engine.setCameraFollow(otherPlayerId, true); // Follow an event with custom smooth animation engine.setCameraFollow(eventId, { time: 1000, ease: "easeInOutQuad" }); // Follow without animation (instant) engine.setCameraFollow(targetId, false); // Return to following current player engine.setCameraFollow(null); ``` ## setKeyboardControls Registers the current player's live CanvasEngine controls on the client. Used automatically when the player component mounts in standalone RPG and MMORPG modes. Destroyed directives are ignored so a retiring component cannot overwrite replacement controls during streamed map updates. Input handling remains client-side; this does not change server movement authority. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Member of: `RpgClientEngine` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} setKeyboardControls ``` ### Parameters * `controlInstance`: `ControlsDirective` ### Returns Nothing. ### Examples ```ts theme={null} // Inside the current player's CanvasEngine mount callback: client.setKeyboardControls(element.directives.controls) ``` ## setLocale Select and persist a game language, or `auto` for browser negotiation. Updates client labels and the connected player's future translations. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Member of: `RpgClientEngine` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} setLocale ``` ### Parameters * `locale`: `string` ### Returns Nothing. ### Examples ```ts theme={null} client.setLocale('fr') ``` ## setSoundResolver Set a resolver function for sounds The resolver is called when a sound is requested but not found in the cache. It can be synchronous (returns directly) or asynchronous (returns a Promise). The resolved sound is automatically cached for future use. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} setSoundResolver(resolver: (id: string) => any | Promise): void ``` ### Parameters * `resolver`: `(id: string) => any | Promise` ### Examples ```ts theme={null} // Synchronous resolver engine.setSoundResolver((id) => { if (id === 'dynamic-sound') { return { id: 'dynamic-sound', src: 'path/to/sound.mp3' }; } return undefined; }); // Asynchronous resolver (loading from API) engine.setSoundResolver(async (id) => { const response = await fetch(`/api/sounds/${id}`); const data = await response.json(); return data; }); ``` ## setSoundVolume Set the persisted volume of one sound channel for the current project. Master volume is applied through Howler, including sounds controlled through the legacy `RpgSound.global` facade. This client-owned preference behaves the same in standalone and MMORPG games and never changes server state. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Member of: `RpgClientEngine` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} setSoundVolume(channel: RpgAudioChannel, value: number): void ``` ### Parameters * `channel`: `RpgAudioChannel` * `value`: `number` ### Examples ```ts theme={null} engine.setSoundVolume('music', 0.6) engine.setSoundVolume('master', 0.8) ``` ## setSpritesheetResolver Set a resolver function for spritesheets The resolver is called when a spritesheet is requested but not found in the cache. It can be synchronous (returns directly) or asynchronous (returns a Promise). The resolved spritesheet is automatically cached for future use. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} setSpritesheetResolver(resolver: (id: string | number) => any | Promise): void ``` ### Parameters * `resolver`: `(id: string | number) => any | Promise` ### Examples ```ts theme={null} // Synchronous resolver engine.setSpritesheetResolver((id) => { if (id === 'dynamic-sprite') { return { id: 'dynamic-sprite', image: 'path/to/image.png', framesWidth: 32, framesHeight: 32 }; } return undefined; }); // Asynchronous resolver (loading from API) engine.setSpritesheetResolver(async (id) => { const response = await fetch(`/api/spritesheets/${id}`); const data = await response.json(); return data; }); ``` ## startTransition Start a transition Convenience method to display a transition by its ID using the GUI system. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} startTransition(id: string, props?: any): Promise ``` ### Parameters * `id`: `string` * `props?`: `any` ### Examples ```ts theme={null} // Start a fade transition engine.startTransition('fade', { duration: 1000, color: 'black' }); // Start with onFinish callback engine.startTransition('fade', { duration: 1000, onFinish: () => console.log('Fade complete') }); // Wait until the transition component calls onFinish await engine.startTransition('fade', { duration: 1000 }); ``` ## stopAllSounds Stop all currently playing sounds This method stops all sounds that are currently playing. Useful when changing maps to prevent sound overlap. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} stopAllSounds(): void ``` ### Examples ```ts theme={null} // Stop all sounds engine.stopAllSounds(); ``` ## stopSound Stop a sound that is currently playing This method stops a sound that was previously started with `playSound()`. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `method` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} stopSound(soundId: string): void ``` ### Parameters * `soundId`: `string` ### Examples ```ts theme={null} // Start a looping sound engine.playSound('background-music', { loop: true }); // Later, stop it engine.stopSound('background-music'); ``` ## visualPause Freezes map rendering for short presentation-only beats such as combat hit-stop. It is deliberately separate from menu/gameplay pause ownership. * Source: `packages/client/src/RpgClientEngine.ts` * Kind: `property` * Defined in: `RpgClientEngine` ### Signature ```ts theme={null} visualPause ``` # Event hooks Source: https://v5.rpgjs.dev/api/map/hooks Hooks for events created as plain objects, as shown in the create-event guide. # Event hooks Hooks for events created as plain objects, as shown in the create-event guide. ## Members * [onAction](#onaction) * [onChanges](#onchanges) * [onDetectInShape](#ondetectinshape) * [onDetectOutShape](#ondetectoutshape) * [onInit](#oninit) * [onInShape](#oninshape) * [onOutShape](#onoutshape) * [onPlayerTouch](#onplayertouch) * [onTouch](#ontouch) * [onTouchEnd](#ontouchend) ## onAction Called when a player performs an action on this event * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `EventHooks` ### Signature ```ts theme={null} onAction: (this: RpgEvent, player: RpgPlayer, input: RpgActionInput) => void | Promise ``` ## onChanges Called during the change-detection cycle for the current player. Use this hook to recompute the event state from player data, especially player variables. This is useful for reactive visuals such as an opened chest, a hidden door, or a conditional NPC graphic. * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `EventHooks` ### Signature ```ts theme={null} onChanges: (this: RpgEvent, player: RpgPlayer) => void ``` ## onDetectInShape Called when a player is detected entering a detection shape attached to the event * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `EventHooks` ### Signature ```ts theme={null} onDetectInShape: (this: RpgEvent, player: RpgPlayer, shape: RpgShape) => void ``` ## onDetectOutShape Called when a player is detected exiting a detection shape attached to the event * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `EventHooks` ### Signature ```ts theme={null} onDetectOutShape: (this: RpgEvent, player: RpgPlayer, shape: RpgShape) => void ``` ## onInit Called when the event is first initialized. Use this hook for default setup that does not depend on a player interaction, such as setting the initial graphic, speed, or movement route. * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `EventHooks` ### Signature ```ts theme={null} onInit: (this: RpgEvent) => void ``` ## onInShape Called when a player enters a shape attached to the event * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `EventHooks` ### Signature ```ts theme={null} onInShape: (this: RpgEvent, zone: RpgShape, player: RpgPlayer) => void ``` ## onOutShape Called when a player exits a shape attached to the event * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `EventHooks` ### Signature ```ts theme={null} onOutShape: (this: RpgEvent, zone: RpgShape, player: RpgPlayer) => void ``` ## onPlayerTouch Called when a player touches this event * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `EventHooks` ### Signature ```ts theme={null} onPlayerTouch: (this: RpgEvent, player: RpgPlayer) => void ``` ## onTouch Called when this event starts touching a player or another event * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `EventHooks` ### Signature ```ts theme={null} onTouch: (this: RpgEvent, other: RpgPlayer | RpgEvent, context: RpgTouchContext) => void | Promise ``` ## onTouchEnd Called when this event stops touching a player or another event * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `EventHooks` ### Signature ```ts theme={null} onTouchEnd: (this: RpgEvent, other: RpgPlayer | RpgEvent, context: RpgTouchContext) => void | Promise ``` # Map API Source: https://v5.rpgjs.dev/api/map/index Reference for RpgMap, world map helpers, controls, and event hooks used by created events. # Map API This reference is generated from the JSDoc comments in `packages/server/src/rooms/map.ts`, `packages/common/src/rooms/Map.ts`, `packages/common/src/rooms/area`, and `packages/common/src/rooms/WorldMaps.ts`. ## Pages * [RpgMap](/api/map/rpg-map) * [World Maps](/api/map/world-maps) * [Map Types](/api/map/map-types) * [Event hooks](/api/map/hooks) # Map Types Source: https://v5.rpgjs.dev/api/map/map-types Supporting interfaces related to map controls. # Map Types Supporting interfaces related to map controls. ## Members * [enableAntiCheat](#enableanticheat) * [maxFrameDelta](#maxframedelta) * [maxInputsPerTick](#maxinputspertick) * [maxTimeDelta](#maxtimedelta) * [minTimeBetweenInputs](#mintimebetweeninputs) ## enableAntiCheat Whether to enable anti-cheat validation * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `Controls` ### Signature ```ts theme={null} enableAntiCheat: boolean ``` ## maxFrameDelta Maximum allowed frame delta between inputs * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `Controls` ### Signature ```ts theme={null} maxFrameDelta: number ``` ## maxInputsPerTick Maximum number of queued inputs processed per server tick * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `Controls` ### Signature ```ts theme={null} maxInputsPerTick: number ``` ## maxTimeDelta Maximum allowed time delta between inputs in milliseconds * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `Controls` ### Signature ```ts theme={null} maxTimeDelta: number ``` ## minTimeBetweenInputs Minimum time between inputs in milliseconds * Source: `packages/server/src/rooms/map-types.ts` * Kind: `property` * Defined in: `Controls` ### Signature ```ts theme={null} minTimeBetweenInputs: number ``` # RpgMap Source: https://v5.rpgjs.dev/api/map/rpg-map Reference for the `RpgMap` class. # RpgMap Reference for the `RpgMap` class. ## Members * [addInDatabase](#addindatabase) * [applySyncToClient](#applysynctoclient) * [broadcast](#broadcast) * [clear](#clear) * [Clear streamed static hitboxes](#clear-streamed-static-hitboxes) * [clearLighting](#clearlighting) * [clearPhysic](#clearphysic) * [clearWeather](#clearweather) * [clientVisual](#clientvisual) * [createDynamicEvent](#createdynamicevent) * [createDynamicWorldMaps](#createdynamicworldmaps) * [createMovingHitbox](#createmovinghitbox) * [createShape](#createshape) * [damageFormulas](#damageformulas) * [database](#database) * [dataIsReady\$](#dataisready) * [deleteWorldMaps](#deleteworldmaps) * [events](#events) * [Find spawn position](#find-spawn-position) * [getBody](#getbody) * [getBodyPosition](#getbodyposition) * [getEvent](#getevent) * [getEventBy](#geteventby) * [getEvents](#getevents) * [getEventsBy](#geteventsby) * [getInWorldMaps](#getinworldmaps) * [getLighting](#getlighting) * [getPlayer](#getplayer) * [getPlayers](#getplayers) * [getShape](#getshape) * [getShapes](#getshapes) * [getTick](#gettick) * [getWeather](#getweather) * [getWorldMaps](#getworldmaps) * [getWorldMapsManager](#getworldmapsmanager) * [globalConfig](#globalconfig) * [guiExit](#guiexit) * [guiInteraction](#guiinteraction) * [heightPx](#heightpx) * [hooks](#hooks) * [id](#id) * [interceptorPacket](#interceptorpacket) * [isMoving](#ismoving) * [maps](#maps) * [nextTick](#nexttick) * [off](#off) * [on](#on) * [onAction](#onaction) * [onInput](#oninput) * [onJoin](#onjoin) * [onLeave](#onleave) * [onRestore](#onrestore) * [patchLighting](#patchlighting) * [patchWeather](#patchweather) * [players](#players) * [playSound](#playsound) * [processInput](#processinput) * [queryArea](#queryarea) * [queryHitbox](#queryhitbox) * [refreshCharacterHitboxes](#refreshcharacterhitboxes) * [removeEvent](#removeevent) * [removeFromWorldMaps](#removefromworldmaps) * [removeInDatabase](#removeindatabase) * [removeShape](#removeshape) * [Replace streamed static hitboxes](#replace-streamed-static-hitboxes) * [setAutoTick](#setautotick) * [setBodyPosition](#setbodyposition) * [setDay](#setday) * [setInWorldMaps](#setinworldmaps) * [setLighting](#setlighting) * [setNight](#setnight) * [setSync](#setsync) * [setWeather](#setweather) * [shakeMap](#shakemap) * [showAnimation](#showanimation) * [showComponentAnimation](#showcomponentanimation) * [sounds](#sounds) * [stopSound](#stopsound) * [tick\$](#tick) * [transitionLighting](#transitionlighting) * [updateHitbox](#updatehitbox) * [updateMap](#updatemap) * [updateWorld](#updateworld) * [updateWorldMaps](#updateworldmaps) * [widthPx](#widthpx) * [worldX](#worldx) * [worldY](#worldy) ## addInDatabase Add data to the map's database This method delegates to BaseRoom's implementation to avoid code duplication. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} addInDatabase(id: string, data: unknown, options?: { force?: boolean }): boolean ``` ### Parameters * `id`: `string` * `data`: `unknown` * `options?`: `{ force?: boolean }` ### Returns true if data was added, false if ignored (ID already exists) ### Examples ```ts theme={null} // Add an item class to the database map.addInDatabase('Potion', PotionClass); // Add an item object to the database map.addInDatabase('custom-item', { name: 'Custom Item', price: 100 }); // Force overwrite existing data map.addInDatabase('Potion', UpdatedPotionClass, { force: true }); ``` ## applySyncToClient Apply sync to the client This method applies sync to the client by calling the `$applySync()` method. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} applySyncToClient() ``` ### Examples ```ts theme={null} map.applySyncToClient(); ``` ## broadcast Broadcast a custom websocket event to all clients connected to this map. This is a convenience wrapper around `$broadcast({ type, value })`. On the client side, receive the event by injecting `WebSocketToken` and subscribing with `socket.on(type, cb)`. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} map.broadcast(type, value) ``` ### Parameters * `type`: `string` * `value?`: `T` ### Examples ```ts theme={null} map.broadcast("weather:warning", { level: "storm", }); ``` ```ts theme={null} import { inject } from "@rpgjs/client"; import { WebSocketToken, type AbstractWebsocket } from "@rpgjs/client"; const socket = inject(WebSocketToken); socket.on("weather:warning", (payload) => { console.log(payload.level); }); ``` ## clear Clear all server resources and reset state This method should be called to clean up all server-side resources when shutting down or resetting the map. It stops the input processing loop and ensures that all subscriptions are properly cleaned up. ## Design This method is used primarily in testing environments to ensure clean state between tests. It stops the tick subscription to prevent memory leaks. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} clear(): void ``` ### Examples ```ts theme={null} // In test cleanup afterEach(() => { map.clear(); }); ``` ## Clear streamed static hitboxes Remove prediction collision geometry previously registered for a map chunk. * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Member of: `RpgCommonMap` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} map.clearStreamedStaticHitboxes ``` ### Parameters * `namespace`: `string` ## clearLighting Clear lighting for this map. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} clearLighting(options?: LightingSetOptions): void ``` ### Parameters * `options?`: `LightingSetOptions` ## clearPhysic Clear all physics content and reset to initial state This method completely clears the physics system by: * Removing all hitboxes (static and movable) * Removing all zones * Clearing all collision data and events * Clearing all movement events and sliding data * Unsubscribing from the tick subscription * Resetting the physics engine to a clean state Use this method when you need to completely reset the map's physics system, such as when changing maps or restarting a level. * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} clearPhysic() ``` ### Examples ```ts theme={null} // Clear all physics when changing maps map.clearPhysic(); // Then reload physics for the new map map.loadPhysic(); ``` ## clearWeather Clear weather for this map. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} clearWeather(options?: WeatherSetOptions): void ``` ### Parameters * `options?`: `WeatherSetOptions` ## clientVisual Trigger a named client visual for all players on the map. Client visuals are registered in the client module with `clientVisuals`. They are client-side macros for grouping existing visual primitives such as flash, sound, component animations, sprite animations, or camera shake. The map broadcasts one compact packet containing the visual name and a serializable payload; each client resolves and renders the visual locally. Prefer direct APIs such as `playSound()`, `showComponentAnimation()`, or `flash()` for a single visual operation. Use `clientVisual()` when one gameplay moment should trigger several client-side visuals together. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} clientVisual(name: string, data?: TData): void ``` ### Parameters * `name`: `string` * `data?`: `TData` ### Examples ```ts theme={null} map.clientVisual("explosion", { position: { x: 320, y: 180 }, power: 2, }); ``` ## createDynamicEvent Creates a dynamic event on the map This method handles both class-based events and object-based events with hooks. For class-based events, it creates a new instance of the class. For object-based events, it creates a dynamic class that extends RpgEvent and implements the hook methods from the object. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} createDynamicEvent(eventObj: EventPosOption, options?: CreateDynamicEventOptions): Promise ``` ### Parameters * `eventObj`: `EventPosOption` * `options?`: `CreateDynamicEventOptions` ### Examples ```ts theme={null} // Using a class-based event class MyEvent extends RpgEvent { onInit() { console.log('Event initialized'); } } map.createDynamicEvent({ x: 100, y: 200, event: MyEvent }); // Using an object-based event map.createDynamicEvent({ x: 100, y: 200, event: { onInit() { console.log('Event initialized'); }, onPlayerTouch(player) { console.log('Player touched event'); } } }); ``` ## createDynamicWorldMaps Create a world manager dynamically Creates a new WorldMapsManager instance and configures it with the provided map configurations. This is used when loading world data from Tiled or other map editors. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} createDynamicWorldMaps(world: { id?: string; maps: WorldMapConfig[] }): WorldMapsManager ``` ### Parameters * `world`: `{ id?: string; maps: WorldMapConfig[] }` ### Returns The newly created WorldMapsManager instance ### Examples ```ts theme={null} const manager = map.createDynamicWorldMaps({ id: 'my-world', maps: [ { id: 'map1', worldX: 0, worldY: 0, width: 800, height: 600 }, { id: 'map2', worldX: 800, worldY: 0, width: 800, height: 600 } ] }); ``` ## createMovingHitbox Create a temporary and moving hitbox on the map Allows to create a temporary hitbox that moves through multiple positions sequentially. For example, you can use it to explode a bomb and find all the affected players, or for a gameplay effect that should emit when entities enter a moving area. For instant melee/combat checks, prefer `queryHitbox()`. The method creates a zone sensor that moves through the specified hitbox positions at the given speed, detecting collisions with players and events at each step. * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} createMovingHitbox(hitboxes: Array<{ x: number; y: number; width: number; height: number }>, options?: { speed?: number }): Observable<(T | any)[]> ``` ### Parameters * `hitboxes`: `Array<{ x: number; y: number; width: number; height: number }>` * `options?`: `{ speed?: number }` ### Returns Observable that emits arrays of hit entities and completes when movement is finished ### Examples ```ts theme={null} // Create a sword slash effect that moves through two positions map.createMovingHitbox([ { x: 100, y: 100, width: 50, height: 50 }, { x: 120, y: 100, width: 50, height: 50 } ], { speed: 2 }).subscribe({ next(hits) { // hits contains RpgPlayer or RpgEvent objects that were hit console.log('Hit entities:', hits); }, complete() { console.log('Movement finished'); } }); ``` ## createShape Create a shape dynamically on the map This method creates a static hitbox on the map that can be used for collision detection, area triggers, or visual boundaries. The shape is backed by the physics engine's static entity system for accurate collision detection. ## Architecture Creates a static entity (hitbox) in the physics engine at the specified position and size. The shape is stored internally and can be retrieved by name. When players or events collide with this hitbox, the `onInShape` and `onOutShape` hooks are automatically triggered on both the player and the event. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} createShape(obj: { x: number; y: number; width: number; height: number; name?: string; z?: number; color?: string; collision?: boolean; properties?: Record; }): RpgShape ``` ### Parameters * `obj`: `{ x: number; y: number; width: number; height: number; name?: string; z?: number; color?: string; collision?: boolean; properties?: Record; }` ### Returns The created RpgShape instance ### Examples ```ts theme={null} // Create a simple rectangular shape const shape = map.createShape({ x: 100, y: 200, width: 50, height: 50, name: "spawn-zone" }); // Create a shape with visual properties const triggerZone = map.createShape({ x: 300, y: 400, width: 100, height: 100, name: "treasure-area", color: "#FFD700", z: 1, collision: false, properties: { type: "treasure", value: 100 } }); // Player hooks will be triggered automatically const player: RpgPlayerHooks = { onInShape(player: RpgPlayer, shape: RpgShape) { console.log('in', player.name, shape.name); }, onOutShape(player: RpgPlayer, shape: RpgShape) { console.log('out', player.name, shape.name); } }; ``` ## damageFormulas Damage formulas configuration for the map Contains formulas for calculating damage from skills, physical attacks, critical hits, and element coefficients. Default formulas are merged with custom formulas when the map is loaded. * Source: `packages/server/src/rooms/map.ts` * Kind: `property` * Defined in: `RpgMap` ### Signature ```ts theme={null} damageFormulas: DamageFormulas ``` ## database Signal containing the map's database of items, classes, and other game data This database can be dynamically populated using `addInDatabase()` and `removeInDatabase()` methods. It's used to store game entities like items, classes, skills, etc. that are specific to this map. * Source: `packages/server/src/rooms/map.ts` * Kind: `property` * Defined in: `RpgMap` ### Signature ```ts theme={null} database ``` ### Examples ```ts theme={null} // Add data to database map.addInDatabase('Potion', PotionClass); // Access database const potion = map.database()['Potion']; ``` ## dataIsReady\$ BehaviorSubject that completes when the map data is ready This subject is used to signal when the map has finished loading all its data. Players wait for this to complete before the map is fully initialized. * Source: `packages/server/src/rooms/map.ts` * Kind: `property` * Defined in: `RpgMap` ### Signature ```ts theme={null} dataIsReady$ ``` ### Examples ```ts theme={null} // Wait for map data to be ready map.dataIsReady$.subscribe(() => { console.log('Map is ready!'); }); ``` ## deleteWorldMaps Delete a world manager by id Removes the world maps manager from this map instance. Currently, only one world manager is supported, so this clears the single manager. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} deleteWorldMaps(id: string): boolean ``` ### Parameters * `id`: `string` ### Returns true if the manager was deleted, false if it didn't exist ### Examples ```ts theme={null} const deleted = map.deleteWorldMaps('my-world'); if (deleted) { console.log('World manager removed'); } ``` ## events Synchronized signal containing all events (NPCs, objects) on the map This signal is automatically synchronized with clients by RPGJS. Events are indexed by their unique ID. * Source: `packages/server/src/rooms/map.ts` * Kind: `property` * Defined in: `RpgMap` ### Signature ```ts theme={null} events ``` ### Examples ```ts theme={null} // Get all events const allEvents = map.events(); // Get a specific event const event = map.events()['event-id']; ``` ## Find spawn position Find a collision-free position for a character hitbox near a preferred point. The search tests positions on growing rings around `preferred`, in a deterministic order, and returns the closest one where the hitbox stays inside the map and overlaps no blocking body: map hitboxes (Tiled tiles, Studio elements), static shapes, and non-`through` players and events. Hitboxes with a `z` range only block when `options.z` is inside it. It never disables collisions and never returns a blocked position: when no position is found within `maxDistance`, it returns `null`. It works on the server and on the client prediction map; use the server result for authoritative placement, e.g. with `player.changeMap()` or `player.teleport()`. * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Member of: `RpgCommonMap` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} map.findSpawnPosition(options) ``` ### Parameters * `options`: `MapSpawnPositionOptions` ### Returns Top-left position, or `null` when none is free. ### Examples ```ts theme={null} // e.g. the `start` point of a generated map const spawn = map.findSpawnPosition({ preferred: { x: 480, y: 320 }, hitbox: { width: 32, height: 32 }, ignoreIds: [player.id], }); if (spawn) { await player.changeMap(map.id, spawn); } ``` ## getBody Get physics body (entity) for an id * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} getBody(id: string): Entity | undefined ``` ### Parameters * `id`: `string` ## getBodyPosition Get body position in different modes * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} getBodyPosition(id: string, mode?: "center" | "top-left"): { x: number; y: number } | undefined ``` ### Parameters * `id`: `string` * `mode?`: `"center" | "top-left"` ### Returns Position coordinates or undefined if entity not found ## getEvent Get an event by its ID Returns the event with the specified ID, or undefined if not found. The return type can be narrowed using TypeScript generics. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} getEvent(eventId: string): T | undefined ``` ### Parameters * `eventId`: `string` ### Returns The event instance, or undefined if not found ### Examples ```ts theme={null} // Get any event const event = map.getEvent('npc-1'); // Get event with type narrowing const npc = map.getEvent('npc-1'); if (npc) { npc.speak('Hello!'); } ``` ## getEventBy Get the first event that matches a condition Searches through all events on the map and returns the first one that matches the provided callback function. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} getEventBy(cb: (event: RpgEvent) => boolean): RpgEvent | undefined ``` ### Parameters * `cb`: `(event: RpgEvent) => boolean` ### Returns The first matching event, or undefined if none found ### Examples ```ts theme={null} // Find an event by name const npc = map.getEventBy(event => event.name === 'Merchant'); // Find an event at a specific position const chest = map.getEventBy(event => event.x === 100 && event.y === 200 ); ``` ## getEvents Get all events on the map Returns an array of all events (NPCs, objects, etc.) that are currently on this map. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} getEvents(): RpgEvent[] ``` ### Returns Array of all RpgEvent instances on the map ### Examples ```ts theme={null} const events = map.getEvents(); console.log(`There are ${events.length} events on the map`); events.forEach(event => { console.log(`- ${event.name} at (${event.x}, ${event.y})`); }); ``` ## getEventsBy Get all events that match a condition Searches through all events on the map and returns all events that match the provided callback function. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} getEventsBy(cb: (event: RpgEvent) => boolean): RpgEvent[] ``` ### Parameters * `cb`: `(event: RpgEvent) => boolean` ### Returns Array of all matching events ### Examples ```ts theme={null} // Find all NPCs const npcs = map.getEventsBy(event => event.name.startsWith('NPC-')); // Find all events in a specific area const nearbyEvents = map.getEventsBy(event => event.x >= 0 && event.x <= 100 && event.y >= 0 && event.y <= 100 ); ``` ## getInWorldMaps Get attached World Recover the world attached to this map (undefined if no world attached) * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` * Since: `3.0.0-beta.8` ### Signature ```ts theme={null} getInWorldMaps(): RpgWorldMaps | undefined ``` ### Returns The world maps manager instance if attached, otherwise undefined ### Examples ```ts theme={null} const world = map.getInWorldMaps(); if (world) { console.log(world.getAllMaps()); } ``` ## getLighting Get the current map lighting state. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} getLighting(): LightingState | null ``` ## getPlayer Get a player by their ID Returns the player with the specified ID, or undefined if not found. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} getPlayer(playerId: string): RpgPlayer | undefined ``` ### Parameters * `playerId`: `string` ### Returns The player instance, or undefined if not found ### Examples ```ts theme={null} const player = map.getPlayer('player-123'); if (player) { console.log(`Player ${player.name} is on the map`); } ``` ## getPlayers Get all players currently on the map Returns an array of all players that are currently connected to this map. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} getPlayers(): RpgPlayer[] ``` ### Returns Array of all RpgPlayer instances on the map ### Examples ```ts theme={null} const players = map.getPlayers(); console.log(`There are ${players.length} players on the map`); players.forEach(player => { console.log(`- ${player.name}`); }); ``` ## getShape Get a shape by its name Returns a shape with the specified name, or undefined if no shape with that name exists on the map. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} getShape(name: string): RpgShape | undefined ``` ### Parameters * `name`: `string` ### Returns The RpgShape instance, or undefined if not found ### Examples ```ts theme={null} // Create a shape with a specific name map.createShape({ x: 100, y: 200, width: 50, height: 50, name: "spawn-point" }); // Retrieve it later const spawnZone = map.getShape("spawn-point"); if (spawnZone) { console.log(`Spawn zone at (${spawnZone.x}, ${spawnZone.y})`); } ``` ## getShapes Get all shapes on the map Returns an array of all shapes that have been created on this map, regardless of whether they are static shapes or player-attached shapes. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} getShapes(): RpgShape[] ``` ### Returns Array of RpgShape instances ### Examples ```ts theme={null} // Create multiple shapes map.createShape({ x: 0, y: 0, width: 50, height: 50, name: "zone1" }); map.createShape({ x: 100, y: 100, width: 50, height: 50, name: "zone2" }); // Get all shapes const allShapes = map.getShapes(); console.log(allShapes.length); // 2 ``` ## getTick Get the current physics tick * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} getTick(): number ``` ### Returns Current tick number ## getWeather Get the current map weather state. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} getWeather(): WeatherState | null ``` ## getWorldMaps Get a world manager by id Returns the world maps manager for the given world ID. Currently, only one world manager is supported per map instance. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} getWorldMaps(id: string): WorldMapsManager | null ``` ### Parameters * `id`: `string` ### Returns The WorldMapsManager instance, or null if not initialized ### Examples ```ts theme={null} const worldManager = map.getWorldMaps('my-world'); if (worldManager) { const mapInfo = worldManager.getMapInfo('map1'); } ``` ## getWorldMapsManager Get the world maps manager * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} getWorldMapsManager(): WorldMapsManager | null ``` ### Returns WorldMapsManager instance or null if not configured ### Examples ```ts theme={null} const worldMaps = map.getWorldMapsManager(); if (worldMaps) { const adjacentMaps = worldMaps.getAdjacentMaps(currentMap, coordinates); } ``` ## globalConfig Global configuration object for the map This object contains configuration settings that apply to the entire map. It's populated from the map data when `updateMap()` is called. * Source: `packages/server/src/rooms/map.ts` * Kind: `property` * Defined in: `RpgMap` ### Signature ```ts theme={null} globalConfig: any ``` ## guiExit Handle GUI exit from a player This method is called when a player closes or exits a GUI. It removes the GUI from the player's active GUIs. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} guiExit(player: RpgPlayer, { guiId, data, guiOpenId }) ``` ### Parameters * `player`: `RpgPlayer` * `{ guiId, data, guiOpenId }` ### Examples ```ts theme={null} // This method is called automatically when a player closes a GUI // The GUI is removed from the player's active GUIs ``` ## guiInteraction Handle GUI interaction from a player This method is called when a player interacts with a GUI element. It synchronizes the player's changes to ensure the client state is up to date. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} guiInteraction(player: RpgPlayer, value: { guiId: string, name: string, data: any }) ``` ### Parameters * `player`: `RpgPlayer` * `value`: `{ guiId: string, name: string, data: any }` ### Examples ```ts theme={null} // This method is called automatically when a player interacts with a GUI // The interaction data is sent from the client ``` ## heightPx Get the height of the map in pixels * Source: `packages/common/src/rooms/Map.ts` * Kind: `getter` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} heightPx ``` ### Returns The height of the map in pixels, or 0 if not loaded ### Examples ```ts theme={null} const height = map.heightPx; console.log(`Map height: ${height}px`); ``` ## hooks Get the hooks system for this map Returns the dependency-injected Hooks instance that allows you to trigger and listen to various game events. * Source: `packages/server/src/rooms/map.ts` * Kind: `getter` * Defined in: `RpgMap` ### Signature ```ts theme={null} hooks ``` ### Returns The Hooks instance for this map ### Examples ```ts theme={null} // Trigger a custom hook map.hooks.callHooks('custom-event', data).subscribe(); ``` ## id Get the unique identifier of the map * Source: `packages/common/src/rooms/Map.ts` * Kind: `getter` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} id ``` ### Returns The map ID, or empty string if not loaded ### Examples ```ts theme={null} const mapId = map.id; console.log(`Current map: ${mapId}`); ``` ## interceptorPacket Intercepts and modifies packets before they are sent to clients This method is automatically called by the RPGJS room runtime for each packet sent to clients. It adds timestamp and acknowledgment information to sync packets for client-side prediction reconciliation. This helps with network synchronization and reduces perceived latency. ## Architecture Adds metadata to packets: * `timestamp`: Current server time for client-side prediction * `ack`: Acknowledgment info with last processed frame and authoritative position * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} interceptorPacket(player: RpgPlayer, packet: any, conn: RpgRoomConnection) ``` ### Parameters * `player`: `RpgPlayer` * `packet`: `any` * `conn`: `RpgRoomConnection` ### Returns Modified packet with timestamp and ack info, or null if player is invalid ### Examples ```ts theme={null} // This method is called automatically by the framework // You typically don't call it directly ``` ## isMoving Check if an entity is currently moving * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} isMoving(id: string): boolean ``` ### Parameters * `id`: `string` ### Returns Boolean indicating if the entity is in motion ### Examples ```ts theme={null} // Check if player is moving if (map.isMoving('player1')) { // Player is in motion } ``` ## maps Array of map configurations - can contain MapOptions objects or instances of map classes This array stores the configuration for this map and any related maps. It's populated when the map is loaded via `updateMap()`. * Source: `packages/server/src/rooms/map.ts` * Kind: `property` * Defined in: `RpgMap` ### Signature ```ts theme={null} maps: (MapOptions | any)[] ``` ## nextTick Manually trigger a single game tick This method allows you to manually advance the game by one tick (16ms at 60fps). It's primarily useful for testing where you need precise control over when physics updates occur, rather than relying on the automatic tick\$ subscription. ## Use Cases * **Testing**: Control exactly when physics steps occur in unit tests * **Manual control**: Step through game state manually for debugging * **Deterministic testing**: Ensure consistent timing in test scenarios ## Important This method should NOT be used in production code alongside the automatic `tick$` subscription, as it will cause double-stepping. Use either: * Automatic ticks (via `loadPhysic()` which subscribes to `tick$`) * Manual ticks (via `nextTick()` without `loadPhysic()` subscription) * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} nextTick(deltaMs?: number): number ``` ### Parameters * `deltaMs?`: `number` ### Returns Number of physics ticks executed ### Examples ```ts theme={null} // In tests: manually advance game by one tick map.nextTick(); // Advances by 16ms (one frame at 60fps) // With custom delta map.nextTick(32); // Advances by 32ms (two frames at 60fps) // In a test loop for (let i = 0; i < 60; i++) { map.nextTick(); // Simulate 1 second of game time } ``` ## off Remove all listeners for a custom client event on this map. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} map.off(type) ``` ### Parameters * `type`: `string` ## on Listen to custom websocket events sent by clients on this map. The callback receives the player who sent the event and the payload. This is useful for map-wide custom interactions that are not covered by built-in actions such as movement, GUI events, or the action button. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} map.on(type, cb) ``` ### Parameters * `type`: `string` * `cb`: `(player: RpgPlayer, data: T) => void | Promise` ### Examples ```ts theme={null} map.on("chat:message", (player, data) => { console.log(player.id, data.text); }); ``` ## onAction Handle action input from a player This method is called when a player performs an action (like pressing a button). It checks for collisions with events and triggers the appropriate hooks. ## Architecture 1. Gets all entities colliding with the player 2. Triggers `onAction` hook on colliding events 3. Triggers `onInput` hook on the player * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} onAction(player: RpgPlayer, action: RpgActionInput): void ``` ### Parameters * `player`: `RpgPlayer` * `action`: `RpgActionInput` ### Examples ```ts theme={null} // This method is called automatically when a player presses an action button // Events near the player will have their onAction hook triggered ``` ## onInput Handle movement input from a player This method is called when a player sends movement input from the client. It queues the input for processing by the game loop. Inputs are processed with frame numbers to ensure proper ordering and client-side prediction. ## Architecture * Inputs are queued in `player.pendingInputs` * Duplicate frames are skipped to prevent processing the same input twice * Inputs are processed asynchronously by the game loop * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} onInput(player: RpgPlayer, input: any) ``` ### Parameters * `player`: `RpgPlayer` * `input`: `any` ### Examples ```ts theme={null} // This method is called automatically when a player moves // The input is queued and processed by processInput() ``` ## onJoin Called when a player joins the map This method is automatically called by the RPGJS room runtime when a player connects to the map. It initializes the player's connection, sets up the map context, and waits for the map data to be ready before playing sounds and triggering hooks. ## Architecture 1. Sets player's map reference and context 2. Initializes the player 3. Waits for map data to be ready 4. Plays map sounds for the player 5. Triggers `server-player-onJoinMap` hook * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} onJoin(player: RpgPlayer, conn: RpgRoomConnection, ctx?: { request?: { url: string } }) ``` ### Parameters * `player`: `RpgPlayer` * `conn`: `RpgRoomConnection` * `ctx?`: `{ request?: { url: string } }` ### Examples ```ts theme={null} // This method is called automatically by the framework // You can listen to the hook to perform custom logic server.addHook('server-player-onJoinMap', (player, map) => { console.log(`Player ${player.id} joined map ${map.id}`); }); ``` ## onLeave Called when a player leaves the map This method is automatically called by the RPGJS room runtime when a player disconnects from the map. It cleans up the player's pending inputs and triggers the appropriate hooks. ## Architecture 1. Triggers `server-player-onLeaveMap` hook 2. Clears pending inputs to prevent processing after disconnection * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} onLeave(player: RpgPlayer, conn: RpgRoomConnection) ``` ### Parameters * `player`: `RpgPlayer` * `conn`: `RpgRoomConnection` ### Examples ```ts theme={null} // This method is called automatically by the framework // You can listen to the hook to perform custom cleanup server.addHook('server-player-onLeaveMap', (player, map) => { console.log(`Player ${player.id} left map ${map.id}`); }); ``` ## onRestore Rebuild non-serializable map resources after a room restart or hibernation. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} onRestore() ``` ## patchLighting Patch the current lighting state. Nested `ambient`, `sun`, and `shadows` values are merged. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} patchLighting(patch: Partial, options?: LightingSetOptions): LightingState | null ``` ### Parameters * `patch`: `Partial` * `options?`: `LightingSetOptions` ## patchWeather Patch the current weather state. Nested `params` values are merged. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} patchWeather(patch: Partial, options?: WeatherSetOptions): WeatherState | null ``` ### Parameters * `patch`: `Partial` * `options?`: `WeatherSetOptions` ## players Synchronized signal containing all players currently on the map This signal is automatically synchronized with clients by RPGJS. Players are indexed by their unique ID. * Source: `packages/server/src/rooms/map.ts` * Kind: `property` * Defined in: `RpgMap` ### Signature ```ts theme={null} players ``` ### Examples ```ts theme={null} // Get all players const allPlayers = map.players(); // Get a specific player const player = map.players()['player-id']; ``` ## playSound Play a sound for all players on the map This method plays a sound for all players currently on the map by iterating over each player and calling `player.playSound()`. The sound must be defined on the client side (in the client module configuration). This is ideal for environmental sounds, battle music, or map-wide events that all players should hear simultaneously. ## Design Iterates over all players on the map and calls `player.playSound()` for each one. This avoids code duplication and reuses the existing player sound logic. For player-specific sounds, use `player.playSound()` directly. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} playSound(soundId: string, options?: { volume?: number; loop?: boolean }): void ``` ### Parameters * `soundId`: `string` * `options?`: `{ volume?: number; loop?: boolean }` ### Examples ```ts theme={null} // Play a sound for all players on the map map.playSound("explosion"); // Play background music for everyone with volume and loop map.playSound("battle-theme", { volume: 0.7, loop: true }); // Play a door opening sound at low volume map.playSound("door-open", { volume: 0.4 }); ``` ## processInput Process pending inputs for a player with anti-cheat validation This method processes pending inputs for a player while performing anti-cheat validation to prevent time manipulation and frame skipping. It validates the time deltas between inputs and ensures they are within acceptable ranges. To preserve movement itinerary under network bursts, the number of distinct client ticks processed per call is capped. ## Architecture **Important**: This method only updates entity velocities - it does NOT step the physics engine. Physics simulation is handled centrally by the game loop (`tick$` -> `runFixedTicks`). This ensures: * Consistent physics timing (60fps fixed timestep) * No double-stepping when multiple inputs are processed * Deterministic physics regardless of input frequency * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} processInput(playerId: string, controls?: Controls): Promise<{ player: RpgPlayer, inputs: RpgMovementInput[] }> ``` ### Parameters * `playerId`: `string` * `controls?`: `Controls` ### Returns Promise containing the player and processed movement inputs ### Examples ```ts theme={null} // Process inputs with default anti-cheat settings const result = await map.processInput('player1'); console.log('Processed inputs:', result.inputs); // Process inputs with custom anti-cheat configuration const result = await map.processInput('player1', { maxTimeDelta: 100, maxFrameDelta: 5, minTimeBetweenInputs: 16, enableAntiCheat: true }); ``` ## queryArea Query players, events, and optional custom targets inside an arbitrary area. `queryArea()` is a low-level selection primitive. It uses the shape bounds as a broad phase, then delegates the precise inclusion logic to `shape.contains()`. Built-in helpers such as `AreaShape.circle()` and `AreaShape.cross()` are conveniences over the same shape contract. The method does not apply gameplay effects. Use the returned hits for damage, healing, target selection, AI, traps, previews, or any custom gameplay. In a networked game, apply those gameplay effects on the server so the authoritative state remains synchronized. Options: * `center`: point or object with `x`, `y`, and optional `hitbox`; objects are resolved to their hitbox center. * `shape`: area shape used for broad-phase bounds and precise inclusion. * `targets`: `"players"`, `"events"`, `"custom"`, an array of them, or `"all"`. Defaults to players and events, plus custom targets when `customTargets` is provided. * `customTargets`: plain objects to include in the query. They should expose `x` and `y`, and may expose `id`, `width`/`height`, or `hitbox`. * `excludeIds`: target ids to ignore, typically the caster or owner. * `filter`: final predicate called before `shape.contains()`. Built-in shape helpers: * `AreaShape.circle({ radius, offset? })` * `AreaShape.rect({ width, height, offset?, angle? })` * `AreaShape.line({ length, thickness, direction, offset? })` * `AreaShape.cross({ armLength, thickness, offset? })` * `AreaShape.composite([shapeA, shapeB])` * `AreaShape.custom({ bounds, contains, distance?, maxDistance? })` Each hit includes `target`, `id`, `kind`, `x`, `y`, `bounds`, `distance`, `distanceRatio`, and `falloff`. `distanceRatio` is clamped between `0` and `1`. `falloff.linear()` returns strong values near the center and weaker values near the edge, which is useful for explosions and other radial effects. * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} queryArea(options: MapAreaQueryOptions): Array> ``` ### Parameters * `options`: `MapAreaQueryOptions` ### Examples ```ts theme={null} import { AreaShape } from "@rpgjs/server"; const hits = map.queryArea({ center: player, shape: AreaShape.circle({ radius: 120 }), targets: ["players", "events"], excludeIds: [player.id], }); for (const hit of hits) { const damage = Math.round(80 * hit.falloff.linear()); hit.target.hp -= damage; } ``` ```ts theme={null} import { AreaShape } from "@rpgjs/server"; function explodeBomb(map, bomb, ownerId?: string) { const hits = map.queryArea({ center: bomb, shape: AreaShape.circle({ radius: 96 }), targets: ["players", "events"], excludeIds: ownerId ? [ownerId] : [], filter: (target) => target.id !== bomb.id, }); for (const hit of hits) { const damage = Math.round(120 * hit.falloff.linear()); hit.target.hp = Math.max(0, hit.target.hp - damage); } } ``` ```ts theme={null} const hits = map.queryArea({ center: { x: 320, y: 240 }, shape: AreaShape.custom({ bounds: ({ center }) => ({ x: center.x - 160, y: center.y - 160, width: 320, height: 320, }), contains: (target, { center }) => { const distance = Math.hypot(target.x - center.x, target.y - center.y); return distance >= 64 && distance <= 160; }, maxDistance: () => 160, }), targets: "custom", customTargets: projectiles, }); ``` ## queryHitbox Query players and events whose physics bodies overlap a rectangular hitbox. Unlike `createMovingHitbox()`, this is an immediate deterministic query. It is intended for melee attacks, AoE checks, and other server-authoritative gameplay that needs to hit entities already inside the area. * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} queryHitbox(rect: MapHitboxQueryRect, options?: MapHitboxQueryOptions): Array ``` ### Parameters * `rect`: `MapHitboxQueryRect` * `options?`: `MapHitboxQueryOptions` ## refreshCharacterHitboxes * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} refreshCharacterHitboxes(): void ``` ## removeEvent Remove an event from the map Removes the event with the specified ID from the map. The event will be removed from the synchronized events signal, causing it to disappear on all clients. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} removeEvent(eventId: string) ``` ### Parameters * `eventId`: `string` ### Examples ```ts theme={null} // Remove an event map.removeEvent('npc-1'); // Remove event after interaction const chest = map.getEvent('chest-1'); if (chest) { // ... do something with chest ... map.removeEvent('chest-1'); } ``` ## removeFromWorldMaps Remove this map from the world Remove this map from the world * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` * Since: `3.0.0-beta.8` ### Signature ```ts theme={null} removeFromWorldMaps(): boolean | undefined ``` ### Returns True if removed, false if not found, undefined if no world attached ### Examples ```ts theme={null} const removed = map.removeFromWorldMaps(); ``` ## removeInDatabase Remove data from the map's database This method delegates to BaseRoom's implementation to avoid code duplication. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} removeInDatabase(id: string): boolean ``` ### Parameters * `id`: `string` ### Returns true if data was removed, false if ID didn't exist ### Examples ```ts theme={null} // Remove an item from the database map.removeInDatabase('Potion'); // Check if removal was successful const removed = map.removeInDatabase('custom-item'); if (removed) { console.log('Item removed successfully'); } ``` ## removeShape Delete a shape from the map Removes a shape by its name and cleans up the associated static hitbox entity. If the shape doesn't exist, the method does nothing. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} removeShape(name: string): void ``` ### Parameters * `name`: `string` ### Returns void ### Examples ```ts theme={null} // Create and then remove a shape const shape = map.createShape({ x: 100, y: 200, width: 50, height: 50, name: "temp-zone" }); // Later, remove it map.removeShape("temp-zone"); ``` ## Replace streamed static hitboxes Replace the client-prediction collision geometry owned by one streamed map chunk. The authoritative server still owns collision results. This method only keeps the predicting client aligned with chunks that the server has disclosed. * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Member of: `RpgCommonMap` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} map.replaceStreamedStaticHitboxes ``` ### Parameters * `namespace`: `string` * `hitboxes`: `MapChunkHitbox[]` ## setAutoTick Enable or disable automatic server tick processing When disabled, the unified input/physics/projectile loop will not run automatically. This is useful for unit tests where you want manual control over server ticks. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} setAutoTick(enabled: boolean): void ``` ### Parameters * `enabled`: `boolean` ### Examples ```ts theme={null} // Disable auto tick for testing map.setAutoTick(false); // Manually trigger tick processing await map.nextTickAsync(); ``` ## setBodyPosition Set body position * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} setBodyPosition(id: string, x: number, y: number, mode?: "center" | "top-left"): Entity | undefined ``` ### Parameters * `id`: `string` * `x`: `number` * `y`: `number` * `mode?`: `"center" | "top-left"` ### Returns True if position was set successfully ## setDay Apply the default daytime lighting preset. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} setDay(options?: LightingSetOptions): LightingState | null ``` ### Parameters * `options?`: `LightingSetOptions` ## setInWorldMaps Assign the map to a world Assign the map to a world * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` * Since: `3.0.0-beta.8` ### Signature ```ts theme={null} setInWorldMaps(worldMap: RpgWorldMaps): void ``` ### Parameters * `worldMap`: `RpgWorldMaps` ### Examples ```ts theme={null} const world = new WorldMapsManager(); world.configure([{ id: 'm1', worldX: 0, worldY: 0, width: 1024, height: 1024 }]); map.setInWorldMaps(world); ``` ## setLighting Set the full lighting state for this map. When `sync` is true (default), all connected clients receive the new lighting. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} setLighting(next: LightingState | null, options?: LightingSetOptions): LightingState | null ``` ### Parameters * `next`: `LightingState | null` * `options?`: `LightingSetOptions` ## setNight Apply the default nighttime lighting preset. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} setNight(options?: LightingSetOptions): LightingState | null ``` ### Parameters * `options?`: `LightingSetOptions` ## setSync Configure runtime synchronized properties on the map This method allows you to dynamically add synchronized properties to the map that will be automatically synced with clients. The schema follows the same structure as module properties with `$initial`, `$syncWithClient`, and `$permanent` options. ## Architecture * Reads a schema object shaped like module props * Creates typed synchronized signals through the RPGJS gameplay contract * Properties are accessible as `map.propertyName` * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} setSync(schema: RpgMapSyncSchema): void ``` ### Parameters * `schema`: `RpgMapSyncSchema` ### Examples ```ts theme={null} // Add synchronized properties to the map map.setSync({ weather: { $initial: 'sunny', $syncWithClient: true, $permanent: false }, timeOfDay: { $initial: 12, $syncWithClient: true, $permanent: false } }); // Use the properties map.weather.set('rainy'); const currentWeather = map.weather(); ``` ## setWeather Set the full weather state for this map. When `sync` is true (default), all connected clients receive the new weather. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} setWeather(next: WeatherState | null, options?: WeatherSetOptions): WeatherState | null ``` ### Parameters * `next`: `WeatherState | null` * `options?`: `WeatherSetOptions` ## shakeMap Shake the map for all players This method triggers a shake animation on the map for all players currently on the map. The shake effect creates a visual feedback that can be used for earthquakes, explosions, impacts, or any dramatic event that should affect the entire map visually. ## Architecture Broadcasts a shake event to all clients connected to the map. Each client receives the shake configuration and triggers the shake animation on the map container using Canvas Engine's shake directive. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} shakeMap(options?: { intensity?: number; duration?: number; frequency?: number; direction?: 'x' | 'y' | 'both'; }): void ``` ### Parameters * `options?`: `{ intensity?: number; duration?: number; frequency?: number; direction?: 'x' | 'y' | 'both'; }` ### Examples ```ts theme={null} // Basic shake with default settings map.shakeMap(); // Intense earthquake effect map.shakeMap({ intensity: 25, duration: 1000, frequency: 15, direction: 'both' }); // Horizontal shake for side impact map.shakeMap({ intensity: 15, duration: 400, direction: 'x' }); // Vertical shake for ground impact map.shakeMap({ intensity: 20, duration: 600, direction: 'y' }); ``` ## showAnimation Display a spritesheet animation at a specific position on the map This method displays a temporary visual animation using a spritesheet at any location on the map. It's a convenience method that internally uses showComponentAnimation with the built-in 'animation' component. This is useful for spell effects, environmental animations, or any visual feedback that uses predefined spritesheets. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} showAnimation(position: { x: number, y: number }, graphic: string, animationName?: string) ``` ### Parameters * `position`: `{ x: number, y: number }` * `graphic`: `string` * `animationName?`: `string` ### Examples ```ts theme={null} // Show explosion at specific coordinates map.showAnimation({ x: 100, y: 200 }, "explosion"); // Show spell effect at player position const playerPos = { x: player.x, y: player.y }; map.showAnimation(playerPos, "spell-effects", "lightning"); // Show environmental effect map.showAnimation({ x: 300, y: 150 }, "nature-effects", "wind-gust"); // Show portal opening animation map.showAnimation({ x: 500, y: 400 }, "portals", "opening"); ``` ## showComponentAnimation Display a component animation at a specific position on the map This method broadcasts a component animation to all clients connected to the map, allowing temporary visual effects to be displayed at any location on the map. Component animations are custom Canvas Engine components that can display complex effects with custom logic and parameters. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} showComponentAnimation(id: string, position: { x: number, y: number }, params: unknown) ``` ### Parameters * `id`: `string` * `position`: `{ x: number, y: number }` * `params`: `unknown` ### Examples ```ts theme={null} // Show explosion at specific coordinates map.showComponentAnimation("explosion", { x: 300, y: 400 }, { intensity: 2.5, duration: 1500 }); // Show area damage effect map.showComponentAnimation("area-damage", { x: player.x, y: player.y }, { radius: 100, color: "red", damage: 50 }); // Show treasure spawn effect map.showComponentAnimation("treasure-spawn", { x: 150, y: 200 }, { sparkle: true, sound: "treasure-appear" }); ``` ## sounds Array of sound IDs to play when players join the map These sounds are automatically played for each player when they join the map. Sounds must be defined on the client side. * Source: `packages/server/src/rooms/map.ts` * Kind: `property` * Defined in: `RpgMap` ### Signature ```ts theme={null} sounds: string[] ``` ### Examples ```ts theme={null} // Set sounds for the map map.sounds = ['background-music', 'ambient-forest']; ``` ## stopSound Stop a sound for all players on the map This method stops a sound that was previously started with `map.playSound()` for all players on the map by iterating over each player and calling `player.stopSound()`. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} stopSound(soundId: string): void ``` ### Parameters * `soundId`: `string` ### Examples ```ts theme={null} // Start background music for everyone map.playSound("battle-theme", { loop: true }); // Later, stop it for everyone map.stopSound("battle-theme"); ``` ## tick\$ Observable representing the game loop tick This observable emits elapsed time and the current epoch timestamp at roughly 60fps. It is shared using the share() operator, meaning that all subscribers receive events from a single timer rather than creating multiple timers. ## Physics Loop Architecture The physics simulation is centralized in this game loop: 1. **Input Processing** (`processInput`): Only updates entity velocities, does NOT step physics 2. **Game Loop** (`tick$` -> `runFixedTicks`): Executes physics simulation with fixed timestep 3. **Fixed Timestep Pattern**: Accumulator-based approach ensures deterministic physics ``` Input Events ─────────────────────────────────────────────────────────────► │ ▼ (update velocity only) ┌─────────────────────────────────────────────────────────────────────────┐ │ Game Loop (tick$) │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ updateMovements │ → │ stepOneTick │ → │ postTickUpdates │ │ │ │ (apply velocity)│ │ (physics step) │ │ (zones, sync) │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` * Source: `packages/common/src/rooms/Map.ts` * Kind: `property` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} tick$ ``` ### Examples ```ts theme={null} // Subscribe to the game tick for custom updates map.tick$.subscribe(({ delta, timestamp }) => { // Custom game logic runs alongside physics this.updateCustomEntities(delta); }); ``` ## transitionLighting Transition lighting over time by broadcasting intermediate lighting states. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} transitionLighting(toLighting: Partial, options?: LightingTransitionOptions & LightingSetOptions): LightingState | null ``` ### Parameters * `toLighting`: `Partial` * `options?`: `LightingTransitionOptions & LightingSetOptions` ## updateHitbox Update hitbox position and size * Source: `packages/common/src/rooms/Map.ts` * Kind: `method` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} updateHitbox(id: string, x: number, y: number, width?: number, height?: number): boolean ``` ### Parameters * `id`: `string` * `x`: `number` * `y`: `number` * `width?`: `number` * `height?`: `number` ### Returns True if hitbox was updated successfully ## updateMap Update the map configuration and data This endpoint receives map data from the client and initializes the map. It loads the map configuration, damage formulas, events, and physics. ## Architecture 1. Validates the request body using MapUpdateSchema 2. Updates map data, global config, and damage formulas 3. Merges events and sounds from map configuration 4. Triggers hooks for map loading 5. Loads physics engine 6. Creates all events on the map 7. Completes the dataIsReady\$ subject * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} updateMap(request: Request) ``` ### Parameters * `request`: `Request` ### Returns Promise that resolves when the map is fully loaded ### Examples ```ts theme={null} // This endpoint is called automatically when a map is loaded // POST /map/update // Body: { // id: string, // width: number, // height: number, // config?: any, // damageFormulas?: any, // database?: any[] | Record // } ``` ## updateWorld Update (or create) a world configuration and propagate to all maps in that world This endpoint receives world map configuration data (typically from Tiled world import) and creates or updates the world manager. The world ID is extracted from the URL path. ## Architecture 1. Authenticates the administrative update request 2. Extracts the world ID from the `/world/:id/update` path segment 3. Normalizes input to array of WorldMapConfig 4. Persists the topology so it survives Durable Object hibernation 5. Creates or updates the world manager Expected payload examples: * `{ id: string, maps: WorldMapConfig[] }` * `WorldMapConfig[]` * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} updateWorld(request: Request) ``` ### Parameters * `request`: `Request` ### Returns Promise resolving to `{ ok: true }` when complete ### Examples ```ts theme={null} // POST /world/my-world/update // Body: [{ id: 'map1', worldX: 0, worldY: 0, width: 800, height: 600 }] // Or with nested structure // Body: { id: 'my-world', maps: [{ id: 'map1', ... }] } ``` ## updateWorldMaps Update world maps by id. Auto-create when missing. Updates the world maps configuration. If the world manager doesn't exist, it is automatically created. This is useful for dynamically loading world data or updating map positions. * Source: `packages/server/src/rooms/map.ts` * Kind: `method` * Defined in: `RpgMap` ### Signature ```ts theme={null} updateWorldMaps(id: string, maps: WorldMapConfig[]) ``` ### Parameters * `id`: `string` * `maps`: `WorldMapConfig[]` ### Returns Promise that resolves when the update is complete ### Examples ```ts theme={null} await map.updateWorldMaps('my-world', [ { id: 'map1', worldX: 0, worldY: 0, width: 800, height: 600 }, { id: 'map2', worldX: 800, worldY: 0, width: 800, height: 600 } ]); ``` ## widthPx Get the width of the map in pixels * Source: `packages/common/src/rooms/Map.ts` * Kind: `getter` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} widthPx ``` ### Returns The width of the map in pixels, or 0 if not loaded ### Examples ```ts theme={null} const width = map.widthPx; console.log(`Map width: ${width}px`); ``` ## worldX Get the X position of this map in the world coordinate system This is used when maps are part of a larger world map. The world position indicates where this map is located relative to other maps. * Source: `packages/common/src/rooms/Map.ts` * Kind: `getter` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} worldX ``` ### Returns The X position in world coordinates, or 0 if not in a world ### Examples ```ts theme={null} const worldX = map.worldX; console.log(`Map is at world position (${worldX}, ${map.worldY})`); ``` ## worldY Get the Y position of this map in the world coordinate system This is used when maps are part of a larger world map. The world position indicates where this map is located relative to other maps. * Source: `packages/common/src/rooms/Map.ts` * Kind: `getter` * Defined in: `RpgCommonMap` ### Signature ```ts theme={null} worldY ``` ### Returns The Y position in world coordinates, or 0 if not in a world ### Examples ```ts theme={null} const worldY = map.worldY; console.log(`Map is at world position (${map.worldX}, ${worldY})`); ``` # World Maps Source: https://v5.rpgjs.dev/api/map/world-maps Reference for world map helpers. # World Maps Reference for world map helpers. ## Members * [configure](#configure) * [getAdjacentMaps](#getadjacentmaps) * [getAllMaps](#getallmaps) * [getLocalPosition](#getlocalposition) * [getMapByWorldCoordinates](#getmapbyworldcoordinates) * [getMapInfo](#getmapinfo) * [getWorldPosition](#getworldposition) * [removeMap](#removemap) ## configure Configure the world maps * Source: `packages/common/src/rooms/WorldMaps.ts` * Kind: `method` * Defined in: `WorldMapsManager` ### Signature ```ts theme={null} configure(configs: WorldMapConfig[]) ``` ### Parameters * `configs`: `WorldMapConfig[]` ### Examples ```ts theme={null} const worldMaps = new WorldMapsManager(); worldMaps.configure([ { id: "town", worldX: 0, worldY: 0, width: 1024, height: 768 }, { id: "forest", worldX: 1024, worldY: 0, width: 1024, height: 768 } ]); ``` ## getAdjacentMaps Find adjacent maps based on various search strategies Supports three search modes: * PositionBox: collect maps intersecting the given box * Direction: collect maps adjacent in the given direction * Point: collect the map containing the given world point The given `map` can be any object exposing `worldX`, `worldY`, `widthPx`, `heightPx` and optional `tileWidth`, `tileHeight` properties (e.g. your `RpgMap` instance or a `WorldMapInfo`). * Source: `packages/common/src/rooms/WorldMaps.ts` * Kind: `method` * Defined in: `WorldMapsManager` ### Signature ```ts theme={null} getAdjacentMaps(map: WorldMapSource, search: | { minX: number; minY: number; maxX: number; maxY: number } | { x: number; y: number } | Direction | number): WorldMapInfo[] ``` ### Parameters * `map`: `WorldMapSource` * `search`: `| { minX: number; minY: number; maxX: number; maxY: number } | { x: number; y: number } | Direction | number` ### Returns Array of matching adjacent map infos ### Examples ```ts theme={null} // Point world.getAdjacentMaps(currentMap, { x: 1024, y: 0 }); // Direction world.getAdjacentMaps(currentMap, Direction.Up); // Box world.getAdjacentMaps(currentMap, { minX: 0, minY: 0, maxX: 2048, maxY: 1024 }); ``` ## getAllMaps Get all configured maps * Source: `packages/common/src/rooms/WorldMaps.ts` * Kind: `method` * Defined in: `WorldMapsManager` ### Signature ```ts theme={null} getAllMaps(): WorldMapInfo[] ``` ### Returns Array of all world maps ## getLocalPosition Calculate local position from world position * Source: `packages/common/src/rooms/WorldMaps.ts` * Kind: `method` * Defined in: `WorldMapsManager` ### Signature ```ts theme={null} getLocalPosition(worldX: number, worldY: number, targetMap: WorldMapInfo): {x: number, y: number} ``` ### Parameters * `worldX`: `number` * `worldY`: `number` * `targetMap`: `WorldMapInfo` ### Returns Local position in the target map ## getMapByWorldCoordinates Find map by world coordinates * Source: `packages/common/src/rooms/WorldMaps.ts` * Kind: `method` * Defined in: `WorldMapsManager` ### Signature ```ts theme={null} getMapByWorldCoordinates(worldX: number, worldY: number): WorldMapInfo | null ``` ### Parameters * `worldX`: `number` * `worldY`: `number` ### Returns Map found or null ## getMapInfo Get map information by ID * Source: `packages/common/src/rooms/WorldMaps.ts` * Kind: `method` * Defined in: `WorldMapsManager` ### Signature ```ts theme={null} getMapInfo(mapId: string): WorldMapInfo | null ``` ### Parameters * `mapId`: `string` ### Returns Map information or null if not found ### Examples ```ts theme={null} const mapInfo = worldMaps.getMapInfo("forest"); ``` ## getWorldPosition Calculate absolute world position of a player * Source: `packages/common/src/rooms/WorldMaps.ts` * Kind: `method` * Defined in: `WorldMapsManager` ### Signature ```ts theme={null} getWorldPosition(map: WorldMapInfo, localX: number, localY: number): {x: number, y: number} ``` ### Parameters * `map`: `WorldMapInfo` * `localX`: `number` * `localY`: `number` ### Returns Absolute coordinates in the world ## removeMap Remove a map from the world by its id Deletes the map from the internal registry and spatial index. * Source: `packages/common/src/rooms/WorldMaps.ts` * Kind: `method` * Defined in: `WorldMapsManager` ### Signature ```ts theme={null} removeMap(mapId: string): boolean ``` ### Parameters * `mapId`: `string` ### Returns True if a map was removed, false otherwise ### Examples ```ts theme={null} const removed = world.removeMap("forest"); ``` # Battle Commands Source: https://v5.rpgjs.dev/api/player/battle-commands Battle formulas and server-side battle helpers for players. # Battle Commands Battle formulas and server-side battle helpers for players. ## Members * [applyDamage](#applydamage) * [getFormulas](#getformulas) ## applyDamage Apply damage. Player will lose HP. the `attackerPlayer` parameter is the other player, the one who attacks. If you don't set the skill parameter, it will be a physical attack. The attack formula is already defined but you can customize it in the server options. This method handles all aspects of damage calculation including critical hits, elemental vulnerabilities, guard effects, and applies the final damage to HP. * Source: `packages/server/src/Player/BattleManager.ts` * Kind: `method` * Defined in: `IBattleManager` ### Signature ```ts theme={null} applyDamage(attackerPlayer: RpgPlayer, skill?: SkillData): DamageResult ``` ### Parameters * `attackerPlayer`: `RpgPlayer` * `skill?`: `SkillData` ### Returns Object containing damage details and special effects that occurred ### Examples ```ts theme={null} // Physical attack const result = player.applyDamage(attackerPlayer); console.log(`Physical damage: ${result.damage}, Critical: ${result.critical}`); // Magical attack with skill const fireSkill = { id: 'fire', power: 50, element: 'fire' }; const magicResult = player.applyDamage(attackerPlayer, fireSkill); console.log(`Magic damage: ${magicResult.damage}, Vulnerable: ${magicResult.elementVulnerable}`); // Check for guard effects if (result.guard) { console.log('Attack was partially blocked!'); } if (result.superGuard) { console.log('Attack was heavily reduced by super guard!'); } ``` ## getFormulas Get damage formulas from the current map Retrieves the damage calculation formulas defined in the current map's configuration. These formulas are used to calculate different types of damage including physical, magical, critical hits, and guard effects. The formulas provide flexibility in customizing the battle system's damage calculations. * Source: `packages/server/src/Player/BattleManager.ts` * Kind: `method` ### Signature ```ts theme={null} getFormulas(name: string) ``` ### Parameters * `name`: `string` ### Returns The formula function or undefined if not found ### Examples ```ts theme={null} // Get physical damage formula const physicFormula = player.getFormulas('damagePhysic'); if (physicFormula) { const damage = physicFormula(attackerParams, defenderParams); } // Get critical damage formula const criticalFormula = player.getFormulas('damageCritical'); if (criticalFormula) { const criticalDamage = criticalFormula(baseDamage, attackerParams, defenderParams); } ``` # Class Commands Source: https://v5.rpgjs.dev/api/player/class-commands Assign and inspect player classes and class-driven behaviors. # Class Commands Assign and inspect player classes and class-driven behaviors. ## Members * [animations](#animations) * [Change Actor](#change-actor) * [changeActor](#changeactor) * [createClassInstance](#createclassinstance) * [resolveClassSnapshot](#resolveclasssnapshot) * [setActor](#setactor) * [setClass](#setclass) * [WithClassManager](#withclassmanager) ## animations Animation names mapped to graphics, consumed by optional combat modules. * Source: `packages/server/src/Player/ClassManager.ts` * Kind: `property` * Defined in: `ActorData` ### Signature ```ts theme={null} animations: Record ``` ## Change Actor Replace the player's active actor identity while preserving acquired progression. The new actor's parameter curves are evaluated at the current level and HP/SP keep their previous fill ratios. Starting equipment is intentionally not granted. Runs on the authoritative server in standalone RPG and MMORPG modes. Animation bindings are available to optional combat modules through `combatAnimations`. * Source: `packages/server/src/Player/ClassManager.ts` * Kind: `method` * Member of: `RpgPlayer` ### Signature ```ts theme={null} player.changeActor(actor) ``` ### Parameters * `actorInput`: `ActorInput` ### Returns The resolved actor object. ### Examples ```ts theme={null} const actor = await player.showCharacterSelect([Hero, Mage], { allowCancel: true }); if (actor) player.changeActor(actor); ``` ## changeActor Change the active actor without granting starting equipment or resetting the player's acquired progression. * Source: `packages/server/src/Player/ClassManager.ts` * Kind: `method` * Defined in: `IClassManager` ### Signature ```ts theme={null} changeActor(actor: ActorInput): ActorData ``` ### Parameters * `actor`: `ActorInput` ### Returns The resolved actor object ## createClassInstance Create a class instance without side effects. * Source: `packages/server/src/Player/ClassManager.ts` * Kind: `method` ### Signature ```ts theme={null} createClassInstance(classInput: ClassInput) ``` ### Parameters * `classInput`: `ClassInput` ## resolveClassSnapshot Resolve class snapshot entry into a class instance without side effects. * Source: `packages/server/src/Player/ClassManager.ts` * Kind: `method` ### Signature ```ts theme={null} resolveClassSnapshot(snapshot: { _class?: any }, mapOverride?: any) ``` ### Parameters * `snapshot`: `{ _class?: any }` * `mapOverride?`: `any` ## setActor Set up the player as a specific actor archetype * Source: `packages/server/src/Player/ClassManager.ts` * Kind: `method` * Defined in: `IClassManager` ### Signature ```ts theme={null} setActor(actor: ActorInput): ActorData ``` ### Parameters * `actor`: `ActorInput` ### Returns The resolved actor object ## setClass Assign a class to the player * Source: `packages/server/src/Player/ClassManager.ts` * Kind: `method` * Defined in: `IClassManager` ### Signature ```ts theme={null} setClass(_class: ClassInput): ClassData ``` ### Parameters * `_class`: `ClassInput` ### Returns The instantiated class object ## WithClassManager Class Manager Mixin Provides class and actor management capabilities to any class. This mixin handles character class assignment and actor setup, including automatic parameter configuration, starting equipment, and skill progression based on class definitions. * Source: `packages/server/src/Player/ClassManager.ts` * Kind: `function` ### Signature ```ts theme={null} WithClassManager(Base: TBase) ``` ### Parameters * `Base`: `TBase` ### Returns Extended class with class management methods ### Examples ```ts theme={null} class MyPlayer extends WithClassManager(BasePlayer) { constructor() { super(); // Class system is automatically initialized } } const player = new MyPlayer(); player.setClass(Fighter); player.setActor(Hero); ``` # Common Commands Source: https://v5.rpgjs.dev/api/player/common-commands Core server-side player commands defined on the main Player class. # Common Commands Core server-side player commands defined on the main Player class. ## Members * [Apply Player Snapshot](#apply-player-snapshot) * [applyDefaultParameters](#applydefaultparameters) * [attachShape](#attachshape) * [cameraFollow](#camerafollow) * [changeMap](#changemap) * [changeRoom](#changeroom) * [clientVisual](#clientvisual) * [createDynamicEvent](#createdynamicevent) * [emit](#emit) * [flash](#flash) * [getCurrentRoom](#getcurrentroom) * [getInShapes](#getinshapes) * [getShapes](#getshapes) * [getTile](#gettile) * [initializeDefaultStats](#initializedefaultstats) * [lastProcessedClientInputTs](#lastprocessedclientinputts) * [lastProcessedInputServerTick](#lastprocessedinputservertick) * [lastProcessedInputTick](#lastprocessedinputtick) * [lastProcessedInputTs](#lastprocessedinputts) * [Listen one-time to data from the client](#listen-one-time-to-data-from-the-client) * [Listen to data from the client](#listen-to-data-from-the-client) * [load](#load) * [otherPlayersCollision](#otherplayerscollision) * [pendingMapPosition](#pendingmapposition) * [Player Snapshot](#player-snapshot) * [playSound](#playsound) * [position](#position) * [position](#position) * [prepareSnapshotForObjectLoad](#preparesnapshotforobjectload) * [Remove listeners of the client event](#remove-listeners-of-the-client-event) * [room](#room) * [Run Sync Changes](#run-sync-changes) * [save](#save) * [setAnimation](#setanimation) * [setGraphicAnimation](#setgraphicanimation) * [setGraphicAnimation](#setgraphicanimation) * [setHitbox](#sethitbox) * [setMass](#setmass) * [setSizes](#setsizes) * [setSync](#setsync) * [shapes](#shapes) * [showAnimation](#showanimation) * [showComponentAnimation](#showcomponentanimation) * [stopAllSounds](#stopallsounds) * [stopSound](#stopsound) * [tiles](#tiles) * [worldPositionX](#worldpositionx) * [worldPositionY](#worldpositiony) ## Apply Player Snapshot Restore authoritative player state without new-game initialization in RPG and MMORPG modes, then run the server onLoad hooks. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} player.applySnapshot(snapshot) ``` ### Parameters * `snapshot`: `string | RpgPlayerSnapshot` ### Returns The resolved snapshot after database references have been restored. ### Examples ```ts theme={null} await player.applySnapshot(saved); ``` ## applyDefaultParameters Apply the built-in default parameter curves to this player. Use this when you want RPGJS to provide the initial parameter setup instead of restoring values from your own database or a saved snapshot. This method only defines the parameter curves and related defaults. It does not restore custom persisted data for you. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} player.applyDefaultParameters() ``` ## attachShape Attach a zone shape to this player using the physic zone system This method creates a zone attached to the player's entity in the physics engine. The zone can be circular or cone-shaped and will detect other entities (players/events) entering or exiting the zone. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} attachShape(idOrOptions: string | AttachShapeOptions, options?: AttachShapeOptions): RpgShape | undefined ``` ### Parameters * `idOrOptions`: `string | AttachShapeOptions` * `options?`: `AttachShapeOptions` ### Examples ```ts theme={null} // Create a circular detection zone player.attachShape("vision", { radius: 150, angle: 360, }); // Create a cone-shaped vision zone player.attachShape("vision", { radius: 200, angle: 120, direction: Direction.Right, limitedByWalls: true, }); // Create a zone with width/height (radius calculated automatically) player.attachShape({ width: 100, height: 100, positioning: "center", }); ``` ## cameraFollow Make the camera follow another player or event This method sends an instruction to the client to fix the viewport on another sprite. The camera will follow the specified player or event, with optional smooth animation. ## Design The camera follow instruction is sent only to this player's client connection. This allows each player to have their own camera target, useful for cutscenes, following NPCs, or focusing on specific events. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} cameraFollow(otherPlayer: RpgPlayer | RpgEvent, options?: { smoothMove?: boolean | { enabled?: boolean; time?: number; ease?: CameraFollowEase; speed?: number; acceleration?: number | null; radius?: number | null; }; }): void ``` ### Parameters * `otherPlayer`: `RpgPlayer | RpgEvent` * `options?`: `{ smoothMove?: boolean | { enabled?: boolean; time?: number; ease?: CameraFollowEase; speed?: number; acceleration?: number | null; radius?: number | null; }; }` ### Examples ```ts theme={null} // Follow another player with default smooth animation player.cameraFollow(otherPlayer, { smoothMove: true }); // Follow an event with custom smooth animation player.cameraFollow(npcEvent, { smoothMove: { time: 1000, ease: "easeInOutQuad" } }); // Follow without animation (instant) player.cameraFollow(targetPlayer, { smoothMove: false }); ``` ## changeMap Change the map for this player * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} changeMap(mapId: string, positions?: { x: number; y: number; z?: number } | string): Promise ``` ### Parameters * `mapId`: `string` * `positions?`: `{ x: number; y: number; z?: number } | string` ### Returns A promise that resolves when the map change is complete ### Examples ```ts theme={null} // Change player to map "town" at position {x: 10, y: 20} await player.changeMap("town", {x: 10, y: 20}); // Change player to map "dungeon" at a named position await player.changeMap("dungeon", "entrance"); // Change player to map "town" at the Tiled "start" position, if present await player.changeMap("town"); ``` ## changeRoom Transfer this player to a registered custom gameplay room. The server resolves the destination, runs authorization hooks, creates a Signe session-transfer token, and tells the client which scene kind to mount. Clients cannot select a destination on their own. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} player.changeRoom(target) ``` ### Parameters * `target`: `RpgRoomTarget` ### Returns `false` when a hook rejects the transfer; otherwise `true`. ### Examples ```ts theme={null} await player.changeRoom({ kind: "battle", params: { id: "encounter-42" }, }) ``` ## clientVisual Trigger a named client visual for this player only. Client visuals are registered in the client module with `clientVisuals`. They group existing client-side visual primitives such as flash, sound, component animations, sprite animations, or camera shake. The server sends only the visual name and a serializable payload, which keeps rendering details on the client and avoids sending several visual packets for one gameplay moment. Use direct APIs like `playSound()`, `flash()`, or `showComponentAnimation()` for one-off visuals. Use `clientVisual()` when several visuals should be orchestrated together by the client. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} clientVisual(name: string, data?: TData): void ``` ### Parameters * `name`: `string` * `data?`: `TData` ### Examples ```ts theme={null} player.clientVisual("hit", { targetId: enemy.id, damage: 25, }); ``` ## createDynamicEvent Legacy v4 helper to create a dynamic event from the player's current map. Prefer `player.getCurrentMap()?.createDynamicEvent(...)` in new code. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} createDynamicEvent(eventObj: EventPosOption): Promise | undefined ``` ### Parameters * `eventObj`: `EventPosOption` ### Returns The created event id, or `undefined` if the player is not on a map. ## emit Send a custom event to the current player's client. Use this to push arbitrary websocket payloads to one client only. On the client side, receive the event by injecting `WebSocketToken` and subscribing with `socket.on(...)`. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} player.emit(type, value) ``` ### Parameters * `type`: `string` * `value?`: `T` ### Examples ```ts theme={null} player.emit("inventory:updated", { slots: player.items().length, }); ``` ```ts theme={null} import { inject } from "@rpgjs/client"; import { WebSocketToken, type AbstractWebsocket } from "@rpgjs/client"; const socket = inject(WebSocketToken); socket.on("inventory:updated", (payload) => { console.log(payload.slots); }); ``` ## flash Trigger a flash animation on this player This method sends a flash animation event to the client, creating a visual feedback effect on the player's sprite. The flash can be configured with various options including type (alpha, tint, or both), duration, cycles, and color. ## Design The flash is sent as a broadcast event to all clients viewing this player. This is useful for visual feedback when the player takes damage, receives a buff, or when an important event occurs. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} flash(options?: { type?: 'alpha' | 'tint' | 'both'; duration?: number; cycles?: number; alpha?: number; tint?: number | string; }): void ``` ### Parameters * `options?`: `{ type?: 'alpha' | 'tint' | 'both'; duration?: number; cycles?: number; alpha?: number; tint?: number | string; }` ### Examples ```ts theme={null} // Simple flash with default settings (alpha flash) player.flash(); // Flash with red tint when taking damage player.flash({ type: 'tint', tint: 0xff0000 }); // Flash with both alpha and tint for dramatic effect player.flash({ type: 'both', alpha: 0.5, tint: 0xff0000, duration: 200, cycles: 2 }); // Quick damage flash player.flash({ type: 'tint', tint: 'red', duration: 150, cycles: 1 }); ``` ## getCurrentRoom Return the active RPGJS room. The result is a lobby, map, or registered custom gameplay room. Use `getCurrentMap()` when map-only APIs are required. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} player.getCurrentRoom() ``` ### Returns The active room, or `null` before the player joins one. ### Examples ```ts theme={null} const battle = player.getCurrentRoom() if (battle?.descriptor.kind === "battle") { console.log(battle.state()) } ``` ## getInShapes Get all shapes where this player is currently located Returns all shapes (from any player/event) where this player is currently inside. This is updated automatically when the player enters or exits shapes. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} getInShapes(): RpgShape[] ``` ### Returns Array of RpgShape instances where this player is located ### Examples ```ts theme={null} // Another player has a detection zone otherPlayer.attachShape("detection", { radius: 200 }); // Check if this player is in any shape const inShapes = player.getInShapes(); if (inShapes.length > 0) { console.log("Player is being detected!"); } ``` ## getShapes Get all shapes attached to this player Returns all shapes that were created using `attachShape()` on this player. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} getShapes(): RpgShape[] ``` ### Returns Array of RpgShape instances attached to this player ### Examples ```ts theme={null} player.attachShape("vision", { radius: 150 }); player.attachShape("detection", { radius: 100 }); const shapes = player.getShapes(); console.log(shapes.length); // 2 ``` ## getTile Legacy v4 Tiled tile lookup. This helper is available only when the current map was loaded through `@rpgjs/tiledmap` / `@canvasengine/tiled`. Coordinates are pixel positions, matching CanvasEngine Tiled's `getTileByPosition(...)` API. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} getTile(x: number, y: number, z?: number): RpgTiledTile | undefined ``` ### Parameters * `x`: `number` * `y`: `number` * `z?`: `number` ### Returns Tiled tile information, or `undefined` when unavailable. ## initializeDefaultStats Initialize the built-in default player stats. This applies the default parameter curves and then restores HP/SP to their current maximum values so the client receives coherent bars on first load. Call this manually in `onConnected()` or `onStart()` when your game relies on the built-in defaults. Do not call it after loading a snapshot or hydrating player data from your own database unless you explicitly want to overwrite those values. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} player.initializeDefaultStats() ``` ## lastProcessedClientInputTs Last client-authored timestamp, kept separately for anti-cheat validation. * Source: `packages/server/src/Player/Player.ts` * Kind: `property` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} lastProcessedClientInputTs: number ``` ## lastProcessedInputServerTick Server physics tick at which that client tick was applied. * Source: `packages/server/src/Player/Player.ts` * Kind: `property` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} lastProcessedInputServerTick: number | null ``` ## lastProcessedInputTick Client physics tick attached to the last processed movement input. * Source: `packages/server/src/Player/Player.ts` * Kind: `property` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} lastProcessedInputTick: number | null ``` ## lastProcessedInputTs Server-clock deadline used to stop idle movement. * Source: `packages/server/src/Player/Player.ts` * Kind: `property` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} lastProcessedInputTs: number ``` ## Listen one-time to data from the client Listen one time to custom data sent by the current player's client. After the first matching event is received, the listener is removed automatically. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` * Since: `3.0.0-beta.5` ### Signature ```ts theme={null} player.once(key, cb) ``` ### Parameters * `key`: `string` * `cb`: `(data: T) => void | Promise` ### Examples ```ts theme={null} player.once("tutorial:ready", (payload) => { console.log("Ready once:", payload.step); }); ``` ## Listen to data from the client Listen to custom data sent by the current player's client. This listens to websocket actions emitted from the client with `socket.emit(key, data)`. It is intended for custom client events that are not already handled by built-in server actions such as `move`, `action`, or GUI interactions. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` * Since: `3.0.0-beta.5` ### Signature ```ts theme={null} player.on(key, cb) ``` ### Parameters * `key`: `string` * `cb`: `(data: T) => void | Promise` ### Examples ```ts theme={null} player.on("chat:message", ({ text }) => { console.log("Client says:", text); }); ``` ```ts theme={null} import { inject } from "@rpgjs/client"; import { WebSocketToken, type AbstractWebsocket } from "@rpgjs/client"; const socket = inject(WebSocketToken); socket.emit("chat:message", { text: "Hello server" }); ``` ## load Load player state. For v4 compatibility, pass a JSON string or plain snapshot object to apply it directly. Pass a slot (`"auto"` or a number) to use the v5 storage strategy. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} load(slot: SaveSlotIndex, context?: SaveRequestContext, options?: { changeMap?: boolean }): Promise ``` ### Parameters * `slot`: `SaveSlotIndex` * `context?`: `SaveRequestContext` * `options?`: `{ changeMap?: boolean }` ## otherPlayersCollision Legacy v4 list of other players or events currently colliding with this player. * Source: `packages/server/src/Player/Player.ts` * Kind: `getter` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} otherPlayersCollision ``` ### Returns Runtime players and events whose physics bodies overlap this player. ## pendingMapPosition Internal: named map position to resolve after the target map data is ready * Source: `packages/server/src/Player/Player.ts` * Kind: `property` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} pendingMapPosition ``` ## Player Snapshot Capture serializable authoritative player state in RPG and MMORPG modes. Derived parameters are recalculated from saved curves, bounds and modifiers. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} player.snapshot() ``` ### Returns Player state suitable for serialization and later restoration. ### Examples ```ts theme={null} const saved = JSON.stringify(player.snapshot()); ``` ## playSound Play a sound on the client side for this player only This method emits an event to play a sound only for this specific player. The sound must be defined on the client side (in the client module configuration). ## Design The sound is sent only to this player's client connection, making it ideal for personal feedback sounds like UI interactions, notifications, or personal achievements. For map-wide sounds that all players should hear, use `map.playSound()` instead. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} playSound(soundId: string, options?: { volume?: number; loop?: boolean } | boolean): void ``` ### Parameters * `soundId`: `string` * `options?`: `{ volume?: number; loop?: boolean } | boolean` ### Examples ```ts theme={null} // Play a sound for this player only (default behavior) player.playSound("item-pickup"); // Play a sound with volume and loop player.playSound("background-music", { volume: 0.5, loop: true }); // Play a notification sound at low volume player.playSound("notification", { volume: 0.3 }); ``` ## position Legacy v4 position object. Prefer the reactive `x`, `y`, and `z` signals in new code. * Source: `packages/server/src/Player/Player.ts` * Kind: `getter` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} position ``` ### Returns Current top-left player position. ## position Set the legacy v4 position object. This updates the player's top-left coordinates and keeps the physics body in sync when the player is currently attached to a map. * Source: `packages/server/src/Player/Player.ts` * Kind: `setter` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} position ``` ## prepareSnapshotForObjectLoad Preserve runtime signals while preparing serialized player data for loading. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} prepareSnapshotForObjectLoad(snapshot: RpgPlayerSnapshot): RpgPlayerSnapshot ``` ### Parameters * `snapshot`: `RpgPlayerSnapshot` ### Returns A copy excluding fields that are restored separately or recomputed. ## Remove listeners of the client event Remove all listeners for a custom client event on this player. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` * Since: `3.0.0-beta.5` ### Signature ```ts theme={null} player.off(key) ``` ### Parameters * `key`: `string` ### Examples ```ts theme={null} player.off("chat:message"); ``` ## room Active RPGJS room. Unlike `map`, this also covers non-spatial gameplay rooms. * Source: `packages/server/src/Player/Player.ts` * Kind: `property` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} room: RpgPlayerRoom | null ``` ## Run Sync Changes Run the change detection cycle. Normally, as soon as a hook is called in a class, the cycle is started. But you can start it manually The method calls the `onChanges` method on events and synchronizes all map data with the client. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Member of: `Player` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} player.syncChanges() ``` ## save Save the player state. For v4 compatibility, calling `save()` without arguments returns a JSON snapshot string. Pass a slot (`"auto"` or a number) to use the v5 storage strategy. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} save(): Promise ``` ## setAnimation * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} setAnimation(animationName: string, nbTimes?: number) ``` ### Parameters * `animationName`: `string` * `nbTimes?`: `number` ## setGraphicAnimation Set the current animation of the player's sprite This method changes the animation state of the player's current sprite. It's used to trigger character animations like attack, skill, or custom movements. When `nbTimes` is set to a finite number, the animation will play that many times before returning to the previous animation state. If `animationFixed` is true, this method will not change the animation. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} setGraphicAnimation(animationName: string, nbTimes: number): void ``` ### Parameters * `animationName`: `string` * `nbTimes`: `number` ## setGraphicAnimation Set the current animation of the player's sprite with a temporary graphic change This method changes the animation state of the player's current sprite and temporarily changes the player's graphic (sprite sheet) during the animation. The graphic is automatically reset when the animation finishes. When `nbTimes` is set to a finite number, the animation will play that many times before returning to the previous animation state and graphic. If `animationFixed` is true, this method will not change the animation. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} setGraphicAnimation(animationName: string, graphic: string | string[], nbTimes: number): void ``` ### Parameters * `animationName`: `string` * `graphic`: `string | string[]` * `nbTimes`: `number` ## setHitbox Set the hitbox of the player for collision detection This method defines the hitbox used for collision detection in the physics engine. The hitbox can be smaller or larger than the visual representation of the player, allowing for precise collision detection. ## Design The hitbox is used by the physics engine to detect collisions with other entities, static obstacles, and shapes. Changing the hitbox will immediately update the collision detection without affecting the visual appearance of the player. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} setHitbox(width: number, height: number): void ``` ### Parameters * `width`: `number` * `height`: `number` ### Examples ```ts theme={null} // Set a 20x20 hitbox for precise collision detection player.setHitbox(20, 20); // Set a larger hitbox for easier collision detection player.setHitbox(40, 40); ``` ## setMass Set the physical mass for this player or event. A mass of `0` or `Infinity` makes the physics body immovable. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} setMass(mass: number): void ``` ### Parameters * `mass`: `number` ## setSizes Legacy v4 size setter. In v5, collision size is represented by the hitbox. This bridge maps the legacy object to `setHitbox(...)`. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} setSizes(obj: { width: number; height: number; hitbox?: { width: number; height: number } }): void ``` ### Parameters * `obj`: `{ width: number; height: number; hitbox?: { width: number; height: number } }` ## setSync Set the sync schema for the map * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} setSync(schema: RpgSyncSchema): void ``` ### Parameters * `schema`: `RpgSyncSchema` ## shapes Legacy v4 list of shapes attached to this player. Prefer `player.getShapes()` in new code. * Source: `packages/server/src/Player/Player.ts` * Kind: `getter` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} shapes ``` ### Returns Shapes created with `player.attachShape(...)`. ## showAnimation * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} showAnimation(graphic: string, animationName: string, replaceGraphic?: boolean) ``` ### Parameters * `graphic`: `string` * `animationName`: `string` * `replaceGraphic?`: `boolean` ## showComponentAnimation Show a temporary component animation on this player This method broadcasts a component animation to all clients, allowing temporary visual effects like hit indicators, spell effects, or status animations to be displayed on the player. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} showComponentAnimation(id: string, params?: TParams): void ``` ### Parameters * `id`: `string` * `params?`: `TParams` ### Examples ```ts theme={null} // Show a hit animation with damage text player.showComponentAnimation("hit", { text: "150", color: "red" }); // Show a heal animation player.showComponentAnimation("heal", { amount: 50 }); ``` ## stopAllSounds Stop all currently playing sounds for this player This method stops all sounds that are currently playing for the player. Useful when changing maps to prevent sound overlap. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} stopAllSounds(): void ``` ### Examples ```ts theme={null} // Stop all sounds before changing map player.stopAllSounds(); await player.changeMap("new-map"); ``` ## stopSound Stop a sound that is currently playing for this player This method stops a sound that was previously started with `playSound()`. The sound must be defined on the client side. * Source: `packages/server/src/Player/Player.ts` * Kind: `method` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} stopSound(soundId: string): void ``` ### Parameters * `soundId`: `string` ### Examples ```ts theme={null} // Start a looping background music player.playSound("background-music", { loop: true }); // Later, stop it player.stopSound("background-music"); ``` ## tiles Legacy v4 list of Tiled tiles currently covered by the player's hitbox. This helper is available only when the current map was loaded through `@rpgjs/tiledmap` / `@canvasengine/tiled`. For non-Tiled maps, it returns `[]`. * Source: `packages/server/src/Player/Player.ts` * Kind: `getter` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} tiles ``` ### Returns Tile information for each Tiled cell touched by the player. ## worldPositionX Computed signal for world X position Calculates the absolute world X position from the map's world position plus the player's local X position. Returns 0 if no map is assigned. * Source: `packages/server/src/Player/Player.ts` * Kind: `getter` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} worldPositionX ``` ### Examples ```ts theme={null} const worldX = player.worldX(); console.log(`Player is at world X: ${worldX}`); ``` ## worldPositionY Computed signal for world Y position Calculates the absolute world Y position from the map's world position plus the player's local Y position. Returns 0 if no map is assigned. * Source: `packages/server/src/Player/Player.ts` * Kind: `getter` * Defined in: `RpgPlayer` ### Signature ```ts theme={null} worldPositionY ``` ### Examples ```ts theme={null} const worldY = player.worldY(); console.log(`Player is at world Y: ${worldY}`); ``` # Components Source: https://v5.rpgjs.dev/api/player/components Component-based UI layout helpers and component utilities for players. # Components Component-based UI layout helpers and component utilities for players. ## Members * [mergeComponents](#mergecomponents) * [mergeComponents](#mergecomponents) * [removeComponents](#removecomponents) * [removeComponents](#removecomponents) * [setComponentsBottom](#setcomponentsbottom) * [setComponentsBottom](#setcomponentsbottom) * [setComponentsCenter](#setcomponentscenter) * [setComponentsCenter](#setcomponentscenter) * [setComponentsLeft](#setcomponentsleft) * [setComponentsLeft](#setcomponentsleft) * [setComponentsRight](#setcomponentsright) * [setComponentsRight](#setcomponentsright) * [setComponentsTop](#setcomponentstop) * [setComponentsTop](#setcomponentstop) * [setGraphic](#setgraphic) * [WithComponentManager](#withcomponentmanager) ## mergeComponents Merge components with existing components at a specific position Merges new components with existing components at the specified position. * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` ### Signature ```ts theme={null} mergeComponents(position: ComponentPosition, layout: ComponentInput, options?: ComponentLayout): void ``` ### Parameters * `position`: `ComponentPosition` * `layout`: `ComponentInput` * `options?`: `ComponentLayout` ### Returns void ### Examples ```ts theme={null} // First set some components player.setComponentsTop([Components.text('{name}')]); // Then merge additional components player.mergeComponents('top', [Components.hpBar()], { width: 100 }); ``` ## mergeComponents Merge components with existing components at a specific position * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` * Defined in: `IComponentManager` ### Signature ```ts theme={null} mergeComponents(position: ComponentPosition, layout: ComponentInput, options?: ComponentLayout): void ``` ### Parameters * `position`: `ComponentPosition` * `layout`: `ComponentInput` * `options?`: `ComponentLayout` ### Returns void ## removeComponents Remove components from a specific position Deletes all components at the specified position. * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` ### Signature ```ts theme={null} removeComponents(position: ComponentPosition): void ``` ### Parameters * `position`: `ComponentPosition` ### Returns void ### Examples ```ts theme={null} player.removeComponents('top'); ``` ## removeComponents Remove components from a specific position * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` * Defined in: `IComponentManager` ### Signature ```ts theme={null} removeComponents(position: ComponentPosition): void ``` ### Parameters * `position`: `ComponentPosition` ### Returns void ## setComponentsBottom Set components to display below the player graphic Components are displayed below the player's sprite and can include text, bars, shapes, or any combination. The components are synchronized to all clients on the map. * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` ### Signature ```ts theme={null} setComponentsBottom(layout: ComponentInput, options?: ComponentLayout): void ``` ### Parameters * `layout`: `ComponentInput` * `options?`: `ComponentLayout` ### Returns void ### Examples ```ts theme={null} player.setComponentsBottom(Components.shape({ fill: '#ff0000', type: 'rectangle', width: 32, height: 32 }), { marginBottom: 16 }); ``` ## setComponentsBottom Set components to display below the player graphic * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` * Defined in: `IComponentManager` ### Signature ```ts theme={null} setComponentsBottom(layout: ComponentInput, options?: ComponentLayout): void ``` ### Parameters * `layout`: `ComponentInput` * `options?`: `ComponentLayout` ### Returns void ## setComponentsCenter Set components to display at the center of the player graphic Components are displayed at the center of the player's sprite. Be careful: if you assign, it deletes the graphics and if the lines are superimposed. * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` ### Signature ```ts theme={null} setComponentsCenter(layout: ComponentInput, options?: ComponentLayout): void ``` ### Parameters * `layout`: `ComponentInput` * `options?`: `ComponentLayout` ### Returns void ### Examples ```ts theme={null} player.setComponentsCenter([ Components.text('{name}'), Components.hpBar() ]); ``` ## setComponentsCenter Set components to display at the center of the player graphic * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` * Defined in: `IComponentManager` ### Signature ```ts theme={null} setComponentsCenter(layout: ComponentInput, options?: ComponentLayout): void ``` ### Parameters * `layout`: `ComponentInput` * `options?`: `ComponentLayout` ### Returns void ## setComponentsLeft Set components to display to the left of the player graphic Components are displayed to the left of the player's sprite. * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` ### Signature ```ts theme={null} setComponentsLeft(layout: ComponentInput, options?: ComponentLayout): void ``` ### Parameters * `layout`: `ComponentInput` * `options?`: `ComponentLayout` ### Returns void ### Examples ```ts theme={null} player.setComponentsLeft([ Components.text('{name}'), Components.hpBar() ]); ``` ## setComponentsLeft Set components to display to the left of the player graphic * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` * Defined in: `IComponentManager` ### Signature ```ts theme={null} setComponentsLeft(layout: ComponentInput, options?: ComponentLayout): void ``` ### Parameters * `layout`: `ComponentInput` * `options?`: `ComponentLayout` ### Returns void ## setComponentsRight Set components to display to the right of the player graphic Components are displayed to the right of the player's sprite. * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` ### Signature ```ts theme={null} setComponentsRight(layout: ComponentInput, options?: ComponentLayout): void ``` ### Parameters * `layout`: `ComponentInput` * `options?`: `ComponentLayout` ### Returns void ### Examples ```ts theme={null} player.setComponentsRight([ Components.text('{name}'), Components.hpBar() ]); ``` ## setComponentsRight Set components to display to the right of the player graphic * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` * Defined in: `IComponentManager` ### Signature ```ts theme={null} setComponentsRight(layout: ComponentInput, options?: ComponentLayout): void ``` ### Parameters * `layout`: `ComponentInput` * `options?`: `ComponentLayout` ### Returns void ## setComponentsTop Set components to display above the player graphic Components are displayed above the player's sprite and can include text, bars, shapes, or any combination. The components are synchronized to all clients on the map. * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` ### Signature ```ts theme={null} setComponentsTop(layout: ComponentInput, options?: ComponentLayout): void ``` ### Parameters * `layout`: `ComponentInput` * `options?`: `ComponentLayout` ### Returns void ### Examples ```ts theme={null} // Single text component player.setComponentsTop(Components.text('{name}')); // Multiple components vertically player.setComponentsTop([ Components.text('HP: {hp}'), Components.text('{name}') ]); // Table layout (columns) player.setComponentsTop([ [Components.text('{hp}'), Components.text('{name}')] ]); // With layout options player.setComponentsTop([ Components.text('HP: {hp}'), Components.text('{name}') ], { width: 100, height: 30, marginBottom: 8 }); ``` ## setComponentsTop Set components to display above the player graphic * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` * Defined in: `IComponentManager` ### Signature ```ts theme={null} setComponentsTop(layout: ComponentInput, options?: ComponentLayout): void ``` ### Parameters * `layout`: `ComponentInput` * `options?`: `ComponentLayout` ### Returns void ## setGraphic Set the graphic(s) for this player Allows setting either a single graphic or multiple graphics for the player. When multiple graphics are provided, they are used for animation sequences. The graphics system provides flexible visual representation that can be dynamically changed during gameplay for different states, equipment, or animations. * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `method` * Defined in: `IComponentManager` ### Signature ```ts theme={null} setGraphic(graphic: GraphicInput): void ``` ### Parameters * `graphic`: `GraphicInput` ### Returns void ### Examples ```ts theme={null} // Set a single graphic for static representation player.setGraphic("hero"); // Set multiple graphics for animation sequences player.setGraphic(["hero_idle", "hero_walk", "hero_run"]); // Set a legacy tile id player.setGraphic(3); // Dynamic graphic changes based on equipment if (player.hasArmor('platemail')) { player.setGraphic("hero_armored"); } // Animation sequences for different actions player.setGraphic(["mage_cast_1", "mage_cast_2", "mage_cast_3"]); ``` ## WithComponentManager Component Manager Mixin Provides graphic and component management capabilities to any class. This mixin allows setting single or multiple graphics for player representation, enabling dynamic visual changes and animation sequences. It also provides methods to display UI components around the player graphic (top, bottom, center, left, right). Components are stored as JSON strings for efficient synchronization. * Source: `packages/server/src/Player/ComponentManager.ts` * Kind: `function` ### Signature ```ts theme={null} WithComponentManager(Base: TBase): new (...args: ConstructorParameters) => InstanceType & IComponentManager ``` ### Parameters * `Base`: `TBase` ### Returns Extended class with component management methods ### Examples ```ts theme={null} class MyPlayer extends WithComponentManager(BasePlayer) { constructor() { super(); this.setGraphic("hero"); } } const player = new MyPlayer(); player.setGraphic(["hero_idle", "hero_walk"]); player.setComponentsTop(Components.text('{name}')); ``` # Effect Commands Source: https://v5.rpgjs.dev/api/player/effect-commands Apply and inspect player effects. # Effect Commands Apply and inspect player effects. ## Members * [effects](#effects) * [hasEffect](#haseffect) * [WithEffectManager](#witheffectmanager) ## effects Gets all currently active effects on the player from multiple sources: * Direct effects assigned to the player * Effects from active states (buffs/debuffs) * Effects from equipped weapons and armor The returned array contains unique effects without duplicates. * Source: `packages/server/src/Player/EffectManager.ts` * Kind: `property` * Defined in: `IEffectManager` ### Signature ```ts theme={null} effects: string[] ``` ### Returns Array of all active effects on the player ## hasEffect Check if the player has a specific effect Determines whether the player currently has the specified effect active. This includes effects from states, equipment, and temporary conditions. The effect system provides a flexible way to apply various gameplay restrictions and enhancements to the player. * Source: `packages/server/src/Player/EffectManager.ts` * Kind: `method` * Defined in: `IEffectManager` ### Signature ```ts theme={null} hasEffect(effect: string): boolean ``` ### Parameters * `effect`: `string` ### Returns true if the player has the effect, false otherwise ### Examples ```ts theme={null} import { Effect } from '@rpgjs/database' // Check for skill restriction const cannotUseSkills = player.hasEffect(Effect.CAN_NOT_SKILL); if (cannotUseSkills) { console.log('Player cannot use skills right now'); } // Check for guard effect const isGuarding = player.hasEffect(Effect.GUARD); if (isGuarding) { console.log('Player is in guard stance'); } // Check for cost reduction const halfCost = player.hasEffect(Effect.HALF_SP_COST); const actualCost = skillCost / (halfCost ? 2 : 1); ``` ## WithEffectManager Effect Manager Mixin Provides effect management capabilities to any class. This mixin handles player effects including restrictions, buffs, and debuffs. Effects can come from various sources like states, equipment, and temporary conditions. * Source: `packages/server/src/Player/EffectManager.ts` * Kind: `function` ### Signature ```ts theme={null} WithEffectManager(Base: TBase) ``` ### Parameters * `Base`: `TBase` ### Returns Extended class with effect management methods ### Examples ```ts theme={null} class MyPlayer extends WithEffectManager(BasePlayer) { constructor() { super(); // Effect system is automatically initialized } } const player = new MyPlayer(); player.effects = [Effect.GUARD]; console.log(player.hasEffect(Effect.GUARD)); // true ``` # Element Commands Source: https://v5.rpgjs.dev/api/player/element-commands Element offense, defense, and coefficient helpers for players. # Element Commands Element offense, defense, and coefficient helpers for players. ## Members * [coefficientElements](#coefficientelements) * [elements](#elements) * [elementsDefense](#elementsdefense) * [elementsEfficiency](#elementsefficiency) * [WithElementManager](#withelementmanager) ## coefficientElements Calculate elemental damage coefficient against another player Determines the damage multiplier when this player attacks another player, taking into account the attacker's offensive elements, the defender's elemental efficiency, and elemental defense from equipment. This is used in the battle system to calculate elemental damage modifiers. * Source: `packages/server/src/Player/ElementManager.ts` * Kind: `method` * Defined in: `IElementManager` ### Signature ```ts theme={null} coefficientElements(otherPlayer: RpgPlayer): number ``` ### Parameters * `otherPlayer`: `RpgPlayer` ### Returns Numerical coefficient to multiply base damage by ### Examples ```ts theme={null} // Calculate elemental damage coefficient const firePlayer = new MyPlayer(); const icePlayer = new MyPlayer(); // Fire player attacks ice player (assuming ice is weak to fire) const coefficient = icePlayer.coefficientElements(firePlayer); console.log(`Damage multiplier: ${coefficient}`); // e.g., 2.0 for double damage // Use in damage calculation const baseDamage = 100; const finalDamage = baseDamage * coefficient; console.log(`Final damage: ${finalDamage}`); // Check for elemental advantage if (coefficient > 1) { console.log('Attacker has elemental advantage!'); } else if (coefficient < 1) { console.log('Defender resists this element'); } ``` ## elements Gets all offensive elements available to the player from equipped weapons and armor. This determines what elemental damage types the player can deal in combat. The system automatically combines elements from all equipped items and removes duplicates. * Source: `packages/server/src/Player/ElementManager.ts` * Kind: `property` * Defined in: `IElementManager` ### Signature ```ts theme={null} elements: { rate: number; element: string; }[] ``` ### Returns Array of element objects with rate and element properties for offensive capabilities ## elementsDefense Gets the defensive capabilities against various elements from equipped items. The system automatically consolidates multiple defensive items, keeping only the highest protection rate for each element type. * Source: `packages/server/src/Player/ElementManager.ts` * Kind: `property` * Defined in: `IElementManager` ### Signature ```ts theme={null} elementsDefense: ElementAffinity[] ``` ### Returns Array of element defense objects with rate and element properties ## elementsEfficiency Manages the player's elemental efficiency modifiers, which determine how effective different elements are against this player. Values greater than 1 indicate vulnerability, while values less than 1 indicate resistance. This combines both class-based efficiency and player-specific modifiers. * Source: `packages/server/src/Player/ElementManager.ts` * Kind: `property` * Defined in: `IElementManager` ### Signature ```ts theme={null} elementsEfficiency: ElementAffinity[] ``` ### Returns Array of element efficiency objects with rate and element properties ## WithElementManager Element Manager Mixin Provides elemental management capabilities to any class. This mixin handles elemental resistances, vulnerabilities, and attack elements. It manages both defensive capabilities (elementsDefense) and offensive elements from equipment, as well as player-specific elemental efficiency modifiers. * Source: `packages/server/src/Player/ElementManager.ts` * Kind: `function` ### Signature ```ts theme={null} WithElementManager(Base: TBase) ``` ### Parameters * `Base`: `TBase` ### Returns Extended class with element management methods ### Examples ```ts theme={null} class MyPlayer extends WithElementManager(BasePlayer) { constructor() { super(); this.elementsEfficiency = [{ rate: 0.5, element: 'fire' }]; } } const player = new MyPlayer(); const fireResistance = player.elementsDefense.find(e => e.element === 'fire'); ``` # Gold Commands Source: https://v5.rpgjs.dev/api/player/gold-commands Read and update player gold on the server side. # Gold Commands Read and update player gold on the server side. ## Members * [Change Gold](#change-gold) ## Change Gold You can change the game money ```ts theme={null} player.gold += 100 ``` * Source: `packages/server/src/Player/GoldManager.ts` * Kind: `property` * Member of: `GoldManager` * Defined in: `GoldManager` ### Signature ```ts theme={null} {number} player.gold ``` ### Default ```ts theme={null} 0 ``` # GUI Commands Source: https://v5.rpgjs.dev/api/player/gui-commands Dialogs, menus, notifications, and custom GUI commands. # GUI Commands Dialogs, menus, notifications, and custom GUI commands. ## Members * [Call custom GUI](#call-custom-gui) * [Call Game Over Menu](#call-game-over-menu) * [Call Main Menu](#call-main-menu) * [Call Shop Menu](#call-shop-menu) * [Close custom GUI](#close-custom-gui) * [Displays a notification](#displays-a-notification) * [Hide Hotbar](#hide-hotbar) * [Hide to GUI attached](#hide-to-gui-attached) * [Show Character Select](#show-character-select) * [Show Choices](#show-choices) * [Show Hotbar](#show-hotbar) * [Show Input](#show-input) * [Show Load](#show-load) * [Show Save](#show-save) * [Show Save/Load](#show-save-load) * [Show Text](#show-text) * [View to GUI attached](#view-to-gui-attached) * [WithGuiManager](#withguimanager) ## Call custom GUI Call a custom Gui ```ts theme={null} // Calls a client-side component, created with VueJS, named "inn". const gui = player.gui('inn') // You can wait for actions on the menu. It only works if the menu is open. gui.on('accept', () => { player.allRecovery() }) // The GUI is opened by passing recoverable data on the client side. gui.open({ hello: 'world' }) ``` When opening the GUI, one can give options ```ts theme={null} await gui.open({ hello: 'world' }, { waitingAction: true, blockPlayerInput: true }) // After the GUI is closed ``` * `blockPlayerInput`: while the GUI is open, the player can not move on the map * `waitingAction`: We explicitly wait until the GUI is closed for the promise to be resolved. * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `GuiManager` * Defined in: `GuiManagerMixin` ### Signature ```ts theme={null} player.gui(guiId) ``` ### Parameters * `guiId`: `string` ## Call Game Over Menu Calls game over menu. Opens the GUI named `rpg-gameover` ```ts theme={null} const selection = await player.callGameover() if (selection?.id === 'title') { await player.gui('rpg-title-screen').open() } if (selection?.id === 'load') { await player.showLoad() } ``` * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `GuiManager` * Defined in: `IGuiManager` ### Signature ```ts theme={null} player.callGameover(options) ``` ### Parameters * `options?`: `GameoverGuiOptions` ## Call Main Menu Calls main menu. Opens the GUI named `rpg-main-menu` * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `GuiManager` * Defined in: `IGuiManager` ### Signature ```ts theme={null} player.callMainMenu(options) ``` ### Parameters * `options?`: `MenuGuiOptions` ## Call Shop Menu Calls shop menu. Opens the GUI named `rpg-shop` * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `GuiManager` * Defined in: `GuiManagerMixin` ### Signature ```ts theme={null} player.callShop() ``` ### Parameters * `items`: `ShopItemInput[] | ShopGuiOptions` ## Close custom GUI Closes the GUI and removes it from memory * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `GuiManager` * Defined in: `GuiManagerMixin` ### Signature ```ts theme={null} player.removeGui(guiId,data) ``` ### Parameters * `guiId`: `string` * `data?`: `unknown` * `guiOpenId?`: `unknown` ## Displays a notification Displays a notification . Opens the GUI named `rpg-notification` * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `GuiManager` * Defined in: `IGuiManager` ### Signature ```ts theme={null} player.showNotification() ``` ### Parameters * `message`: `string` * `options?`: `{ time?: number; icon?: string; sound?: string; type?: "info" | "warn" | "error" }` ## Hide Hotbar Hide the default hotbar GUI. Persistent slot assignments are unchanged. Call `showHotbar()` to create and display the GUI again. * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `GuiManagerMixin` ### Signature ```ts theme={null} player.hideHotbar() ``` ### Examples ```ts theme={null} player.hideHotbar(); await player.showHotbar(); ``` ## Hide to GUI attached Hide the GUI attached to the players * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `GuiManager` * Defined in: `GuiManagerMixin` * Since: `3.0.0-beta.5` ### Signature ```ts theme={null} player.hideAttachedGui(players?) ``` ### Parameters * `players?`: `RpgPlayer[] | RpgPlayer` ### Examples ```ts theme={null} player.hideAttachedGui() ``` ```ts theme={null} player.hideAttachedGui(aPlayer) ``` ```ts theme={null} player.hideAttachedGui([player1, player2]) ``` ## Show Character Select Display the prebuilt character selection GUI and return the server-owned actor chosen by the player. This method never applies the actor. * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `GuiManagerMixin` ### Signature ```ts theme={null} player.showCharacterSelect(actors,options) ``` ### Parameters * `actors`: `readonly ActorInput[]` * `options?`: `CharacterSelectOptions` ### Returns The validated actor, or `null` when cancellation is allowed and requested. ### Examples ```ts theme={null} const actor = await player.showCharacterSelect([Hero, Mage]) if (actor) player.setActor(actor) ``` ## Show Choices Shows a dialog box with a choice. Opens the GUI named `rpg-dialog` ```ts theme={null} const choice = await player.showChoices('What color do you prefer?', [ { text: 'Black', value: 'black' }, { text: 'Rather the blue', value: 'blue' }, { text: 'I don\'t have a preference!', value: 'none' } ]) // If the player selects the first console.log(choice) // { text: 'Black', value: 'black' } ``` * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `GuiManager` * Defined in: `IGuiManager` ### Signature ```ts theme={null} player.showChoices(text,choices) ``` ### Parameters * `msg`: `string` * `choices`: `Choice[]` * `options?`: `DialogBaseOptions` ## Show Hotbar Display the persistent player hotbar. The server owns slot content and validates every use. The default client GUI provides direct keyboard shortcuts and a gamepad radial selector. Calling it again refreshes the existing GUI without duplicating it. * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `GuiManagerMixin` ### Signature ```ts theme={null} player.showHotbar(options) ``` ### Parameters * `options?`: `HotbarGuiOptions` ### Returns The GUI open result. ### Examples ```ts theme={null} await player.showHotbar({ capacity: 8, lockedSlotHint: (_current, slot) => `Unlock slot ${slot + 1}`, }); ``` ## Show Input Opens the prebuilt input GUI and waits for the player to submit or cancel it. The player cannot move while the form is open. Number inputs resolve to a `number`; text inputs and textareas resolve to a `string`; cancellation and an empty optional number input resolve to `null`. ```ts theme={null} const age = await player.showInput('Your age', { type: 'number', required: true, min: 1 }) // age is number | null const biography = await player.showInput('Biography', { control: 'textarea', rows: 6, maxLength: 500 }) // biography is string | null ``` * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `GuiManager` * Defined in: `IGuiManager` ### Signature ```ts theme={null} player.showInput(message,options) ``` ### Parameters * `message`: `string` * `options`: `NumberInputOptions` ### Returns The typed submitted value, or `null` when cancelled or when an optional number is empty. ## Show Load Display a load slots screen. Opens the GUI named `rpg-save` ```ts theme={null} const index = await player.showLoad(slots) ``` * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `GuiManager` * Defined in: `IGuiManager` ### Signature ```ts theme={null} player.showLoad(slots,options) ``` ### Parameters * `slots?`: `SaveSlot[]` * `options?`: `SaveLoadOptions` ## Show Save Display a save slots screen. Opens the GUI named `rpg-save` ```ts theme={null} const index = await player.showSave(slots) ``` * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `GuiManager` * Defined in: `IGuiManager` ### Signature ```ts theme={null} player.showSave(slots,options) ``` ### Parameters * `slots?`: `SaveSlot[]` * `options?`: `SaveLoadOptions` ## Show Save/Load Display a save/load slots screen. Opens the GUI named `rpg-save` ```ts theme={null} const index = await player.showSaveLoad(slots, { mode: 'save' }) ``` * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `GuiManager` * Defined in: `IGuiManager` ### Signature ```ts theme={null} player.showSaveLoad(slots,options) ``` ### Parameters * `slots?`: `SaveSlot[]` * `options?`: `SaveLoadOptions` ## Show Text Show a text. This is a graphical interface already built. Opens the GUI named `rpg-dialog` ```ts theme={null} player.showText('Hello World') ``` The method returns a promise. It is resolved when the dialog box is closed. ```ts theme={null} await player.showText('Hello World') // dialog box is closed, then ... ``` **Option: position** You can define how the dialog box is displayed: * top * middle * bottom (bottom by default) ```ts theme={null} player.showText('Hello World', { position: 'top' }) ``` Add a typed input directly below the dialog text: ```ts theme={null} const age = await player.showText('How old are you?', { input: { type: 'number', required: true, min: 1 } }) // age is number | null ``` **Option: fullWidth** `boolean` (true by default) Indicate that the dialog box will take the full width of the screen. ```ts theme={null} player.showText('Hello World', { fullWidth: true }) ``` **Option: autoClose** `boolean` (false by default) If false, the user will have to press Enter to close the dialog box. ```ts theme={null} player.showText('Hello World', { autoClose: true }) ``` **Option: typewriterEffect** `boolean` (true by default) Performs a typewriter effect ```ts theme={null} player.showText('Hello World', { typewriterEffect: false }) ``` **Option: talkWith** `RpgPlayer` (nothing by default) If you specify the event or another player, the other player will stop his or her movement and look in the player's direction. ```ts theme={null} // Code in an event player.showText('Hello World', { talkWith: this }) ``` * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `GuiManager` * Defined in: `IGuiManager` ### Signature ```ts theme={null} player.showText(text,options) ``` ### Parameters * `msg`: `string` * `options`: `DialogBaseOptions & { input: NumberInputOptions }` ## View to GUI attached Display the GUI attached to the players If you don't specify the players as parameters, it will display the GUI of the instance But you can specify which GUIs to display by specifying the players as the first parameter * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `method` * Member of: `GuiManager` * Defined in: `GuiManagerMixin` * Since: `3.0.0-beta.5` ### Signature ```ts theme={null} player.showAttachedGui(players?) ``` ### Parameters * `players?`: `RpgPlayer[] | RpgPlayer` ### Examples ```ts theme={null} player.showAttachedGui() ``` ```ts theme={null} player.showAttachedGui(aPlayer) ``` ```ts theme={null} player.showAttachedGui([player1, player2]) ``` ## WithGuiManager GUI Manager Mixin Provides graphical user interface management capabilities to any class. This mixin handles dialog boxes, menus, notifications, shops, and custom GUI components. It manages the complete GUI system including opening, closing, and data passing between client and server. * Source: `packages/server/src/Player/GuiManager.ts` * Kind: `function` ### Signature ```ts theme={null} WithGuiManager(Base: TBase): new (...args: ConstructorParameters) => InstanceType & IGuiManager ``` ### Parameters * `Base`: `TBase` ### Returns Extended class with GUI management methods ### Examples ```ts theme={null} class MyPlayer extends WithGuiManager(BasePlayer) { constructor() { super(); // GUI system is automatically initialized } } const player = new MyPlayer(); await player.showText('Hello World!'); player.callMainMenu(); ``` # Hotbar Commands Source: https://v5.rpgjs.dev/api/player/hotbar-commands Assign and use persistent server-authoritative skill and item slots. # Hotbar Commands Assign and use persistent server-authoritative skill and item slots. ## Members * [action](#action) * [allowedEntryTypes](#allowedentrytypes) * [Assign Hotbar Slot](#assign-hotbar-slot) * [capacity](#capacity) * [Clear Hotbar Slot](#clear-hotbar-slot) * [Configure Hotbar](#configure-hotbar) * [entry](#entry) * [Get Hotbar](#get-hotbar) * [Get Hotbar Capacity](#get-hotbar-capacity) * [Get Hotbar Locked Slot Hint](#get-hotbar-locked-slot-hint) * [getHotbarEntryType](#gethotbarentrytype) * [Initialize Hotbar](#initialize-hotbar) * [Is Hotbar Entry Type Allowed](#is-hotbar-entry-type-allowed) * [lockedSlotHint](#lockedslothint) * [Refresh Hotbar](#refresh-hotbar) * [registerHotbarEntryType](#registerhotbarentrytype) * [resolve](#resolve) * [resolveHotbarEntryPresentation](#resolvehotbarentrypresentation) * [Select Hotbar Slot](#select-hotbar-slot) * [slot](#slot) * [slot](#slot) * [state](#state) * [target](#target) * [type](#type) * [use](#use) * [Use Active Hotbar Slot](#use-active-hotbar-slot) * [Use Hotbar Slot](#use-hotbar-slot) * [validate](#validate) ## action Mutation or refresh that produced the snapshot. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `property` * Defined in: `HotbarChangePayload` ### Signature ```ts theme={null} action: "initialize" | "assign" | "clear" | "select" | "refresh" ``` ## allowedEntryTypes Entry types available in this hotbar. Omit this option to allow every registered entry type. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `property` * Defined in: `HotbarConfiguration` ### Signature ```ts theme={null} allowedEntryTypes: HotbarAllowedEntryTypesResolver ``` ## Assign Hotbar Slot Assign an available slot, moving any duplicate entry. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `HotbarManagerMixin` ### Signature ```ts theme={null} player.assignHotbarSlot(slot,entry) ``` ### Parameters * `slot`: `number` * `entry`: `HotbarEntry` ### Returns The updated detached state. ## capacity Number of currently accessible slots or a dynamic player resolver. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `property` * Defined in: `HotbarConfiguration` ### Signature ```ts theme={null} capacity: HotbarCapacityResolver ``` ## Clear Hotbar Slot Clear a slot while preserving all other assignments. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `HotbarManagerMixin` ### Signature ```ts theme={null} player.clearHotbarSlot(slot) ``` ### Parameters * `slot`: `number` ### Returns The updated detached state. ## Configure Hotbar Configure dynamic capacity, allowed entry types, and locked-slot messaging for this player. The resolver is evaluated on refresh and gameplay changes. Reducing capacity keeps assignments in locked slots so they return if capacity grows again. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `HotbarManagerMixin` ### Signature ```ts theme={null} player.configureHotbar(options) ``` ### Parameters * `options?`: `HotbarConfiguration` ### Returns The refreshed detached hotbar state. ### Examples ```ts theme={null} player.configureHotbar({ capacity: current => Math.min(10, current.level + 2), allowedEntryTypes: ["skill", "item"], lockedSlotHint: (_current, slot) => `Unlocks at level ${slot + 1}`, }); ``` ## entry Assigned or selected entry when available. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `property` * Defined in: `HotbarChangePayload` ### Signature ```ts theme={null} entry: HotbarEntry ``` ## Get Hotbar Return a detached snapshot of the player's persistent hotbar. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `HotbarManagerMixin` ### Signature ```ts theme={null} player.getHotbar() ``` ### Returns The current detached ten-slot state. ## Get Hotbar Capacity Return the number of slots currently available to the player. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `HotbarManagerMixin` ### Signature ```ts theme={null} player.getHotbarCapacity() ``` ### Returns An integer between 1 and 10. ## Get Hotbar Locked Slot Hint Return the optional unlock hint for a slot. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `HotbarManagerMixin` ### Signature ```ts theme={null} player.getHotbarLockedSlotHint(slot) ``` ### Parameters * `slot`: `number` ### Returns The resolved hint, or `undefined`. ## getHotbarEntryType Return the registered definition for an entry type. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `function` ### Signature ```ts theme={null} getHotbarEntryType(type: string): HotbarEntryTypeDefinition | undefined ``` ### Parameters * `type`: `string` ### Returns The definition, or `undefined` when the type is unknown. ## Initialize Hotbar Seed the hotbar once from explicit entries or the player loadout. Learned skills are placed before usable consumable items when `entries` is omitted. Invalid explicit entries are ignored. Calling this method after initialization preserves the player's assignments. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `HotbarManagerMixin` ### Signature ```ts theme={null} player.initializeHotbar(entries) ``` ### Parameters * `entries?`: `HotbarEntry[]` ### Returns The initialized state, or the existing state when already seeded. ### Examples ```ts theme={null} player.initializeHotbar([ { type: "item", id: "berry-snack" }, { type: "skill", id: "water-crops" }, ]); ``` ## Is Hotbar Entry Type Allowed Return whether an entry type is enabled by the current configuration. A missing `allowedEntryTypes` option allows every registered or plugin-provided type. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `HotbarManagerMixin` ### Signature ```ts theme={null} player.isHotbarEntryTypeAllowed(type) ``` ### Parameters * `type`: `string` ### Returns `true` when entries of this type may be displayed and used. ## lockedSlotHint Player-visible unlock hint or a per-slot resolver. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `property` * Defined in: `HotbarConfiguration` ### Signature ```ts theme={null} lockedSlotHint: HotbarLockedSlotHintResolver ``` ## Refresh Hotbar Re-evaluate dynamic configuration and refresh an open hotbar GUI. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `HotbarManagerMixin` ### Signature ```ts theme={null} player.refreshHotbar() ``` ### Returns The refreshed detached hotbar state. ## registerHotbarEntryType Register a server-side hotbar entry type. The definition owns validation, client presentation, and authoritative use. The returned function restores the previous definition. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `function` ### Signature ```ts theme={null} registerHotbarEntryType(definition: HotbarEntryTypeDefinition): () => void ``` ### Parameters * `definition`: `HotbarEntryTypeDefinition` ### Returns A cleanup function that restores the previous definition. ### Examples ```ts theme={null} const unregister = registerHotbarEntryType({ type: "emote", validate(_player, id) { if (id !== "wave") throw new Error("Unknown emote"); }, resolve(_player, id) { return { id, type: "emote", name: "Wave", usable: true, activation: { mode: "instant" }, }; }, use(player) { player.showAnimation("wave"); }, }); ``` ## resolve Build the serializable presentation sent to the current player. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Defined in: `HotbarEntryTypeDefinition` ### Signature ```ts theme={null} resolve(player: RpgPlayer, id: string): HotbarEntryPresentation ``` ### Parameters * `player`: `RpgPlayer` * `id`: `string` ## resolveHotbarEntryPresentation Resolve the serializable client presentation for a hotbar entry. Unavailable or unknown entries return a disabled fallback instead of exposing an authoritative validation error to the client. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `function` ### Signature ```ts theme={null} resolveHotbarEntryPresentation(player: RpgPlayer, entry: HotbarEntry): HotbarEntryPresentation ``` ### Parameters * `player`: `RpgPlayer` * `entry`: `HotbarEntry` ### Returns Serializable presentation for the generic hotbar GUI. ## Select Hotbar Slot Persist the player's active available slot. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `HotbarManagerMixin` ### Signature ```ts theme={null} player.selectHotbarSlot(slot) ``` ### Parameters * `slot`: `number` ### Returns The updated detached state. ## slot Zero-based slot used by the player. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `property` * Defined in: `HotbarUseContext` ### Signature ```ts theme={null} slot: number ``` ## slot Affected zero-based slot when the action targets one slot. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `property` * Defined in: `HotbarChangePayload` ### Signature ```ts theme={null} slot: number ``` ## state Detached state after the change. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `property` * Defined in: `HotbarChangePayload` ### Signature ```ts theme={null} state: HotbarState ``` ## target Optional targeting data supplied by the client or battle module. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `property` * Defined in: `HotbarUseContext` ### Signature ```ts theme={null} target: unknown ``` ## type Unique serialized entry type. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `property` * Defined in: `HotbarEntryTypeDefinition` ### Signature ```ts theme={null} type: string ``` ## use Execute the authoritative gameplay action. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Defined in: `HotbarEntryTypeDefinition` ### Signature ```ts theme={null} use(player: RpgPlayer, id: string, context: HotbarUseContext): unknown | Promise ``` ### Parameters * `player`: `RpgPlayer` * `id`: `string` * `context`: `HotbarUseContext` ## Use Active Hotbar Slot Use the currently active slot, if one is selected. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `HotbarManagerMixin` ### Signature ```ts theme={null} player.useActiveHotbarSlot(target) ``` ### Parameters * `target?`: `unknown` ### Returns The entry handler result, or `null` when no slot is active. ## Use Hotbar Slot Use an entry through its registered authoritative type handler. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Member of: `RpgPlayer` * Defined in: `HotbarManagerMixin` ### Signature ```ts theme={null} player.useHotbarSlot(slot,target) ``` ### Parameters * `slot`: `number` * `target?`: `unknown` ### Returns The entry handler result, or `null` for an empty slot. ## validate Throw when the player may not assign or use this entry. * Source: `packages/server/src/Player/HotbarManager.ts` * Kind: `method` * Defined in: `HotbarEntryTypeDefinition` ### Signature ```ts theme={null} validate(player: RpgPlayer, id: string): void ``` ### Parameters * `player`: `RpgPlayer` * `id`: `string` # Player API Source: https://v5.rpgjs.dev/api/player/index Server-side Player API reference for RpgPlayer and its managers. # Player API This reference is generated from the JSDoc comments in `@rpgjs/server`. It groups the server-side Player API by manager so you can browse the available commands more easily. ## Pages * [Common Commands](/api/player/common-commands) * [Components](/api/player/components) * [Parameter Commands](/api/player/parameter-commands) * [Class Commands](/api/player/class-commands) * [Gold Commands](/api/player/gold-commands) * [State Commands](/api/player/state-commands) * [Element Commands](/api/player/element-commands) * [Item Commands](/api/player/item-commands) * [Skill Commands](/api/player/skill-commands) * [Hotbar Commands](/api/player/hotbar-commands) * [Variable Commands](/api/player/variable-commands) * [Move Commands](/api/player/move-commands) * [GUI Commands](/api/player/gui-commands) * [Effect Commands](/api/player/effect-commands) * [Battle Commands](/api/player/battle-commands) # Item Commands Source: https://v5.rpgjs.dev/api/player/item-commands Inventory, equipment, and item usage commands for players. # Item Commands Inventory, equipment, and item usage commands for players. ## Members * [\_type](#type) * [addItem](#additem) * [addStates](#addstates) * [atk](#atk) * [buyItem](#buyitem) * [consumable](#consumable) * [createItemInstance](#createiteminstance) * [description](#description) * [elements](#elements) * [equip](#equip) * [getItem](#getitem) * [hasItem](#hasitem) * [hitRate](#hitrate) * [hpValue](#hpvalue) * [id](#id) * [mpValue](#mpvalue) * [name](#name) * [onAdd](#onadd) * [onEquip](#onequip) * [onRemove](#onremove) * [onUse](#onuse) * [onUseFailed](#onusefailed) * [paramsModifier](#paramsmodifier) * [pdef](#pdef) * [price](#price) * [removeItem](#removeitem) * [removeStates](#removestates) * [resolveEquipmentsSnapshot](#resolveequipmentssnapshot) * [resolveItemsSnapshot](#resolveitemssnapshot) * [sdef](#sdef) * [sellItem](#sellitem) * [useItem](#useitem) * [WithItemManager](#withitemmanager) ## \_type Item type (for equipment validation) * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemData` ### Signature ```ts theme={null} _type: 'item' | 'weapon' | 'armor' ``` ## addItem Add an item in the player's inventory You can add items using: * Item class (automatically registered in database if needed) * Item object (automatically registered in database if needed) * String ID (must be pre-registered in database) The `onAdd()` method is called on the ItemClass or ItemObject when the item is added. * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `method` * Defined in: `IItemManager` ### Signature ```ts theme={null} addItem(item: ItemClass | ItemObject | string, nb?: number): Item ``` ### Parameters * `item`: `ItemClass | ItemObject | string` * `nb?`: `number` ### Returns The item instance added to inventory ### Examples ```ts theme={null} import Potion from 'your-database/potion' // Add using class player.addItem(Potion, 5) // Add using string ID (must be registered in database) player.addItem('Potion', 3) // Add using object player.addItem({ id: 'custom-potion', name: 'Custom Potion', price: 200, onAdd(player) { console.log('Custom potion added!') } }, 2) ``` ## addStates States to add when used * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemData` ### Signature ```ts theme={null} addStates: StateApplication[] ``` ## atk Get the player's attack (sum of items equipped) Returns the total attack value from all equipped items on the player. * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `IItemManager` ### Signature ```ts theme={null} atk: number ``` ### Returns Total attack value from equipped items ### Examples ```ts theme={null} console.log(player.atk) // 150 (sum of all equipped weapons/armors attack) ``` ## buyItem Purchases an item and reduces the amount of gold The player's gold is reduced by `nb * item.price`. The item is then added to the inventory. The `onAdd()` method is called on the ItemClass when the item is added. * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `method` * Defined in: `IItemManager` ### Signature ```ts theme={null} buyItem(item: ItemClass | ItemObject | string, nb?: number): Item ``` ### Parameters * `item`: `ItemClass | ItemObject | string` * `nb?`: `number` ### Returns Item instance added to inventory ### Examples ```ts theme={null} import Potion from 'your-database/potion' try { player.buyItem(Potion) } catch (err) { if (err.id === 'NOT_ENOUGH_GOLD') { console.log('Not enough gold!') } else if (err.id === 'NOT_PRICE') { console.log('Item has no price!') } } ``` ## consumable Whether the item is consumable * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemData` ### Signature ```ts theme={null} consumable: boolean ``` ## createItemInstance Create an item instance without inventory changes or hook execution. * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `method` ### Signature ```ts theme={null} createItemInstance(item: ItemClass | ItemObject | string, nb?: number) ``` ### Parameters * `item`: `ItemClass | ItemObject | string` * `nb?`: `number` ## description Item description * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemData` ### Signature ```ts theme={null} description: string ``` ## elements Elemental properties * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemData` ### Signature ```ts theme={null} elements: ElementAffinity[] ``` ## equip Equips a weapon or armor on a player Think first to add the item in the inventory with the `addItem()` method before equipping the item, or pass `"auto"` to add the item if it is missing and equip it. The `onEquip()` method is called on the ItemClass when the item is equipped or unequipped. * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `method` * Defined in: `IItemManager` ### Signature ```ts theme={null} equip(itemId: string, equip?: boolean | 'auto'): void ``` ### Parameters * `itemId`: `string` * `equip?`: `boolean | 'auto'` ### Examples ```ts theme={null} try { player.addItem('sword') player.equip('sword') // Later, unequip it player.equip('sword', false) } catch (err) { console.log(err) } ``` ## getItem Retrieves the information of an object: the number and the instance The returned Item instance contains the quantity information accessible via `quantity()` method. * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `method` * Defined in: `IItemManager` ### Signature ```ts theme={null} getItem(itemClass: ItemClass | string): Item ``` ### Parameters * `itemClass`: `ItemClass | string` ### Returns Item instance containing quantity and item data ### Examples ```ts theme={null} import Potion from 'your-database/potion' player.addItem(Potion, 5) const inventory = player.getItem(Potion) console.log(inventory.quantity()) // 5 console.log(inventory) // ``` ## hasItem Check if the player has the item in his inventory * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `method` * Defined in: `IItemManager` ### Signature ```ts theme={null} hasItem(itemClass: ItemClass | string): boolean ``` ### Parameters * `itemClass`: `ItemClass | string` ### Returns `true` if player has the item, `false` otherwise ### Examples ```ts theme={null} import Potion from 'your-database/potion' player.hasItem(Potion) // false player.addItem(Potion, 1) player.hasItem(Potion) // true ``` ## hitRate Chance to successfully use the item (0-1) * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemData` ### Signature ```ts theme={null} hitRate: number ``` ## hpValue HP value restored when used * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemData` ### Signature ```ts theme={null} hpValue: number ``` ## id Item identifier (required if not using class or string) * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` ### Signature ```ts theme={null} id: string ``` ## mpValue MP/SP value restored when used * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemData` ### Signature ```ts theme={null} mpValue: number ``` ## name Item name * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemData` ### Signature ```ts theme={null} name: string ``` ## onAdd Called when the item is added to the player's inventory * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemHooks` ### Signature ```ts theme={null} onAdd: (player: RpgPlayer) => void | Promise ``` ### Parameters * \`\` - The player receiving the item ## onEquip Called when the item is equipped or unequipped * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemHooks` ### Signature ```ts theme={null} onEquip: (player: RpgPlayer, equip: boolean) => void | Promise ``` ### Parameters * \`\` - true if equipping, false if unequipping ## onRemove Called when the item is removed from the inventory * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemHooks` ### Signature ```ts theme={null} onRemove: (player: RpgPlayer) => void | Promise ``` ### Parameters * \`\` - The player losing the item ## onUse Called when the item is successfully used * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemHooks` ### Signature ```ts theme={null} onUse: (player: RpgPlayer) => void | Promise ``` ### Parameters * \`\` - The player using the item ## onUseFailed Called when the item usage fails (e.g., chance roll failed) * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemHooks` ### Signature ```ts theme={null} onUseFailed: (player: RpgPlayer) => void | Promise ``` ### Parameters * \`\` - The player attempting to use the item ## paramsModifier Parameter modifiers * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemData` ### Signature ```ts theme={null} paramsModifier: Record ``` ## pdef Get the player's physical defense (sum of items equipped) Returns the total physical defense value from all equipped items on the player. * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `IItemManager` ### Signature ```ts theme={null} pdef: number ``` ### Returns Total physical defense value from equipped items ### Examples ```ts theme={null} console.log(player.pdef) // 80 (sum of all equipped armors physical defense) ``` ## price Item price * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemData` ### Signature ```ts theme={null} price: number ``` ## removeItem Deletes an item from inventory Decreases the quantity by `nb`. If the quantity falls to 0 or below, the item is removed from the inventory. The method returns `undefined` if the item is completely removed. The `onRemove()` method is called on the ItemClass when the item is removed. * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `method` * Defined in: `IItemManager` ### Signature ```ts theme={null} removeItem(itemClass: ItemClass | string, nb?: number): Item | undefined ``` ### Parameters * `itemClass`: `ItemClass | string` * `nb?`: `number` ### Returns Item instance or `undefined` if the item was completely removed ### Examples ```ts theme={null} import Potion from 'your-database/potion' try { player.removeItem(Potion, 5) } catch (err) { console.log(err) // { id: 'ITEM_NOT_INVENTORY', msg: '...' } } ``` ## removeStates States to remove when used * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `ItemData` ### Signature ```ts theme={null} removeStates: StateApplication[] ``` ## resolveEquipmentsSnapshot Resolve equipment snapshot entries into Item instances without side effects. * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `method` ### Signature ```ts theme={null} resolveEquipmentsSnapshot(snapshot: { equipments?: any[]; items?: any[] }, mapOverride?: any) ``` ### Parameters * `snapshot`: `{ equipments?: any[]; items?: any[] }` * `mapOverride?`: `any` ## resolveItemsSnapshot Resolve item snapshot entries into Item instances without side effects. * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `method` ### Signature ```ts theme={null} resolveItemsSnapshot(snapshot: { items?: any[] }, mapOverride?: any) ``` ### Parameters * `snapshot`: `{ items?: any[] }` * `mapOverride?`: `any` ## sdef Get the player's skill defense (sum of items equipped) Returns the total skill defense value from all equipped items on the player. * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `property` * Defined in: `IItemManager` ### Signature ```ts theme={null} sdef: number ``` ### Returns Total skill defense value from equipped items ### Examples ```ts theme={null} console.log(player.sdef) // 60 (sum of all equipped armors skill defense) ``` ## sellItem Sell an item and the player wins the amount of the item divided by 2 The player receives `(item.price / 2) * nbToSell` gold. The item is removed from the inventory. The `onRemove()` method is called on the ItemClass when the item is removed. * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `method` * Defined in: `IItemManager` ### Signature ```ts theme={null} sellItem(itemClass: ItemClass | string, nbToSell?: number): Item ``` ### Parameters * `itemClass`: `ItemClass | string` * `nbToSell?`: `number` ### Returns Item instance that was sold ### Examples ```ts theme={null} import Potion from 'your-database/potion' try { player.addItem(Potion) player.sellItem(Potion) } catch (err) { console.log(err) } ``` ## useItem Use an object. Applies effects and states. Removes the object from the inventory When an item is used: * Effects are applied to the player (HP/MP restoration, etc.) * States are applied/removed as defined in the item * The item is removed from inventory (consumed) If the item has a `hitRate` property (0-1), there's a chance the usage might fail. If usage fails, the item is still removed and `onUseFailed()` is called instead of `onUse()`. The `onUse()` method is called on the ItemClass if the use was successful. The `onUseFailed()` method is called on the ItemClass if the chance roll failed. The `onRemove()` method is called on the ItemClass when the item is removed. * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `method` * Defined in: `IItemManager` ### Signature ```ts theme={null} useItem(itemClass: ItemClass | string): Item ``` ### Parameters * `itemClass`: `ItemClass | string` ### Returns Item instance that was used ### Examples ```ts theme={null} import Potion from 'your-database/potion' try { player.addItem(Potion) player.useItem(Potion) } catch (err) { if (err.id === 'USE_CHANCE_ITEM_FAILED') { console.log('Item usage failed due to chance roll') } else { console.log(err) } } ``` ## WithItemManager Item Manager Mixin Provides comprehensive item management capabilities to any class. This mixin handles inventory management, item usage, equipment, buying/selling, and item effects. It manages the complete item system including restrictions, transactions, and equipment. * Source: `packages/server/src/Player/ItemManager.ts` * Kind: `function` ### Signature ```ts theme={null} WithItemManager(Base: TBase) ``` ### Parameters * `Base`: `TBase` ### Returns Extended class with item management methods ### Examples ```ts theme={null} class MyPlayer extends WithItemManager(BasePlayer) { constructor() { super(); // Item system is automatically initialized } } const player = new MyPlayer(); player.addItem('potion', 5); player.useItem('potion'); ``` # Move Commands Source: https://v5.rpgjs.dev/api/player/move-commands Movement APIs, movement strategies, and move route helpers. # Move Commands Movement APIs, movement strategies, and move route helpers. ## Members * [addMovement](#addmovement) * [addMovement](#addmovement) * [animationFixed](#animationfixed) * [applyIceMovement](#applyicemovement) * [applyIceMovement](#applyicemovement) * [breakRoutes](#breakroutes) * [clearAllPlayerStates](#clearallplayerstates) * [clearMovements](#clearmovements) * [clearPlayerState](#clearplayerstate) * [dash](#dash) * [dash](#dash) * [directionFixed](#directionfixed) * [followPath](#followpath) * [followPath](#followpath) * [frequency](#frequency) * [frequencyRatio](#frequencyratio) * [getActiveMovements](#getactivemovements) * [hasActiveMovements](#hasactivemovements) * [infiniteMoveRoute](#infinitemoveroute) * [knockback](#knockback) * [knockback](#knockback) * [moveRoutes](#moveroutes) * [moveTo](#moveto) * [moveTo](#moveto) * [onStuck](#onstuck) * [oscillate](#oscillate) * [oscillate](#oscillate) * [removeMovement](#removemovement) * [replayRoutes](#replayroutes) * [shootProjectile](#shootprojectile) * [shootProjectile](#shootprojectile) * [stopMoveTo](#stopmoveto) * [stuckThreshold](#stuckthreshold) * [stuckTimeout](#stucktimeout) * [through](#through) * [throughEvent](#throughevent) * [throughOtherPlayer](#throughotherplayer) * [WithMoveManager](#withmovemanager) ## addMovement Add a movement strategy to this entity Returns a Promise that resolves when the movement completes (when `isFinished()` returns true). If the strategy doesn't implement `isFinished()`, the Promise resolves immediately. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `WithMoveManagerClass` ### Signature ```ts theme={null} addMovement(strategy: MovementStrategy, options?: MovementOptions): Promise ``` ### Parameters * `strategy`: `MovementStrategy` * `options?`: `MovementOptions` ### Returns Promise that resolves when the movement completes ### Examples ```ts theme={null} // Fire and forget player.addMovement(new LinearMove({ x: 1, y: 0 }, 200)); // Wait for completion await player.addMovement(new Dash(10, { x: 1, y: 0 }, 200)); console.log('Dash completed!'); // With callbacks await player.addMovement(new Knockback({ x: -1, y: 0 }, 5, 300), { onStart: () => console.log('Knockback started'), onComplete: () => console.log('Knockback completed') }); ``` ## addMovement Add a custom movement strategy to this entity Returns a Promise that resolves when the movement completes. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} addMovement(strategy: MovementStrategy, options?: MovementOptions): Promise ``` ### Parameters * `strategy`: `MovementStrategy` * `options?`: `MovementOptions` ### Returns Promise that resolves when the movement completes ## animationFixed Whether animation changes are locked (prevents automatic animation changes) * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `property` * Defined in: `IMoveManager` ### Signature ```ts theme={null} animationFixed: boolean ``` ## applyIceMovement Apply ice movement physics Simulates slippery surface physics where the entity accelerates gradually and has difficulty stopping. The maximum speed is based on the player's base speed multiplied by a speed factor. With default speed=4 and factor=1: maxSpeed = 4 (same as original default) * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `WithMoveManagerClass` ### Signature ```ts theme={null} applyIceMovement(direction: { x: number, y: number }, speedFactor?: number): void ``` ### Parameters * `direction`: `{ x: number, y: number }` * `speedFactor?`: `number` ### Examples ```ts theme={null} // Normal ice physics player.applyIceMovement({ x: 1, y: 0 }); // Fast ice sliding player.applyIceMovement({ x: 0, y: 1 }, 1.5); ``` ## applyIceMovement Apply ice movement physics Max speed is calculated from the player's base speed multiplied by the speedFactor. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} applyIceMovement(direction: { x: number, y: number }, speedFactor?: number): void ``` ### Parameters * `direction`: `{ x: number, y: number }` * `speedFactor?`: `number` ## breakRoutes Stop an infinite movement * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} breakRoutes(force?: boolean): void ``` ### Parameters * `force?`: `boolean` ## clearAllPlayerStates Clears all player movement states Useful for cleanup during server shutdown or when resetting game state. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `MoveList` ### Signature ```ts theme={null} clearAllPlayerStates(): void ``` ### Examples ```ts theme={null} // Clear all states on server shutdown Move.clearAllPlayerStates(); ``` ## clearMovements Remove all active movement strategies from this entity * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} clearMovements(): void ``` ## clearPlayerState Clears the movement state for a specific player Should be called when a player changes map or is destroyed to prevent memory leaks and stale stuck detection data. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `MoveList` ### Signature ```ts theme={null} clearPlayerState(playerId: string): void ``` ### Parameters * `playerId`: `string` ### Examples ```ts theme={null} // Clear state when player leaves map Move.clearPlayerState(player.id); ``` ## dash Perform a dash movement in the specified direction Creates a burst of velocity for a fixed duration. The total speed is calculated by adding the player's base speed (`this.speed`) to the additional dash speed. This ensures faster players also dash faster proportionally. With default speed=4 and additionalSpeed=4: total = 8 (same as original default) * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `WithMoveManagerClass` ### Signature ```ts theme={null} dash(direction: { x: number, y: number }, additionalSpeed?: number, duration?: number, options?: MovementOptions): Promise ``` ### Parameters * `direction`: `{ x: number, y: number }` * `additionalSpeed?`: `number` * `duration?`: `number` * `options?`: `MovementOptions` ### Returns Promise that resolves when the dash completes ### Examples ```ts theme={null} // Dash to the right and wait for completion await player.dash({ x: 1, y: 0 }); // Powerful dash with callbacks await player.dash({ x: 0, y: -1 }, 12, 300, { onStart: () => console.log('Dash started!'), onComplete: () => console.log('Dash finished!') }); ``` ## dash Perform a dash movement in the specified direction The total speed is calculated by adding the player's base speed to the additional speed. Returns a Promise that resolves when the dash completes. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} dash(direction: { x: number, y: number }, additionalSpeed?: number, duration?: number, options?: MovementOptions): Promise ``` ### Parameters * `direction`: `{ x: number, y: number }` * `additionalSpeed?`: `number` * `duration?`: `number` * `options?`: `MovementOptions` ### Returns Promise that resolves when the dash completes ## directionFixed Whether direction changes are locked (prevents automatic direction changes) * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `property` * Defined in: `IMoveManager` ### Signature ```ts theme={null} directionFixed: boolean ``` ## followPath Follow a sequence of waypoints Makes the entity move through a list of positions at a speed calculated from the player's base speed. The `speedMultiplier` allows adjusting the travel speed relative to the player's normal movement speed. With default speed=4 and multiplier=0.5: speed = 2 (same as original default) * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `WithMoveManagerClass` ### Signature ```ts theme={null} followPath(waypoints: Array<{ x: number, y: number }>, speedMultiplier?: number, loop?: boolean): void ``` ### Parameters * `waypoints`: `Array<{ x: number, y: number }>` * `speedMultiplier?`: `number` * `loop?`: `boolean` ### Examples ```ts theme={null} // Follow a patrol path at normal speed const patrol = [ { x: 100, y: 100 }, { x: 200, y: 100 }, { x: 200, y: 200 } ]; player.followPath(patrol, 1, true); // Loop at full speed // Slow walk through waypoints player.followPath(waypoints, 0.25, false); ``` ## followPath Follow a sequence of waypoints Speed is calculated from the player's base speed multiplied by the speedMultiplier. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} followPath(waypoints: Array<{ x: number, y: number }>, speedMultiplier?: number, loop?: boolean): void ``` ### Parameters * `waypoints`: `Array<{ x: number, y: number }>` * `speedMultiplier?`: `number` * `loop?`: `boolean` ## frequency Frequency for movement timing (milliseconds between movements) * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `property` * Defined in: `IMoveManager` ### Signature ```ts theme={null} frequency: number ``` ## frequencyRatio Multiplier applied to the player's frequency delay between route segments. The default keeps legacy route timing. Lower values are useful for generated routes that need smoother visual motion, such as Studio random movement. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `property` * Defined in: `MoveRoutesOptions` ### Signature ```ts theme={null} frequencyRatio: number ``` ## getActiveMovements Get all active movement strategies for this entity * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} getActiveMovements(): MovementStrategy[] ``` ### Returns Array of active movement strategies ## hasActiveMovements Check if this entity has any active movement strategies * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} hasActiveMovements(): boolean ``` ### Returns True if entity has active movements ## infiniteMoveRoute Give a path that repeats itself in a loop to a character * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} infiniteMoveRoute(routes: Routes, options?: MoveRoutesOptions): void ``` ### Parameters * `routes`: `Routes` * `options?`: `MoveRoutesOptions` ## knockback Apply knockback effect in the specified direction Pushes the entity with an initial force that decays over time. Returns a Promise that resolves when the knockback completes **or is cancelled**. ## Design notes * The underlying physics `MovementManager` can cancel strategies via `remove()`, `clear()`, or `stopMovement()` **without resolving the Promise** returned by `add()`. * For this reason, this method considers the knockback finished when either: * the `add()` promise resolves (normal completion), or * the strategy is no longer present in the active movements list (cancellation). * When multiple knockbacks overlap, `directionFixed` and `animationFixed` are restored only after **all** knockbacks have finished (including cancellations). * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `WithMoveManagerClass` ### Signature ```ts theme={null} knockback(direction: { x: number, y: number }, force?: number, duration?: number, options?: MovementOptions): Promise ``` ### Parameters * `direction`: `{ x: number, y: number }` * `force?`: `number` * `duration?`: `number` * `options?`: `MovementOptions` ### Returns Promise that resolves when the knockback completes or is cancelled ### Examples ```ts theme={null} // Simple knockback (await is optional) await player.knockback({ x: 1, y: 0 }, 5, 300); // Overlapping knockbacks: flags are restored only after the last one ends player.knockback({ x: -1, y: 0 }, 5, 300); player.knockback({ x: 0, y: 1 }, 3, 200); // Cancellation (e.g. map change) will still restore fixed flags // even if the underlying movement strategy promise is never resolved. ``` ## knockback Apply knockback effect in the specified direction The force is scaled by the player's base speed for consistent behavior. Returns a Promise that resolves when the knockback completes. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} knockback(direction: { x: number, y: number }, force?: number, duration?: number, options?: MovementOptions): Promise ``` ### Parameters * `direction`: `{ x: number, y: number }` * `force?`: `number` * `duration?`: `number` * `options?`: `MovementOptions` ### Returns Promise that resolves when the knockback completes ## moveRoutes Give an itinerary to follow using movement strategies * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} moveRoutes(routes: Routes, options?: MoveRoutesOptions): Promise ``` ### Parameters * `routes`: `Routes` * `options?`: `MoveRoutesOptions` ### Returns Promise that resolves when all routes are completed ## moveTo Move toward a target player or position using local obstacle avoidance Uses the `SeekAvoid` strategy to navigate toward the target while avoiding blocking hitboxes and events locally. It does not compute a full path. The movement speed is based on the player's current `speed` and `frequency` settings, scaled appropriately. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `WithMoveManagerClass` ### Signature ```ts theme={null} moveTo(target: RpgCommonPlayer | { x: number, y: number }): void ``` ### Parameters * `target`: `RpgCommonPlayer | { x: number, y: number }` ### Examples ```ts theme={null} // Move toward another player player.moveTo(otherPlayer); // Move toward a specific position player.moveTo({ x: 200, y: 150 }); ``` ## moveTo Move toward a target player or position using local obstacle avoidance * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} moveTo(target: RpgCommonPlayer | { x: number, y: number }): void ``` ### Parameters * `target`: `RpgCommonPlayer | { x: number, y: number }` ## onStuck Callback function called when the player gets stuck (cannot move towards target) This callback is triggered when the player is trying to move but cannot make progress towards the target position, typically due to obstacles or collisions. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `property` * Defined in: `MoveRoutesOptions` ### Signature ```ts theme={null} onStuck: (player: RpgPlayer, target: { x: number; y: number }, currentPosition: { x: number; y: number }) => boolean | void ``` ### Parameters * \`\` - The current position of the player ### Returns If true, the route will continue; if false, the route will be cancelled ### Examples ```ts theme={null} await player.moveRoutes([Move.right()], { onStuck: (player, target, currentPos) => { console.log('Player is stuck!'); return false; // Cancel the route } }); ``` ## oscillate Apply oscillating movement pattern Creates a back-and-forth movement along the specified axis. The movement oscillates sinusoidally between -amplitude and +amplitude from the starting position. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `WithMoveManagerClass` ### Signature ```ts theme={null} oscillate(direction: { x: number, y: number }, amplitude?: number, period?: number): void ``` ### Parameters * `direction`: `{ x: number, y: number }` * `amplitude?`: `number` * `period?`: `number` ### Examples ```ts theme={null} // Horizontal oscillation player.oscillate({ x: 1, y: 0 }, 100, 3000); // Diagonal bobbing motion player.oscillate({ x: 1, y: 1 }, 30, 1000); ``` ## oscillate Apply oscillating movement pattern * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} oscillate(direction: { x: number, y: number }, amplitude?: number, period?: number): void ``` ### Parameters * `direction`: `{ x: number, y: number }` * `amplitude?`: `number` * `period?`: `number` ## removeMovement Remove a specific movement strategy from this entity * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} removeMovement(strategy: MovementStrategy): boolean ``` ### Parameters * `strategy`: `MovementStrategy` ### Returns True if the strategy was found and removed ## replayRoutes Replay an infinite movement * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} replayRoutes(): void ``` ## shootProjectile Shoot a projectile in the specified direction Creates a projectile with ballistic trajectory. The speed is calculated from the player's base speed multiplied by a speed factor. With default speed=4 and factor=50: speed = 200 (same as original default) * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `WithMoveManagerClass` ### Signature ```ts theme={null} shootProjectile(type: ProjectileType, direction: { x: number, y: number }, speedFactor?: number): void ``` ### Parameters * `type`: `ProjectileType` * `direction`: `{ x: number, y: number }` * `speedFactor?`: `number` ### Examples ```ts theme={null} // Straight projectile player.shootProjectile(ProjectileType.Straight, { x: 1, y: 0 }); // Fast arc projectile player.shootProjectile(ProjectileType.Arc, { x: 1, y: -0.5 }, 75); ``` ## shootProjectile Shoot a projectile in the specified direction Speed is calculated from the player's base speed multiplied by the speedFactor. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} shootProjectile(type: ProjectileType, direction: { x: number, y: number }, speedFactor?: number): void ``` ### Parameters * `type`: `ProjectileType` * `direction`: `{ x: number, y: number }` * `speedFactor?`: `number` ## stopMoveTo Stop the current moveTo behavior * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `method` * Defined in: `IMoveManager` ### Signature ```ts theme={null} stopMoveTo(): void ``` ## stuckThreshold Minimum distance change in pixels to consider movement progress (default: 1 pixel) If the player moves less than this distance over the stuckTimeout period, they are considered stuck. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `property` * Defined in: `MoveRoutesOptions` ### Signature ```ts theme={null} stuckThreshold: number ``` ## stuckTimeout Time in milliseconds to wait before considering the player stuck (default: 500ms) The player must be unable to make progress for this duration before onStuck is called. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `property` * Defined in: `MoveRoutesOptions` ### Signature ```ts theme={null} stuckTimeout: number ``` ## through Whether the player goes through all characters (players and events) When `true`, the player can walk through all character entities (both players and events) without collision. Walls and obstacles still block movement. This takes precedence over `throughOtherPlayer` and `throughEvent`. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `property` * Defined in: `IMoveManager` ### Signature ```ts theme={null} through: boolean ``` ### Default ```ts theme={null} false ``` ### Examples ```ts theme={null} // Enable ghost mode - pass through all characters player.through = true; // Disable ghost mode player.through = false; ``` ## throughEvent Whether the player passes through events (NPCs, objects) When `true`, the player can walk through event entities without collision. This is useful for NPCs that shouldn't block player movement. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `property` * Defined in: `IMoveManager` ### Signature ```ts theme={null} throughEvent: boolean ``` ### Default ```ts theme={null} false ``` ### Examples ```ts theme={null} // Allow passing through events player.throughEvent = true; // Block passage through events player.throughEvent = false; ``` ## throughOtherPlayer Whether the player passes through other players When `true`, the player can walk through other player entities without collision. This is useful for busy areas where players shouldn't block each other. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `property` * Defined in: `IMoveManager` ### Signature ```ts theme={null} throughOtherPlayer: boolean ``` ### Default ```ts theme={null} true ``` ### Examples ```ts theme={null} // Disable player-to-player collision player.throughOtherPlayer = true; // Enable player-to-player collision player.throughOtherPlayer = false; ``` ## WithMoveManager Move Manager Mixin Provides comprehensive movement management capabilities to any class. This mixin handles various types of movement including target seeking, physics-based movement, route following, and advanced movement strategies like dashing, knockback, and projectile movement. * Source: `packages/server/src/Player/MoveManager.ts` * Kind: `function` ### Signature ```ts theme={null} WithMoveManager(Base: TBase) ``` ### Parameters * `Base`: `TBase` ### Returns Extended class with movement management methods ### Examples ```ts theme={null} class MyPlayer extends WithMoveManager(BasePlayer) { constructor() { super(); this.frequency = Frequency.High; } } const player = new MyPlayer(); player.moveTo({ x: 100, y: 100 }); player.dash({ x: 1, y: 0 }, 8, 200); ``` # Parameter Commands Source: https://v5.rpgjs.dev/api/player/parameter-commands Level, experience, HP, SP, and parameter management for players. # Parameter Commands Level, experience, HP, SP, and parameter management for players. ## Members * [Add custom parameters](#add-custom-parameters) * [Add custom parameters](#add-custom-parameters) * [All Recovery](#all-recovery) * [All Recovery](#all-recovery) * [Change Experience](#change-experience) * [Change Experience](#change-experience) * [Change Experience Curve](#change-experience-curve) * [Change HP](#change-hp) * [Change HP](#change-hp) * [Change Level](#change-level) * [Change Level](#change-level) * [Change SP](#change-sp) * [Change SP](#change-sp) * [Experience for next level ?](#experience-for-next-level) * [Experience for next level ?](#experience-for-next-level) * [Get Param Value](#get-param-value) * [Get Param Value](#get-param-value) * [getParamValue](#getparamvalue) * [parameters](#parameters) * [Recovery HP and/or SP](#recovery-hp-and-or-sp) * [Recovery HP and/or SP](#recovery-hp-and-or-sp) * [Set final level](#set-final-level) * [Set final level](#set-final-level) * [Set initial level](#set-initial-level) * [Set initial level](#set-initial-level) * [Set Parameters Modifier](#set-parameters-modifier) * [Set Parameters Modifier](#set-parameters-modifier) * [setParameter](#setparameter) * [WithParameterManager](#withparametermanager) ## Add custom parameters Give a new parameter. Give a start value and an end value. The start value will be set to the level set at `player.initialLevel` and the end value will be linked to the level set at `player.finalLevel`. ```ts theme={null} const SPEED = 'speed' player.addParameter(SPEED, { start: 10, end: 100 }) player.param[SPEED] // 10 player.level += 5 player.param[SPEED] // 14 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `method` * Member of: `ParameterManager` * Defined in: `IParameterManager` ### Signature ```ts theme={null} player.addParameter(name,curve) ``` ### Parameters * `name`: `string` * `curve`: `{ start: number, end: number }` ## Add custom parameters Give a new parameter. Give a start value and an end value. The start value will be set to the level set at `player.initialLevel` and the end value will be linked to the level set at `player.finalLevel`. ```ts theme={null} const SPEED = 'speed' player.addParameter(SPEED, { start: 10, end: 100 }) player.param[SPEED] // 10 player.level += 5 player.param[SPEED] // 14 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `method` * Member of: `ParameterManager` ### Signature ```ts theme={null} player.addParameter(name,curve) ``` ### Parameters * `name`: `string` * `value`: `ParameterValue` ## All Recovery restores all HP and SP ```ts theme={null} import { Presets } from '@rpgjs/server' const { MAXHP, MAXSP } = Presets console.log(player.param[MAXHP], player.param[MAXSP]) // 800, 230 player.hp = 100 player.sp = 0 player.allRecovery() console.log(player.hp, player.sp) // 800, 230 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `method` * Member of: `ParameterManager` * Defined in: `IParameterManager` ### Signature ```ts theme={null} player.allRecovery() ``` ## All Recovery restores all HP and SP ```ts theme={null} import { Presets } from '@rpgjs/server' const { MAXHP, MAXSP } = Presets console.log(player.param[MAXHP], player.param[MAXSP]) // 800, 230 player.hp = 100 player.sp = 0 player.allRecovery() console.log(player.hp, player.sp) // 800, 230 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `method` * Member of: `ParameterManager` ### Signature ```ts theme={null} player.allRecovery() ``` ## Change Experience Changing the player's experience. ```ts theme={null} player.exp += 100 ``` Levels are based on the experience curve. ```ts theme={null} console.log(player.level) // 1 console.log(player.expForNextlevel) // 150 player.exp += 160 console.log(player.level) // 2 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `property` * Member of: `ParameterManager` * Defined in: `IParameterManager` ### Signature ```ts theme={null} {number} player.exp ``` ### Default ```ts theme={null} 0 ``` ## Change Experience Changing the player's experience. ```ts theme={null} player.exp += 100 ``` Levels are based on the experience curve. ```ts theme={null} console.log(player.level) // 1 console.log(player.expForNextlevel) // 150 player.exp += 160 console.log(player.level) // 2 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `setter` * Member of: `ParameterManager` ### Signature ```ts theme={null} {number} player.exp ``` ### Default ```ts theme={null} 0 ``` ## Change Experience Curve With Object-based syntax, you can use following options: * `basis: number` * `extra: number` * `accelerationA: number` * `accelerationB: number` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `property` * Member of: `ParameterManager` * Defined in: `IParameterManager` ### Signature ```ts theme={null} {object} player.expCurve ``` ### Default ````ts theme={null} ```ts { basis: 30, extra: 20, accelerationA: 30, accelerationB: 30 } ```` ```` ## Change HP Changes the health points - Cannot exceed the MaxHP parameter - Cannot have a negative value - If the value is 0, a hook named `onDead()` is called in the RpgPlayer class. ```ts player.hp = 100 ```` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `property` * Member of: `ParameterManager` * Defined in: `IParameterManager` ### Signature ```ts theme={null} {number} player.hp ``` ### Default ```ts theme={null} MaxHPValue ``` ## Change HP Changes the health points * Cannot exceed the MaxHP parameter * Cannot have a negative value * If the value is 0, a hook named `onDead()` is called in the RpgPlayer class. ```ts theme={null} player.hp = 100 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `setter` * Member of: `ParameterManager` ### Signature ```ts theme={null} {number} player.hp ``` ### Default ```ts theme={null} MaxHPValue ``` ## Change Level Changing the player's level. ```ts theme={null} player.level += 1 ``` The level will be between the initial level given by the `initialLevel` and final level given by `finalLevel` ```ts theme={null} player.finalLevel = 50 player.level = 60 console.log(player.level) // 50 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `property` * Member of: `ParameterManager` * Defined in: `IParameterManager` ### Signature ```ts theme={null} {number} player.level ``` ### Default ```ts theme={null} 1 ``` ## Change Level Changing the player's level. ```ts theme={null} player.level += 1 ``` The level will be between the initial level given by the `initialLevel` and final level given by `finalLevel` ```ts theme={null} player.finalLevel = 50 player.level = 60 console.log(player.level) // 50 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `setter` * Member of: `ParameterManager` ### Signature ```ts theme={null} {number} player.level ``` ### Default ```ts theme={null} 1 ``` ## Change SP Changes the skill points * Cannot exceed the MaxSP parameter * Cannot have a negative value ```ts theme={null} player.sp = 200 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `property` * Member of: `ParameterManager` * Defined in: `IParameterManager` ### Signature ```ts theme={null} {number} player.sp ``` ### Default ```ts theme={null} MaxSPValue ``` ## Change SP Changes the skill points * Cannot exceed the MaxSP parameter * Cannot have a negative value ```ts theme={null} player.sp = 200 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `setter` * Member of: `ParameterManager` ### Signature ```ts theme={null} {number} player.sp ``` ### Default ```ts theme={null} MaxSPValue ``` ## Experience for next level ? ```ts theme={null} console.log(player.expForNextlevel) // 150 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `property` * Member of: `ParameterManager` * Defined in: `IParameterManager` ### Signature ```ts theme={null} {number} player.expForNextlevel ``` ## Experience for next level ? ```ts theme={null} console.log(player.expForNextlevel) // 150 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `getter` * Member of: `ParameterManager` ### Signature ```ts theme={null} {number} player.expForNextlevel ``` ## Get Param Value Read the value of a parameter. Put the name of the parameter. ```ts theme={null} import { Presets } from '@rpgjs/server' const { MAXHP } = Presets console.log(player.param[MAXHP]) ``` > Possible to use the `player.getParamValue(name)` method instead * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `property` * Member of: `ParameterManager` * Defined in: `IParameterManager` ### Signature ```ts theme={null} {object} player.param ``` ## Get Param Value Read the value of a parameter. Put the name of the parameter. ```ts theme={null} import { Presets } from '@rpgjs/server' const { MAXHP } = Presets console.log(player.param[MAXHP]) ``` > Possible to use the `player.getParamValue(name)` method instead * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `getter` * Member of: `ParameterManager` ### Signature ```ts theme={null} {object} player.param ``` ## getParamValue Get the value of a specific parameter by name * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `method` * Defined in: `IParameterManager` ### Signature ```ts theme={null} getParamValue(name: string): number ``` ### Parameters * `name`: `string` ### Returns The calculated parameter value ### Examples ```ts theme={null} import { Presets } from '@rpgjs/server' const { MAXHP } = Presets // Preferred way (reactive) const maxHp = player.param[MAXHP]; // Legacy way (still works) const maxHp = player.getParamValue(MAXHP); ``` ## parameters Get or set the parameters object * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `property` * Member of: `ParameterManager` * Defined in: `IParameterManager` ### Signature ```ts theme={null} {object} parameters ``` ## Recovery HP and/or SP Gives back in percentage of health points to skill points ```ts theme={null} import { Presets } from '@rpgjs/server' const { MAXHP } = Presets console.log(player.param[MAXHP]) // 800 player.hp = 100 player.recovery({ hp: 0.5 }) // = 800 * 0.5 console.log(player.hp) // 400 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `method` * Member of: `ParameterManager` * Defined in: `IParameterManager` ### Signature ```ts theme={null} player.recovery(params) ``` ### Parameters * `params`: `{ hp?: number, sp?: number }` ## Recovery HP and/or SP Gives back in percentage of health points to skill points ```ts theme={null} import { Presets } from '@rpgjs/server' const { MAXHP } = Presets console.log(player.param[MAXHP]) // 800 player.hp = 100 player.recovery({ hp: 0.5 }) // = 800 * 0.5 console.log(player.hp) // 400 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `method` * Member of: `ParameterManager` ### Signature ```ts theme={null} player.recovery(params) ``` ### Parameters * `{ hp, sp }`: `{ hp?: number, sp?: number }` ## Set final level ```ts theme={null} player.finalLevel = 50 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `property` * Member of: `ParameterManager` * Defined in: `IParameterManager` ### Signature ```ts theme={null} {number} player.finalLevel ``` ### Default ```ts theme={null} 99 ``` ## Set final level ```ts theme={null} player.finalLevel = 50 ``` The server retains this curve bound in saves and room transfers in both RPG and MMORPG modes. * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `getter` * Member of: `ParameterManager` ### Signature ```ts theme={null} {number} player.finalLevel ``` ### Default ```ts theme={null} 99 ``` ## Set initial level ```ts theme={null} player.initialLevel = 5 ``` * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `property` * Member of: `ParameterManager` * Defined in: `IParameterManager` ### Signature ```ts theme={null} {number} player.initialLevel ``` ### Default ```ts theme={null} 1 ``` ## Set initial level ```ts theme={null} player.initialLevel = 5 ``` The server retains this curve bound in saves and room transfers in both RPG and MMORPG modes. * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `getter` * Member of: `ParameterManager` ### Signature ```ts theme={null} {number} player.initialLevel ``` ### Default ```ts theme={null} 1 ``` ## Set Parameters Modifier Direct parameter modifiers (reactive signal) > It is important that these parameters have been created beforehand with the `addParameter()` method. > By default, the following settings have been created: * maxhp * maxsp * str * int * dex * agi **Object Key** The key of the object is the name of the parameter > The good practice is to retrieve the name coming from a constant **Object Value** The value of the key is an object containing: ``` { value: number, rate: number } ``` * value: Adds a number to the parameter * rate: Adds a rate to the parameter > Note that you can put both (value and rate) This property uses reactive signals - changes automatically trigger parameter recalculation. The final parameter values in `param` include aggregated modifiers from equipment, states, etc. * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `property` * Member of: `ParameterManager` * Defined in: `IParameterManager` ### Signature ```ts theme={null} {Object} [paramsModifier] ``` ### Examples ```ts theme={null} import { Presets } from '@rpgjs/server' const { MAXHP } = Presets // Set direct modifiers (reactive) player.paramsModifier = { [MAXHP]: { value: 100 } } // Parameters automatically recalculate console.log(player.param[MAXHP]); // Updated value ``` ## Set Parameters Modifier Changes the values of some parameters > It is important that these parameters have been created beforehand with the `addParameter()` method. > By default, the following settings have been created: * maxhp * maxsp * str * int * dex * agi **Object Key** The key of the object is the name of the parameter > The good practice is to retrieve the name coming from a constant **Object Value** The value of the key is an object containing: ``` { value: number, rate: number } ``` * value: Adds a number to the parameter * rate: Adds a rate to the parameter > Note that you can put both (value and rate) In the case of a state or the equipment of a weapon or armor, the parameters will be changed but if the state disappears or the armor/weapon is de-equipped, then the parameters will return to the initial state. * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `setter` * Member of: `ParameterManager` ### Signature ```ts theme={null} {Object} [paramsModifier] ``` ### Examples ````ts theme={null} ```ts import { Presets } from '@rpgjs/server' const { MAXHP } = Presets player.paramsModifier = { [MAXHP]: { value: 100 } } ```` 1. Player has 741 MaxHp 2. After changing the parameter, he will have 841 MaxHp ```` ## setParameter Set a parameter with either a fixed value or a level curve A numeric value is stored as a fixed parameter where `start === end`. - Source: `packages/server/src/Player/ParameterManager.ts` - Kind: `method` - Defined in: `IParameterManager` ### Signature ```ts setParameter(name: string, value: ParameterValue): void ```` ### Parameters * `name`: `string` * `value`: `ParameterValue` ## WithParameterManager Parameter Manager Mixin with Reactive Signals Provides comprehensive parameter management through RPGJS reactive gameplay signals. This mixin handles health points (HP), skill points (SP), experience and level progression, custom parameters, and parameter modifiers with automatic reactivity. **Key Features:** * ✨ **Reactive Parameters**: All parameters automatically recalculate when level or modifiers change * 🚀 **Performance Optimized**: Uses computed signals to avoid unnecessary recalculations * 🔄 **Real-time Updates**: Changes propagate automatically throughout the system * 🎯 **Type Safe**: Full TypeScript support with proper type inference * Source: `packages/server/src/Player/ParameterManager.ts` * Kind: `function` ### Signature ```ts theme={null} WithParameterManager(Base: TBase) ``` ### Parameters * `Base`: `TBase` ### Returns Extended class with reactive parameter management methods ### Examples ```ts theme={null} class MyPlayer extends WithParameterManager(BasePlayer) { constructor() { super(); // Add custom parameters this.addParameter('strength', { start: 10, end: 100 }); this.addParameter('magic', { start: 5, end: 80 }); } } const player = new MyPlayer(); // Reactive parameter updates player.level = 5; console.log(player.param.strength); // Automatically calculated for level 5 // Reactive modifiers player.paramsModifier = { [MAXHP]: { value: 100, rate: 1.2 } }; console.log(player.param[MAXHP]); // Automatically includes modifiers ``` # Skill Commands Source: https://v5.rpgjs.dev/api/player/skill-commands Learn, forget, and use skills from the server-side player API. # Skill Commands Learn, forget, and use skills from the server-side player API. ## Members * [\_type](#type) * [coefficient](#coefficient) * [createSkillInstance](#createskillinstance) * [description](#description) * [forgetSkill](#forgetskill) * [forgetSkill](#forgetskill) * [getSkill](#getskill) * [getSkill](#getskill) * [hitRate](#hitrate) * [id](#id) * [learnSkill](#learnskill) * [learnSkill](#learnskill) * [name](#name) * [onForget](#onforget) * [onLearn](#onlearn) * [onUse](#onuse) * [onUseFailed](#onusefailed) * [power](#power) * [resolveSkillsSnapshot](#resolveskillssnapshot) * [spCost](#spcost) * [useSkill](#useskill) * [useSkill](#useskill) * [WithSkillManager](#withskillmanager) ## \_type Type marker for database * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `property` * Defined in: `SkillObject` ### Signature ```ts theme={null} _type: 'skill' ``` ## coefficient Coefficient multipliers for damage calculation * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `property` * Defined in: `SkillObject` ### Signature ```ts theme={null} coefficient: Record ``` ## createSkillInstance Create a skill instance without learning side effects. * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `method` ### Signature ```ts theme={null} createSkillInstance(skillInput: SkillClass | SkillObject | string) ``` ### Parameters * `skillInput`: `SkillClass | SkillObject | string` ## description Description of the skill * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `property` * Defined in: `SkillObject` ### Signature ```ts theme={null} description: string ``` ## forgetSkill Forget a learned skill Removes a skill from the player's skill list. * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `method` ### Signature ```ts theme={null} forgetSkill(skillInput: SkillClass | SkillObject | string, options?: SkillChangeOptions): SkillData ``` ### Parameters * `skillInput`: `SkillClass | SkillObject | string` * `options?`: `SkillChangeOptions` ### Returns The forgotten skill data ### Examples ```ts theme={null} player.forgetSkill('fire'); // or player.forgetSkill(FireSkill); ``` ## forgetSkill Forget a skill * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `method` * Defined in: `ISkillManager` ### Signature ```ts theme={null} forgetSkill(skillInput: SkillClass | SkillObject | string, options?: SkillChangeOptions): SkillData ``` ### Parameters * `skillInput`: `SkillClass | SkillObject | string` * `options?`: `SkillChangeOptions` ### Returns The forgotten skill data ## getSkill Retrieves a learned skill Searches the player's learned skills by ID, class, or object. * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `method` ### Signature ```ts theme={null} getSkill(skillInput: SkillClass | SkillObject | string): Skill | null ``` ### Parameters * `skillInput`: `SkillClass | SkillObject | string` ### Returns The skill data if found, null otherwise ### Examples ```ts theme={null} const skill = player.getSkill('fire'); if (skill) { console.log(`Fire skill costs ${skill.spCost} SP`); } ``` ## getSkill Retrieves a learned skill. Returns null if not found * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `method` * Defined in: `ISkillManager` ### Signature ```ts theme={null} getSkill(skillInput: SkillClass | SkillObject | string): Skill | null ``` ### Parameters * `skillInput`: `SkillClass | SkillObject | string` ### Returns The skill data or null ## hitRate Hit rate (0-1) - probability of successful skill usage * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `property` * Defined in: `SkillObject` ### Signature ```ts theme={null} hitRate: number ``` ### Default ```ts theme={null} 1 ``` ## id Unique identifier for the skill If not provided, one will be auto-generated * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `property` * Defined in: `SkillObject` ### Signature ```ts theme={null} id: string ``` ## learnSkill Learn a new skill Adds a skill to the player's skill list. Supports three input formats: * **String ID**: Retrieves the skill from the database * **Class**: Creates an instance and adds to database if needed * **Object**: Uses directly and adds to database if needed * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `method` ### Signature ```ts theme={null} learnSkill(skillInput: SkillClass | SkillObject | string, options?: SkillChangeOptions): SkillObject ``` ### Parameters * `skillInput`: `SkillClass | SkillObject | string` * `options?`: `SkillChangeOptions` ### Returns The learned skill data ### Examples ```ts theme={null} // From database player.learnSkill('fire'); // From class player.learnSkill(FireSkill); // From object player.learnSkill({ id: 'custom-skill', name: 'Custom Skill', spCost: 20, onLearn(player) { console.log('Learned custom skill!'); } }); ``` ## learnSkill Learn a skill Supports three input formats: * String ID: Retrieves from database * Class: Creates instance and adds to database * Object: Uses directly and adds to database * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `method` * Defined in: `ISkillManager` ### Signature ```ts theme={null} learnSkill(skillInput: SkillClass | SkillObject | string, options?: SkillChangeOptions): SkillObject ``` ### Parameters * `skillInput`: `SkillClass | SkillObject | string` * `options?`: `SkillChangeOptions` ### Returns The learned skill data ## name Display name of the skill * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `property` * Defined in: `SkillObject` ### Signature ```ts theme={null} name: string ``` ## onForget Called when the skill is forgotten * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `property` * Defined in: `SkillHooks` ### Signature ```ts theme={null} onForget: (player: RpgPlayer) => void | Promise ``` ### Parameters * \`\` - The player forgetting the skill ## onLearn Called when the skill is learned by the player * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `property` * Defined in: `SkillHooks` ### Signature ```ts theme={null} onLearn: (player: RpgPlayer) => void | Promise ``` ### Parameters * \`\` - The player learning the skill ## onUse Called when the skill is successfully used * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `property` * Defined in: `SkillHooks` ### Signature ```ts theme={null} onUse: (player: RpgPlayer, target?: RpgPlayer | RpgPlayer[]) => void | Promise ``` ### Parameters * \`\` - The target player(s) if any ## onUseFailed Called when the skill usage fails (e.g., chance roll failed) * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `property` * Defined in: `SkillHooks` ### Signature ```ts theme={null} onUseFailed: (player: RpgPlayer, target?: RpgPlayer | RpgPlayer[]) => void | Promise ``` ### Parameters * \`\` - The intended target player(s) if any ## power Base power of the skill for damage calculation * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `property` * Defined in: `SkillObject` ### Signature ```ts theme={null} power: number ``` ## resolveSkillsSnapshot Resolve skill snapshot entries into Skill instances without side effects. * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `method` ### Signature ```ts theme={null} resolveSkillsSnapshot(snapshot: { skills?: any[] }, mapOverride?: any) ``` ### Parameters * `snapshot`: `{ skills?: any[] }` * `mapOverride?`: `any` ## spCost SP (Skill Points) cost to use the skill * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `property` * Defined in: `SkillObject` ### Signature ```ts theme={null} spCost: number ``` ### Default ```ts theme={null} 0 ``` ## useSkill Use a learned skill Executes a skill, consuming SP and applying effects to targets. The skill must be learned and the player must have enough SP. * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `method` ### Signature ```ts theme={null} useSkill(skillInput: SkillClass | SkillObject | string, otherPlayer?: RpgPlayer | RpgPlayer[]): SkillData ``` ### Parameters * `skillInput`: `SkillClass | SkillObject | string` * `otherPlayer?`: `RpgPlayer | RpgPlayer[]` ### Returns The used skill data ### Examples ```ts theme={null} // Use skill without target player.useSkill('fire'); // Use skill on a target player.useSkill('fire', enemy); // Use skill on multiple targets player.useSkill('fire', [enemy1, enemy2]); ``` ## useSkill Use a skill * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `method` * Defined in: `ISkillManager` ### Signature ```ts theme={null} useSkill(skillInput: SkillClass | SkillObject | string, otherPlayer?: RpgPlayer | RpgPlayer[]): SkillData ``` ### Parameters * `skillInput`: `SkillClass | SkillObject | string` * `otherPlayer?`: `RpgPlayer | RpgPlayer[]` ### Returns The used skill data ## WithSkillManager Skill Manager Mixin Provides skill management capabilities to any class. This mixin handles learning, forgetting, and using skills, including SP cost management, hit rate calculations, and skill effects application. Supports three input formats for skills: * **String ID**: References a skill in the database * **Class**: A skill class that will be instantiated * **Object**: A skill object with properties and hooks * Source: `packages/server/src/Player/SkillManager.ts` * Kind: `function` ### Signature ```ts theme={null} WithSkillManager(Base: TBase): TBase ``` ### Parameters * `Base`: `TBase` ### Returns Extended class with skill management methods ### Examples ```ts theme={null} // Using string ID (from database) player.learnSkill('fire'); // Using skill class player.learnSkill(FireSkill); // Using skill object player.learnSkill({ id: 'ice', name: 'Ice', spCost: 15, onUse(player) { console.log('Ice spell cast!'); } }); ``` # State Commands Source: https://v5.rpgjs.dev/api/player/state-commands Apply, remove, and inspect player states. # State Commands Apply, remove, and inspect player states. ## Members * [addState](#addstate) * [applyStates](#applystates) * [createStateInstance](#createstateinstance) * [findStateEfficiency](#findstateefficiency) * [getState](#getstate) * [removeState](#removestate) * [resolveStatesSnapshot](#resolvestatessnapshot) * [statesDefense](#statesdefense) * [statesEfficiency](#statesefficiency) * [WithStateManager](#withstatemanager) ## addState Adds a state to the player * Source: `packages/server/src/Player/StateManager.ts` * Kind: `method` * Defined in: `IStateManager` ### Signature ```ts theme={null} addState(stateClass: StateClass | string, chance?: number): object | null ``` ### Parameters * `stateClass`: `StateClass | string` * `chance?`: `number` ### Returns The state instance if successfully applied, null if already present ## applyStates Apply states to a player from skill or item effects * Source: `packages/server/src/Player/StateManager.ts` * Kind: `method` * Defined in: `IStateManager` ### Signature ```ts theme={null} applyStates(player: RpgPlayer, states: { addStates?: StateApplication[]; removeStates?: StateApplication[] }): void ``` ### Parameters * `player`: `RpgPlayer` * `states`: `{ addStates?: StateApplication[]; removeStates?: StateApplication[] }` ## createStateInstance Create a state instance without side effects. * Source: `packages/server/src/Player/StateManager.ts` * Kind: `method` ### Signature ```ts theme={null} createStateInstance(stateInput: StateClass | string) ``` ### Parameters * `stateInput`: `StateClass | string` ## findStateEfficiency Find state efficiency modifier for a specific state class * Source: `packages/server/src/Player/StateManager.ts` * Kind: `method` * Defined in: `IStateManager` ### Signature ```ts theme={null} findStateEfficiency(stateClass: StateClass): StateEfficiency | undefined ``` ### Parameters * `stateClass`: `StateClass` ### Returns The efficiency object if found, undefined otherwise ## getState Get a state to the player. Returns null if the state is not present * Source: `packages/server/src/Player/StateManager.ts` * Kind: `method` * Defined in: `IStateManager` ### Signature ```ts theme={null} getState(stateClass: StateClass | string): StateData | undefined ``` ### Parameters * `stateClass`: `StateClass | string` ### Returns The state instance if found, null otherwise ## removeState Remove a state to the player * Source: `packages/server/src/Player/StateManager.ts` * Kind: `method` * Defined in: `IStateManager` ### Signature ```ts theme={null} removeState(stateClass: StateClass | string, chance?: number): void ``` ### Parameters * `stateClass`: `StateClass | string` * `chance?`: `number` ## resolveStatesSnapshot Resolve state snapshot entries into state instances without side effects. * Source: `packages/server/src/Player/StateManager.ts` * Kind: `method` ### Signature ```ts theme={null} resolveStatesSnapshot(snapshot: { states?: any[] }, mapOverride?: any) ``` ### Parameters * `snapshot`: `{ states?: any[] }` * `mapOverride?`: `any` ## statesDefense Gets the defensive capabilities against various states from equipped items * Source: `packages/server/src/Player/StateManager.ts` * Kind: `property` * Defined in: `IStateManager` ### Signature ```ts theme={null} statesDefense: StateEfficiency[] ``` ### Returns Array of state defense objects with rate and state properties ## statesEfficiency Manages the player's state efficiency modifiers * Source: `packages/server/src/Player/StateManager.ts` * Kind: `property` * Defined in: `IStateManager` ### Signature ```ts theme={null} statesEfficiency: RpgWritableSignal ``` ### Returns Signal containing array of state efficiency objects ## WithStateManager State Manager Mixin Provides state management capabilities to any class. This mixin handles player states (buffs/debuffs), state defense from equipment, and state efficiency modifiers. It manages the complete state system including application, removal, and resistance mechanics. * Source: `packages/server/src/Player/StateManager.ts` * Kind: `function` ### Signature ```ts theme={null} WithStateManager(Base: TBase) ``` ### Parameters * `Base`: `TBase` ### Returns Extended class with state management methods ### Examples ```ts theme={null} class MyPlayer extends WithStateManager(BasePlayer) { constructor() { super(); // State system is automatically initialized } } const player = new MyPlayer(); player.addState(Paralyze); console.log(player.getState(Paralyze)); ``` # Variable Commands Source: https://v5.rpgjs.dev/api/player/variable-commands Store and retrieve player variables that persist across saves and map transfers. # Variable Commands Store and retrieve player variables that persist across saves and map transfers. ## Members * [clearVariables](#clearvariables) * [getVariable](#getvariable) * [getVariableKeys](#getvariablekeys) * [hasVariable](#hasvariable) * [removeVariable](#removevariable) * [setVariable](#setvariable) * [variables](#variables) * [WithVariableManager](#withvariablemanager) ## clearVariables Clear all variables * Source: `packages/server/src/Player/VariableManager.ts` * Kind: `method` * Defined in: `IVariableManager` ### Signature ```ts theme={null} clearVariables(): void ``` ## getVariable Get a variable value * Source: `packages/server/src/Player/VariableManager.ts` * Kind: `method` * Defined in: `IVariableManager` ### Signature ```ts theme={null} getVariable(key: string): T | undefined ``` ### Parameters * `key`: `string` ### Returns The stored value or undefined if not found ## getVariableKeys Get all variable keys * Source: `packages/server/src/Player/VariableManager.ts` * Kind: `method` * Defined in: `IVariableManager` ### Signature ```ts theme={null} getVariableKeys(): string[] ``` ### Returns Array of all variable keys ## hasVariable Check if a variable exists * Source: `packages/server/src/Player/VariableManager.ts` * Kind: `method` * Defined in: `IVariableManager` ### Signature ```ts theme={null} hasVariable(key: string): boolean ``` ### Parameters * `key`: `string` ### Returns true if the variable exists, false otherwise ## removeVariable Remove a variable * Source: `packages/server/src/Player/VariableManager.ts` * Kind: `method` * Defined in: `IVariableManager` ### Signature ```ts theme={null} removeVariable(key: string): boolean ``` ### Parameters * `key`: `string` ### Returns true if a variable existed and has been removed, false otherwise ## setVariable Assign a variable to the player. Use player variables for quest flags, per-player event state, and any value that must survive saves and map transitions. * Source: `packages/server/src/Player/VariableManager.ts` * Kind: `method` * Member of: `VariableManager` * Defined in: `IVariableManager` ### Signature ```ts theme={null} setVariable(key: string, val: T): void ``` ### Parameters * `key`: `string` * `val`: `T` ## variables Map storing all player variables. These values belong to the player, are persisted, and travel with the player snapshot when switching maps or servers. * Source: `packages/server/src/Player/VariableManager.ts` * Kind: `property` * Defined in: `IVariableManager` ### Signature ```ts theme={null} variables: RpgWritableSignal> ``` ## WithVariableManager Variable Manager Mixin Provides variable management capabilities to any class. Variables are key-value pairs that can store any type of data associated with the player, such as quest progress, game flags, inventory state, and custom game data. Player variables have two main roles: 1. Persist player-specific state so it can be restored from saves. 2. Carry that state across maps and map servers through the player snapshot. * Source: `packages/server/src/Player/VariableManager.ts` * Kind: `function` ### Signature ```ts theme={null} WithVariableManager(Base: TBase) ``` ### Parameters * `Base`: `TBase` ### Returns Extended class with variable management methods ### Examples ```ts theme={null} class MyPlayer extends WithVariableManager(BasePlayer) { constructor() { super(); // Variables are automatically initialized } } const player = new MyPlayer(); player.setVariable('questCompleted', true); ``` # MMORPG Account Source: https://v5.rpgjs.dev/gui/account Display a sign-in and sign-up GUI before connecting an RPGJS MMORPG client. # MMORPG Account `@rpgjs/account` is an optional MMORPG-only module. It renders a replaceable CanvasEngine account GUI before the first WebSocket connection. Your application continues to own its HTTP API, database, password policy, and token format. ```bash theme={null} npm install @rpgjs/account @rpgjs/ui-css ``` ```ts theme={null} import { accountQuery, provideAccount } from "@rpgjs/account/client"; import { provideMmorpg, startGame } from "@rpgjs/client"; startGame({ providers: [ provideAccount({ client: { signIn: async credentials => { const response = await fetch("/api/sign-in", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(credentials), }); if (!response.ok) throw new Error("sign-in failed"); return response.json(); // { token: string } }, signUp: async ({ username, email, password, passwordConfirmation }) => { const response = await fetch("/api/sign-up", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ username, email, password, passwordConfirmation }), }); if (!response.ok) throw new Error("sign-up failed"); return response.json(); }, storageKey: "my-game-account-token", }, }), provideMmorpg({ deferConnection: true, query: accountQuery, }), ], }); ``` Import `@rpgjs/ui-css/index.css` for the default semantic styles. A custom CanvasEngine or Vue GUI can replace the component through the `component` and `renderer` options while reusing `submitAccount()`, `accountSession`, `accountPending`, `accountError`, and `signOut()`. Tokens are stored in `localStorage` by default. This persists the account across browser restarts but means an XSS vulnerability could read it. Use a strict CSP, avoid unsafe script injection, and provide another `storage` implementation when your security model requires one. Passwords are only passed to the configured adapter and are never stored by the module. The default registration form asks for a distinct username and email, plus a password confirmation. Confirmation is checked client-side before `signUp` is called. Username and email uniqueness must still be enforced by the account endpoint; client validation is never authoritative. After the server accepts the token, the account GUI closes and the normal RPGJS flow continues. A server-provided title screen can then offer Start or Load; authentication does not select or load a save slot. ## Branding and sound The built-in GUI accepts `title`, `subtitle`, `backgroundImage` (URL), `backgroundMusic` (registered RPGJS sound ID), and `sounds: { open, navigate, confirm, error }` (registered sound IDs). Missing sound overrides use the engine's configured UI cues. Register the assets using the normal client sound configuration. Music uses the engine music controller and is released when the account screen closes; browser autoplay restrictions still apply until the first player interaction. ```ts theme={null} provideAccount({ client: { signIn, signUp, title: "Crystal Chronicles", subtitle: "Your next adventure awaits.", backgroundImage: "/images/login.webp", backgroundMusic: "login-theme", sounds: { navigate: "menu-select", confirm: "menu-confirm", error: "menu-error" }, forgotPassword: ({ email }) => api.requestPasswordReset({ email }), resetPassword: credentials => api.resetPassword(credentials), resetToken: tokenFromEmailLink, }, }); ``` ## Password recovery Providing `forgotPassword` enables the email request form. Providing `resetPassword` enables the recovery-code/new-password form. Both adapters return `Promise`; recovery does not authenticate or open a WebSocket. Pass an optional `resetToken` from your application's email-link routing to open the reset form directly, even when a session token is stored. The server must issue expiring, single-use codes, enforce its password policy, and return the same request response for known and unknown email addresses. The old password is never retrieved or displayed. Successful reset returns the player to sign-in. Adapter errors may throw `{ key, params }` for localized feedback. The generic fallback does not expose raw server errors. Shared CSS primitives include `rpg-ui-form-grid`, `rpg-ui-form-field`, `rpg-ui-form-hint`, `rpg-ui-form-feedback` (`data-tone="error|success"`), `rpg-ui-form-actions`, and `rpg-ui-text-button`. Storybook's `Compositions / Account` covers login, registration, recovery, reset and feedback. When a stored token exists, the account GUI remains hidden while the session is restored. A successful restoration therefore opens the title screen directly, without flashing the account form first. # Calendar module Source: https://v5.rpgjs.dev/gui/calendar A generic calendar with declared events, sources of events (quests, birthdays, weather), hooks and a calendar window, with @rpgjs/calendar. # Calendar module `@rpgjs/calendar` is an optional RPGJS v5 module. It keeps the **calendar of the world** (the date of today and the events on it) and shows it in a window. It has no clock: your game moves the date, for example when the player sleeps. A synchronized time system can drive it later without changing the API below. The server owns the data. The client only displays what the server sends, and asks it for another month. ## Register it ```ts theme={null} // server.ts import { provideCalendar } from "@rpgjs/calendar/server" import { createServer, provideServerModules } from "@rpgjs/server" export default createServer({ providers: [ provideCalendar({ calendar: { seasons: ["spring", "summer", "autumn", "winter"] }, today: { year: 1, month: 1, day: 3 }, }), provideServerModules([]), ], }) ``` ```ts theme={null} // client config: before provideClientModules() import { provideCalendar } from "@rpgjs/calendar/client" import { provideClientModules } from "@rpgjs/client" export default { providers: [provideCalendar(), provideClientModules([])], } ``` ## The calendar | Option | Default | | | - | - | - | | `months` | `12` | Months of a year. | | `daysPerMonth` | `30` | One number, or one number per month. | | `weekdays` | `mon` … `sun` | Keys of the days, in display order. The first one starts the week. | | `epochWeekday` | `0` | Index in `weekdays` of the first day of year 1. | | `seasons` | none | Split evenly over the months. | ## Events ```ts theme={null} provideCalendar({ categories: { market: { color: "#5fd16a", icon: "⚑" }, combat: { color: "#ef5a4a", icon: "/icons/swords.png" }, }, events: [ // A date: comes back every year because there is no `year` { id: "market", title: "Village Market", description: "Merchants gather.", category: "market", on: { month: 1, day: 7 } }, // Every month: no `month` { id: "pay-day", title: "Pay day", on: { day: 1 } }, // Every friday, only in summer { id: "arena", title: "Arena Challenge", category: "combat", every: { weekday: "fri" }, season: "summer" }, // Several days, with the time and the place { id: "festival", title: "Spring Festival", on: { month: 1, day: 21 }, lasts: 3, from: "10:00", to: "22:00", map: "capital" }, ], }) ``` * `title` and `description` are shown as they are, or translated when they start with `rpg.`. * `icon` is an image URL or a path (`/icons/market.png`), or a short glyph (`⚑`). An event takes the icon and the color of its category when it has none. * A multi-day event is listed on each of its days. * An invalid event throws a readable error when it is added. Add and remove events at runtime with `calendar.addEvent()` and `calendar.removeEvent(id)`. ## Sources A source provides events for a range of dates. Use it for what the game already knows: quests, birthdays of NPCs, the weather of the day. ```ts theme={null} calendar.addSource({ id: "quests", scope: "player", // only asked for the player who opens the calendar // `getQuests()` stands for your own quest system list: ({ player, range, today }) => player.getQuests().filter((quest) => quest.deadline).map((quest) => ({ id: `quest-${quest.id}`, title: quest.name, category: "quest", on: quest.deadline, })), }) ``` * `world` sources (the default) are shown to everybody. `player` sources are only asked when a player opens the calendar, so one player's events never reach another. * `list()` is called **once** each time the calendar is opened or another month is asked. It gets a range of about a year (the month and the events to come), so a source that reads a database should filter by `range`. * A source that throws, or returns an invalid event, is logged and skipped: the calendar still opens. ## Move today and react ```ts theme={null} const calendar = inject(CalendarService) await calendar.advance(1) // tomorrow. { weeks: 1 } works too await calendar.setToday({ year: 1, month: 2, day: 1 }) calendar.today() ``` ```ts theme={null} provideCalendar({ hooks: { onDayChange: ({ previous, current }) => {}, onEventStart: ({ entry, date }) => {}, // today reached the first day of a world event onEventEnd: ({ entry, date }) => {}, // today passed its last day }, }) ``` Hooks only see the events of the world. A hook that throws is logged and the others still run. ## Open the window ```ts theme={null} await calendar.open(player) // resolves when the player closes it await calendar.open(player, { month: 2 }) // another month calendar.open(player, { waitForClose: false }) ``` The player moves between months with the arrows, selects a day with the pointer or the movement keys (left and right move by a day, up and down by a week), and closes the window with the close button or `escape`. The list on the right shows the events from the selected day on. Add `import "@rpgjs/ui-css/index.css"` to get the styles. The catalogue of the GUI has the calendar in **Compositions / Calendar**. Replace the window with `provideCalendar({ component })`; it receives the `CalendarView` as `data`. ## Texts Translate with `rpg.calendar.*` keys (English and French are included): `title`, `subtitle`, `events`, `weekday.mon`…, `season.spring`…, `month.1`… A month is named by its `rpg.calendar.month.N` key when you give one. Otherwise it shows its season, with its place when a season has several months (`Spring 2`). # Character Select Source: https://v5.rpgjs.dev/gui/character-select Offer server-owned Actors and let the player choose one before starting a game. # Character Select Use `player.showCharacterSelect()` to open the native `rpg-character-select` GUI. The server resolves every Actor, sends only presentation data to the client, and returns only an Actor that was in the offered list. ```ts theme={null} const actor = await player.showCharacterSelect([Hero, Mage], { title: "Choose your hero", selectedActorId: "hero", }) if (actor) { player.setActor(actor) } ``` The selector does not apply the result automatically. This keeps game startup, save-slot creation, and MMORPG account logic explicit. Inputs can be Actor constructors, registered database IDs, or resolved Actor objects. Every resolved Actor must expose a unique non-empty `id`. The built-in cinematic carousel supports keyboard, gamepad, pointer buttons, and horizontal swipe. It renders these optional Actor presentation fields: * `name`, `description`, `graphic`, `faceset`, and `illustration` * `className`, `classDescription`, and `classIcon`, or a resolved `class` object * `parameters.maxHp`, `parameters.str`, `parameters.pdef`, `parameters.agi`, and `parameters.int` (the curve `start` value is displayed) Only these presentation fields are sent to the client. Other Actor and Class properties remain server-side. Register `graphic`, `illustration`, and `classIcon` sprite sheets on the client before opening the GUI. `allowCancel` defaults to `false`. When enabled, cancellation resolves to `null`. Player movement is blocked while the GUI is open. Projects can replace the CanvasEngine component by registering another GUI with `PrebuiltGui.CharacterSelect`; preserve the interactions documented in [Prebuilt GUI Contracts](/gui/prebuilt-contracts). ## Switching actors during play Use `changeActor()` when reopening the selector during gameplay: ```ts theme={null} const actor = await player.showCharacterSelect([Hero, Mage], { selectedActorId: "hero", allowCancel: true, }) if (actor) player.changeActor(actor) ``` The authoritative server applies the actor's name, graphic, hitbox, default class, parameter curves and combat animation bindings. Level, experience, inventory, equipment, learned skills, states and modifiers are retained. HP/SP keep their fill ratios, and starting equipment is not granted again. Cancellation applies no actor changes. `setActor()` remains the initialization API. In Studio, the `call_character_select` block offers all Actors or an ordered `actorIds` subset and persists the chosen Actor ID. It normalizes Studio media references before calling the server API. `change_class` can then select another resolved Class. Saves and room transfers retain that class, the selected Actor, and progression; restoring a player does not grant starting inventory again. These rules apply in both standalone RPG and MMORPG modes. # Chat module Source: https://v5.rpgjs.dev/gui/chat Configure authoritative map or global chat, moderation hooks, themes, and replacement components with @rpgjs/chat. # Chat module `@rpgjs/chat` is an optional RPGJS v5 module with a server-authoritative message flow: 1. The client submits text and a requested channel. 2. The server normalizes and validates the text, applies rate limits and moderation, and reconstructs the author and timestamp. 3. The server broadcasts the accepted `ChatMessage`. 4. Each client stores the message in a reactive signal and renders it. The default GUI is a CanvasEngine component, but presentation is replaceable without changing the server behavior. ## Install ```bash theme={null} npm install @rpgjs/chat @rpgjs/ui-css ``` The default component also uses the semantic styles from `@rpgjs/ui-css`. ```ts theme={null} // src/client.ts or src/standalone.ts import '@rpgjs/ui-css/index.css' ``` ## Configure the client Use the explicit client entry point. This prevents server code from entering a browser bundle when standalone mode loads both runtime entry graphs. ```ts theme={null} // config/config.client.ts import { provideChat } from '@rpgjs/chat/client' import { provideClientModules } from '@rpgjs/client' export default { providers: [ provideChat({ client: { position: 'bottom-left', maxMessages: 100, maxLength: 180, autoOpen: true, }, }), // Module providers must be registered before this aggregator. provideClientModules([]), ], } ``` ### Client options | Option | Default | Description | | - | - | - | | `guiId` | `'rpg-chat'` | GUI identifier used by `RpgGui`. | | `component` | Built-in `chat.ce` | CanvasEngine or Vue replacement component. | | `renderer` | `'canvas'` | Use `'canvas'` for `.ce` and `'vue'` for Vue. | | `autoOpen` | `true` | Display the chat when the current player is ready. | | `position` | `'bottom-left'` | `top-left`, `top-right`, `bottom-left`, or `bottom-right`. | | `maxMessages` | `100` | Maximum messages retained by the local client signal. | | `maxLength` | `180` | Maximum length accepted by the built-in input. Keep it aligned with the server option. | `maxMessages` limits client memory only. It does not create permanent history. The server remains authoritative for `maxLength`; configure the same value on both sides so the built-in input rejects exactly the same message lengths. ## Configure the server Use the explicit server entry point and register it before `provideServerModules()`: ```ts theme={null} // server.ts import { provideChat } from '@rpgjs/chat/server' import { createServer, provideServerModules } from '@rpgjs/server' export default createServer({ providers: [ provideChat({ server: { channels: ['map'], maxLength: 180, rateLimit: { maxMessages: 5, windowMs: 10_000, }, }, }), provideServerModules([]), ], }) ``` Map chat is enabled by default. A map message is broadcast only to players in the sender's current map. An omitted channel defaults to `map`; any explicitly supplied value other than `map` or `global` is rejected. ### Server options and hooks | Option | Default | Description | | - | - | - | | `channels` | `['map']` | Accepted channels: `map`, `global`, or both. | | `maxLength` | `180` | Maximum normalized message length. | | `rateLimit` | 5 messages / 10 seconds | Per-player rate limit; use `false` to disable it. | | `formatAuthor(player)` | Player name or `Player {id}` | Builds the trusted display name on the server. | | `beforeSend(context)` | — | Rejects with `false`, or returns the text that should be broadcast. May be async. | | `afterSend(message, player)` | — | Runs after a successful broadcast. Useful for audit, metrics, or automated replies. May be async. | | `broadcastGlobal(message, player)` | — | Required adapter when `global` is enabled. | The hook order is: ```text theme={null} normalize → length/rate validation → beforeSend → construct ChatMessage → broadcast → afterSend ``` ### Moderation example ```ts theme={null} import { provideChat } from '@rpgjs/chat/server' provideChat({ server: { channels: ['map'], formatAuthor(player) { return `[Guild] ${player.name || player.id}` }, beforeSend({ text, player, channel }) { if (isMuted(player, channel)) return false return filterBlockedWords(text) }, async afterSend(message, player) { await auditChatMessage({ message, accountId: player.id, }) }, }, }) ``` Clients never choose the accepted author, player ID, map ID, or timestamp. ## Global chat Global chat is opt-in because the correct broadcast mechanism depends on the deployment topology. Enable the channel and provide an adapter: ```ts theme={null} provideChat({ server: { channels: ['map', 'global'], async broadcastGlobal(message, player) { await applicationChatBus.publish(message, { senderId: player.id, }) }, }, }) ``` The adapter is responsible for delivering the message to all intended clients, including clients hosted by another room, process, or edge location. ## Client API Replacement components and client modules can import: | Export | Purpose | | - | - | | `chatMessages` | Reactive `ChatMessage[]` signal. | | `chatError` | Reactive signal containing the last server validation error. | | `chatClientOptions` | Resolved client configuration signal. | | `sendChatMessage(text, channel?)` | Submits text to the authoritative server. | | `clearChatMessages()` | Clears local retained messages. | | `openChat()` | Opens the configured chat GUI manually. | `sendChatMessage()` does not add an optimistic message. The UI changes only after the server broadcasts the accepted `ChatMessage`. ### Message contract ```ts theme={null} interface ChatMessage { id: string text: string author: string playerId: string channel: 'map' | 'global' mapId?: string createdAt: number } ``` ## Replace the CanvasEngine component A replacement component does not receive the message history as props. Instead, it consumes the public chat signals. GUI data contains the configured `position` and `maxLength`. Create `gui/custom-chat.ce`: ```html theme={null}
@for (message of messages) {

{message.author} {message.text}

}
@if (errorText()) { }
``` CanvasEngine's `DOMContainer` binds `value={draft}` in both directions. `submit={submitMessage}` prevents the browser's default form navigation and passes `(event, formData)` to the handler. Register the replacement: ```ts theme={null} import { provideChat } from '@rpgjs/chat/client' import CustomChat from './gui/custom-chat.ce' provideChat({ client: { component: CustomChat, renderer: 'canvas', }, }) ``` See the CanvasEngine documentation for [DOMContainer](https://canvasengine.net/components/dom-container.md), [template syntax](https://canvasengine.net/concepts/template-syntax.md), and [reactive signals](https://canvasengine.net/concepts/reactive.md). ## Use a Vue component Install and configure the RPGJS Vue overlay as described in [Vue.js integration](/gui/vue-integration), then use a Vue component as the replacement: ```ts theme={null} import { provideChat } from '@rpgjs/chat/client' import { provideClientModules } from '@rpgjs/client' import { provideVueGui } from '@rpgjs/vue' import CustomChat from './gui/custom-chat.vue' export default { providers: [ provideVueGui({ selector: '#vue-gui-overlay', createIfNotFound: true, }), provideChat({ client: { component: CustomChat, renderer: 'vue', }, }), provideClientModules([]), ], } ``` The Vue component can subscribe to `chatMessages.observable` and `chatError.observable`, and submit with `sendChatMessage()`. Remember to unsubscribe when the component unmounts. ```vue theme={null} ``` ## Themes and CSS The default component uses semantic `rpg-ui-*` classes. Import the base theme: ```ts theme={null} import '@rpgjs/ui-css/index.css' ``` Add the pixel theme after it: ```ts theme={null} import '@rpgjs/ui-css/index.css' import '@rpgjs/ui-css/theme-pixel.css' ``` Then apply the theme class to an ancestor, usually ``: ```html theme={null}
``` You can also override individual semantic classes: ```css theme={null} .rpg-ui-chat { width: 36rem; background: rgba(15, 12, 28, 0.96); } .rpg-ui-chat-author { color: #f6c177; } .rpg-ui-chat-message[data-player-id="pixel-guide"] { color: #7ecf6b; } ``` ## Errors and translations The default client module provides these English translation keys: ```text theme={null} rpg.chat.placeholder rpg.chat.send rpg.chat.error.empty rpg.chat.error.too-long rpg.chat.error.rate-limit rpg.chat.error.channel rpg.chat.error.rejected ``` Override them through the regular RPGJS i18n configuration. Custom components should display `chatError` through `engine.i18n().t()` instead of exposing raw translation keys. ## Run the pixel chat playground The repository includes a complete standalone and MMORPG-compatible example: ```bash theme={null} pnpm --dir playground dev:chat ``` Open `http://localhost:5189`. The example demonstrates map broadcast, rate limiting, an `afterSend` automated reply, a CanvasEngine GUI, and the shared pixel theme. Private messages, voice chat, and permanent history are intentionally outside the core module. # Dialog Box Source: https://v5.rpgjs.dev/gui/dialog-box Guide for Dialog Box in RPGJS. # Dialog Box The dialog box system in RPG.js allows you to display text messages and choices to players. It provides a built-in GUI component that handles text display, typewriter effects, and choice selection. ## Basic Usage ### Show Text The `showText()` method displays a simple text message to the player: ```typescript theme={null} // Basic text display await player.showText('Hello World!'); // Text with options await player.showText('Welcome to our village!', { position: 'bottom', typewriterEffect: true, autoClose: false }); ``` ### Show Choices The `showChoices()` method displays a dialog with multiple choices for the player to select from: ```typescript theme={null} // Basic choices const choice = await player.showChoices('What would you like to do?', [ { text: 'Fight', value: 'fight' }, { text: 'Run away', value: 'run' }, { text: 'Talk', value: 'talk' } ]); if (choice) { console.log('Player selected:', choice.value); } ``` ### Show an Input Pass `input` to display a typed field in the same dialog box. The field is active immediately, including while the typewriter animation is running. It accepts the same input, textarea, and validation options as `player.showInput()`. ```typescript theme={null} const age = await player.showText('How old are you?', { speaker: 'Innkeeper', input: { type: 'number', required: true, min: 1, max: 120 } }) // age is number | null ``` Number inputs resolve to `number | null`; text inputs and textareas resolve to `string | null`. Input and `choices` are mutually exclusive. Validation runs on the server, while the dialog remains open to display translated errors. Button labels are customizable with `confirmText` and `cancelText`. Set `cancelButton: false` to hide the Cancel button while keeping the dialog's normal close controls available. ## Dialog Options `showText()` and `showChoices()` accept the following dialog options: ### Position Controls where the dialog box appears on screen: ```typescript theme={null} await player.showText('Message', { position: 'bottom' // 'top', 'middle', or 'bottom' }); ``` ### Full Width Determines if the dialog box takes the full width of the screen: ```typescript theme={null} await player.showText('Message', { fullWidth: true // true by default }); ``` ### Auto Close If enabled, the dialog automatically closes after a delay: ```typescript theme={null} await player.showText('Message', { autoClose: true // false by default }); ``` ### Typewriter Effect Controls the typewriter animation effect: ```typescript theme={null} await player.showText('Message', { typewriterEffect: true // true by default }); ``` ### Talk With Makes an event or player turn toward the player during the dialog: ```typescript theme={null} // In an event await player.showText('Hello!', { talkWith: this // Makes the event turn toward the player }); ``` ### Speaker Name Overrides the speaker label shown in the dialog: ```typescript theme={null} await player.showText('Hello!', { talkWith: this, speaker: 'Mystery Voice' }); ``` ### Face Display Shows a character face with a specific expression: ```typescript theme={null} await player.showText('Hello!', { face: { id: 'facesetId', expression: 'happy' } }); ``` ## Faceset Configuration To use faces in dialogs, you need to configure the faceset spritesheet on the client side. The faceset is a single image containing multiple character expressions arranged in a grid. ### Client-side Setup In your client configuration (`config.client.ts`), add the faceset spritesheet: ```typescript theme={null} import { Presets } from "@rpgjs/client"; export default { providers: [ provideClientModules([ { spritesheets: [ Presets.FacesetPreset({ id: "facesetId", image: "faceset.png", width: 1024, height: 1024, }, 3, 4, { happy: [0, 0], sad: [1, 0], angry: [2, 0], surprised: [0, 1], neutral: [1, 1], worried: [2, 1] }), ], }, ]), ], }; ``` ### Faceset Parameters * `id`: Unique identifier for the faceset * `image`: Path to the faceset image file * `width/height`: Dimensions of the faceset image * `framesWidth/framesHeight`: Number of frames horizontally and vertically in the grid * `expressions`: Object mapping expression names to their grid positions `[frameX, frameY]` ### Using Faces in Dialogs Once configured, you can use the faceset in dialogs: ```typescript theme={null} // Server-side usage await player.showText('Hello!', { face: { id: 'facesetId', // Must match the spritesheet ID expression: 'happy' // Must match an expression defined in the preset } }); ``` ### Faceset Image Layout Your faceset image should be organized in a grid. For example, with `framesWidth: 3` and `framesHeight: 4`: ``` [0,0] [1,0] [2,0] [0,1] [1,1] [2,1] [0,2] [1,2] [2,2] [0,3] [1,3] [2,3] ``` Each position `[frameX, frameY]` corresponds to a specific expression in your faceset. ## Dialog Box Customization You can customize the appearance and sounds of the dialog box using `provideClientGlobalConfig()` in your client configuration. ### Styling the Dialog Box ```typescript theme={null} import { provideClientGlobalConfig } from "@rpgjs/client"; export default { providers: [ provideClientGlobalConfig({ box: { styles: { backgroundColor: "#1a1a2e", // Background color backgroundOpacity: 0.9, // Background opacity } } }), ], }; ``` ### Customizing Sounds You can customize the sounds used by the dialog box. The sound IDs in `provideClientGlobalConfig()` must correspond to the sound IDs defined in `provideClientModules()`. > **See also:** [Sounds Guide](/guide/sounds) for comprehensive information about sound configuration, dynamic sound resolution, and playing sounds from the server. ```typescript theme={null} import { provideClientGlobalConfig } from "@rpgjs/client"; export default { providers: [ provideClientGlobalConfig({ box: { sounds: { typewriter: "typewriterId", // Sound for typewriter effect cursorMove: "cursorId", // Sound when moving cursor in choices cursorSelect: "selectId" // Sound when selecting a choice } } }), ], }; ``` **Important**: The sound IDs in the `box.sounds` configuration must match the sound IDs defined in your `provideClientModules()` configuration. The sounds themselves are loaded through the `sounds` array in `provideClientModules()`. ### Complete Configuration Example ```typescript theme={null} import { provideClientGlobalConfig, provideClientModules } from "@rpgjs/client"; export default { providers: [ provideClientGlobalConfig({ box: { styles: { backgroundColor: "#2a2a4e", backgroundOpacity: 0.95, }, sounds: { typewriter: "typewriterId", cursorMove: "cursorId", cursorSelect: "selectId" } } }), provideClientModules([ { sounds: [ { id: "typewriterId", src: "typewriter.wav", }, { id: "cursorId", src: "cursor.wav", }, { id: "selectId", src: "select.wav", } ], }, ]), ], }; ``` ## See Also * [Sounds Guide](/guide/sounds) - Comprehensive guide on configuring and using sounds, including dynamic sound resolution * [Prebuilt GUI Contracts](/gui/prebuilt-contracts) - Data and interaction contract for replacing `rpg-dialog` with your own CanvasEngine or Vue dialog box # Engine Injection in .ce Files Source: https://v5.rpgjs.dev/gui/engine-injection Guide for Engine Injection in .ce Files in RPGJS. # Engine Injection in .ce Files This guide explains how to access the RPG-JS client engine and retrieve events and players data within Canvas Engine (`.ce`) component files. ## Injecting the Engine To access the RPG-JS client engine in a `.ce` file, you need to use the dependency injection system: ```html theme={null} ``` ## Accessing Events and Players Once you have injected the engine, you can access the events and players through the scene map: ```html theme={null} ``` ## Understanding Signals Both `players` and `events` are **signals** - reactive data structures that automatically notify components when their data changes. This means: * **Automatic Updates**: When a player moves or an event changes on the server, the signal automatically updates * **Reactive Rendering**: Your component will re-render automatically when the data changes * **Real-time Synchronization**: Changes from the server are synchronized in real-time through WebSocket connections ### Signal Properties ```javascript theme={null} // players and events are signals containing Record const players = engine.sceneMap.players; // Signal> const events = engine.sceneMap.events; // Signal> ``` ## Complete Example Here's a complete example showing how to use the engine injection to display events and players: ```html theme={null} @for ((event, id) of events) { } @for ((player, id) of players) { } ``` ## GUI System with Dependencies RPG-JS provides an advanced GUI system that supports automatic display management based on signal dependencies. This allows you to create GUIs that only appear when certain conditions are met. ### Basic GUI Configuration ```typescript theme={null} import { defineModule, RpgClient } from '@rpgjs/client' import InventoryComponent from './inventory.ce' defineModule({ gui: [ { id: 'inventory', component: InventoryComponent } ] }) ``` ### Auto Display You can configure a GUI to automatically display when it's added to the system: ```typescript theme={null} defineModule({ gui: [ { id: 'inventory', component: InventoryComponent, autoDisplay: true // GUI will show immediately when loaded } ] }) ``` ### Dependencies-Based Display The most powerful feature is the ability to define dependencies that must be resolved before the GUI appears: ```typescript theme={null} import { inject, RpgClientEngine } from "@rpgjs/client"; defineModule({ gui: [ { id: 'player-stats', component: PlayerStatsComponent, autoDisplay: true, dependencies: () => { const engine = inject(RpgClientEngine); return [ engine.scene.currentPlayer, // Wait for player to be loaded ]; } } ] }) ``` ### Advanced Dependencies Example Here's a more complex example showing multiple dependencies: ```typescript theme={null} defineModule({ gui: [ { id: 'shop-interface', component: ShopComponent, autoDisplay: true, dependencies: () => { const engine = inject(RpgClientEngine); const gui = inject(RpgGui); return [ engine.scene.currentPlayer, // Player must be loaded engine.scene.data, // Scene data must be available someCustomSignal // Your custom signal ]; } }, { id: 'minimap', component: MinimapComponent, autoDisplay: true, dependencies: () => { const engine = inject(RpgClientEngine); return [ engine.scene.data, // Wait for map data computed(() => engine.scene.players().length == 0 ? undefined : engine.scene.players()) // Wait for players data ]; } } ] }) ``` ### Manual GUI Control You can still manually control GUIs using the `RpgGui` service: ```typescript theme={null} // In a .ce component or hook import { inject, RpgGui, RpgClientEngine } from "@rpgjs/client"; const gui = inject(RpgGui); const engine = inject(RpgClientEngine); // Display a GUI manually gui.display('inventory', { items: [] }); // Display with runtime dependencies (overrides config dependencies) gui.display('shop', { shopId: 1 }, [engine.scene.currentPlayer]); // Hide a GUI gui.hide('inventory'); // Check if GUI exists if (gui.exists('inventory')) { // GUI is registered } ``` ### Best Practices for GUI Dependencies 1. **Specific Dependencies**: Only include signals that are truly required for your GUI 2. **Avoid Circular Dependencies**: Don't create dependency chains that could cause loops 3. **Performance**: Keep dependency arrays small for better performance 4. **Error Handling**: Ensure your dependencies can handle undefined states gracefully 5. **Testing**: Test your GUIs with different loading scenarios ### Common Dependency Patterns ```typescript theme={null} // Wait for player authentication dependencies: () => [inject(RpgClientEngine).playerIdSignal] // Wait for scene to be fully loaded dependencies: () => { const engine = inject(RpgClientEngine); return [engine.scene.data, engine.scene.currentPlayer]; } // Wait for specific game state dependencies: () => { const engine = inject(RpgClientEngine); return [ engine.scene.currentPlayer, customGameStateSignal, inventorySignal ]; } // Multiple related GUIs dependencies: () => { const engine = inject(RpgClientEngine); const gui = inject(RpgGui); return [ engine.scene.currentPlayer, // Wait for another GUI to be ready gui.get('main-menu')?.display ]; } ``` ## Additional Engine Properties The engine provides access to many other useful properties: ```javascript theme={null} const engine = inject(RpgClientEngine); // Scene and map data const sceneData = engine.sceneMap.data; const componentAnimations = engine.componentAnimations; // Resources const spritesheets = engine.spritesheets; const sounds = engine.sounds; // Configuration const globalConfig = engine.globalConfig; // Particle settings const particleSettings = engine.particleSettings; ``` ## Entity Property Signals Use `getEntityProp()` to read player or event properties through a single typed API. It returns a signal, so you can read it with `()` and compose it in `computed()` or `effect()`. ```javascript theme={null} import { getEntityProp } from "@rpgjs/client"; import { computed } from "canvasengine"; import { inject } from "@rpgjs/client"; import { RpgClientEngine } from "@rpgjs/client"; const engine = inject(RpgClientEngine); const currentPlayer = engine.scene.currentPlayer; const level = getEntityProp(currentPlayer, "level"); const maxHp = getEntityProp(currentPlayer, "params.maxHp"); const hp = getEntityProp(currentPlayer, "hp"); const hpPercent = computed(() => (hp() / (maxHp() || 1)) * 100); ``` You can also use it with events: ```javascript theme={null} const someEvent = engine.scene.events()[eventId]; const eventName = getEntityProp(someEvent, "name"); ``` ## Signal Synchronization The signals are automatically synchronized with the server through the RPGJS synchronization layer: * **Server Changes**: When the server updates player positions, event states, or other data * **WebSocket Events**: Changes are sent via WebSocket `sync` events * **Automatic Updates**: The `load()` function updates the signals with new data * **Component Re-rendering**: Canvas Engine components automatically re-render when signals change This creates a seamless real-time experience where your UI components stay synchronized with the game state without manual intervention. ## Best Practices 1. **Import Path**: Always use the correct relative path for imports based on your file location 2. **Signal Access**: Access signals directly - they will automatically update your component 3. **Performance**: Signals are optimized for performance and only trigger updates when data actually changes 4. **Type Safety**: Use TypeScript for better development experience with proper typing 5. **GUI Dependencies**: Use the dependency system to ensure GUIs only appear when appropriate data is available 6. **Memory Management**: The system automatically handles subscription cleanup, but be mindful of creating custom subscriptions ## Common Use Cases * **Player Lists**: Display all connected players * **Event Interaction**: Show interactive events on the map * **Real-time Updates**: Automatically update UI when game state changes * **Character Movement**: Track and display character positions * **Game Effects**: Access and display visual effects * **Conditional GUIs**: Show/hide interfaces based on game state * **Loading States**: Display GUIs only when required data is available * **Progressive Enhancement**: Build interfaces that enhance as more data becomes available # HUD Source: https://v5.rpgjs.dev/gui/hud Guide for HUD in RPGJS. # HUD The HUD (heads-up display) is a built-in GUI component that shows the player's level, HP, and SP. It can optionally display a faceset portrait using `DOMSprite`. ## Basic Usage Register the HUD in your client module and auto-display it when the current player is ready: ```typescript theme={null} import { defineModule, RpgClient, RpgClientEngine } from '@rpgjs/client'; import { inject } from '@rpgjs/common'; import { HudComponent } from '@rpgjs/client'; export default defineModule({ gui: [ { id: 'hud', component: HudComponent, autoDisplay: true, data: { faceset: { id: 'facesetId', expression: 'happy' } }, dependencies: () => { const engine = inject(RpgClientEngine); return [engine.scene.currentPlayer]; } } ] }); ``` You can also use this exact auto-display config in your client module: ```typescript theme={null} { id: "hud", component: HudComponent, autoDisplay: true, dependencies: () => { const engine = inject(RpgClientEngine) return [engine.scene.currentPlayer] } } ``` The HUD stays hidden until the player has entered a map, and while the title screen (`PrebuiltGui.TitleScreen`) is displayed. It remains visible during map transfers. ## Faceset (Optional) The HUD can render a face portrait from a faceset spritesheet. The faceset data is provided in the GUI data, and the portrait is only displayed when `faceset` is defined. ### 1) Provide a faceset spritesheet ```typescript theme={null} import { Presets } from '@rpgjs/client'; spritesheetResolver: async (id: string) => { if (id === 'facesetId') { return Presets.FacesetPreset({ id: 'facesetId', image: 'faceset.png', width: 1024, height: 1024, }, 3, 4, { happy: [0, 0], sad: [1, 0], }); } return undefined; } ``` ### 2) Pass faceset data to the HUD ```typescript theme={null} import { inject } from '@rpgjs/common'; import { RpgGui } from '@rpgjs/client'; const gui = inject(RpgGui); gui.display('hud', { faceset: { id: 'facesetId', expression: 'happy' } }); ``` If you already auto-display the HUD, you can update the faceset later: ```typescript theme={null} gui.display('hud', { faceset: { id: 'facesetId', expression: 'happy' } }); ``` Notes: * If `faceset.expression` is missing, the HUD uses `"default"`. * If `faceset` is not provided, no portrait area is rendered. # GUI overview Source: https://v5.rpgjs.dev/gui/index Choose the right RPGJS GUI pattern for screens, HUDs, dialogs, and sprite-attached interfaces. # GUI overview GUI pages cover interfaces that open, close, receive user input, and exchange actions with the game. They are separate from sprite rendering components. Use GUI when the player interacts with an interface, when the server opens a menu or dialog, or when a screen-level element should live above the game canvas. ## Choose a GUI pattern | Need | Use | | - | - | | Change the visual theme or reuse CSS elements | [Customize GUI themes](/gui/theming) | | Show a start screen before the player enters the game | [Title Screen](/gui/title-screen) | | Authenticate an MMORPG account before connecting | [MMORPG Account](/gui/account) | | Let the player choose a server-owned Actor | [Character Select](/gui/character-select) | | Display conversations or narrative text | [Dialog Box](/gui/dialog-box) | | Show persistent game information on screen | [HUD](/gui/hud) | | Replace a prebuilt GUI with your own component | [Prebuilt GUI Contracts](/gui/prebuilt-contracts) | | Add authoritative map or global chat | [Chat module](/gui/chat) | | Attach an interactive interface to a sprite | [Attach GUI to Sprites](/guide/gui/attach-gui) | | Apply client-side feedback before the server confirms an action | [Optimistic GUI Actions](/gui/optimistic-actions) | | Inject engine services inside `.ce` GUI files | [Engine Injection in .ce Files](/gui/engine-injection) | | Build GUI with Vue components | [Vue.js integration](/gui/vue-integration) | ## GUI or component? Use [Components overview](/guide/components-overview) when you only need a passive visual layer around sprites. Use GUI when the element behaves like an interface: it can be opened, hidden, updated with GUI data, or can send interactions back to the server. GUI IDs and their data/interaction contracts are renderer-neutral. CanvasEngine is the default renderer for `.ce` components; Vue registrations explicitly opt into the DOM overlay with `vueGui()`. ## Attached GUI An attached GUI follows a sprite in the map, but it still belongs to the GUI system. This makes it useful for tooltips, contextual menus, and interactive widgets that need GUI lifecycle methods. For passive labels or bars around sprites, prefer [Sprite Components](/guide/sprite-components) or [Authoritative Sprite Components](/guide/component). # Mobile Source: https://v5.rpgjs.dev/gui/mobile # Mobile Controls RPGJS ships a CanvasEngine mobile overlay through `withMobile()`. It renders a virtual joystick and action buttons over the game canvas, and forwards touches to the same controls used by keyboard and gamepad input. ## Enable Mobile Controls Add `withMobile()` to your client modules: ```ts theme={null} import { provideClientModules, withMobile } from '@rpgjs/client' export default { providers: [ provideClientModules([ withMobile() ]) ] } ``` By default, the overlay is displayed only on mobile user agents and waits until the current player controls are available. ## Configure the Overlay Use `enabled: "always"` while testing from a desktop browser: ```ts theme={null} withMobile({ enabled: 'always', joystick: { outerColor: '#d7e7ff', innerColor: '#ffffff', scale: 0.82, moveInterval: 40, threshold: 0.15 }, buttons: { action: true, back: true, dash: true } }) ``` Available options: | Option | Description | | - | - | | `id` | GUI id. Defaults to `mobile-gui`. | | `enabled` | `"auto"`, `"always"`, `"never"`, or a predicate. Defaults to `"auto"`. | | `layout` | Positions the overlay. Use `joystickSide`, `margin`, `buttonsMargin`, `joystickMargin`, and `gap` to tune placement. | | `components` | Replaces the default CanvasEngine joystick or button components. | | `joystick` | Set to `false` to hide it, or pass colors, scale, and movement settings. `outerScale` and `innerScale` are available when using custom joystick sprites. | | `buttons.action` | Shows the A button and triggers the `action` control. | | `buttons.back` | Shows the B button and triggers the `back` control. | | `buttons.dash` | Shows the D button and triggers the `dash` control. | ## Customize Components The default overlay is built with CanvasEngine components. You can replace the joystick, individual buttons, or both. Custom components receive the same control props as the default component plus `defaultProps`, so they can wrap CanvasEngine `Joystick` and `Button` without reimplementing RPGJS input behavior. ```ts theme={null} import { provideClientModules, withMobile } from '@rpgjs/client' import MobileButton from './components/mobile-button.ce' import MobileJoystick from './components/mobile-joystick.ce' export default { providers: [ provideClientModules([ withMobile({ layout: { joystickSide: 'right', joystickMargin: [30, 68, 30, 30], gap: 16 }, components: { joystick: MobileJoystick, buttons: { action: MobileButton, dash: MobileButton } }, buttons: { action: { enabled: true, width: 70, height: 70 }, back: false, dash: { enabled: true, width: 58, height: 58 } } }) ]) ] } ``` For one-off tweaks, pass CanvasEngine props directly on `joystick` or a button option. For larger visual changes, prefer `components` and wrap the default props inside your `.ce` component. ## Runtime Behavior Mobile controls are client-side input helpers. In standalone mode and MMORPG mode they send the same movement and action inputs as keyboard/gamepad controls; they do not create gameplay authority on the client. The virtual joystick uses CanvasEngine's `Joystick` component for input. RPGJS movement is cardinal, so diagonal joystick positions are resolved to the nearest single RPGJS movement control before being repeated. Holding the joystick continues movement without requiring further pointer movement. Changing direction preserves the current joystick power. Releasing the joystick or returning it below the configured threshold stops repetition. In MMORPG mode, the overlay follows replacement current-player controls so a player remount does not require releasing and pressing the joystick again. # Optimistic GUI Actions Source: https://v5.rpgjs.dev/gui/optimistic-actions Guide for Optimistic GUI Actions in RPGJS. # Optimistic GUI Actions This architecture makes GUI actions feel instant on the client, while still reconciling with server state. It is designed to be generic (items, skills, shop, future GUIs) and to work with existing `gui.open` flows. ## Goals * UI updates immediately after user input (no waiting for round-trip). * Server remains authoritative. * Client reconciles when server sends updated data. ## How It Works ### 1) Client action pipeline When a GUI component calls `onInteraction(name, data)`, the client: 1. Generates a `clientActionId`. 2. Applies an optimistic reducer (if registered for the GUI). 3. Sends the action to the server via `gui.interaction` with `clientActionId`. ### 2) Server reconcile On the server, after processing an action, the GUI sends a **snapshot update**: ``` gui.update({ guiId, data, clientActionId }) ``` The client: * Replaces GUI data with the server snapshot. * Removes the matching pending action (if `clientActionId` exists). * Replays remaining pending actions to keep the UI responsive. ## Client API (RpgGui) Register optimistic reducers per GUI: ```ts theme={null} gui.registerOptimisticReducer("rpg-main-menu", (data, action) => { if (action.name === "useItem") { // return next data } return data; }); ``` Reducers are pure functions that return a new data object (or the original one if no change). ## Server API (Gui) Use `update()` to reconcile from the server: ```ts theme={null} gui.update(data, { clientActionId }); ``` This is called after any action that changes GUI state (items, skills, shop, etc.). ## Built-In Example The Main Menu already uses optimistic updates for: * `useItem` * `equipItem` The server responds with a `gui.update` snapshot after each action to reconcile the local state. ## Extending the System To add a new optimistic action: 1. Register a reducer for the GUI id. 2. Update GUI data immutably in the reducer. 3. Call `gui.update()` on the server after processing. This keeps instant UX on the client and authoritative state on the server. # Prebuilt GUI Contracts Source: https://v5.rpgjs.dev/gui/prebuilt-contracts Data and interaction contracts for replacing RPGJS prebuilt GUI components. # Prebuilt GUI Contracts RPGJS prebuilt GUIs are regular GUI entries registered with fixed IDs. You can replace their visual implementation with your own CanvasEngine component by registering a GUI with the same ID. The server API stays the same. For example, `player.showText()` still opens `rpg-dialog`, but your component receives the dialog data and decides how to render it. ## Register a Replacement Create a `.ce` file and register it with the prebuilt ID you want to replace: ```ts theme={null} import { provideClientModules } from '@rpgjs/client' import { PrebuiltGui } from '@rpgjs/common' import MyDialog from './gui/my-dialog.ce' export default { providers: [ provideClientModules([ { gui: [ { id: PrebuiltGui.Dialog, component: MyDialog, renderer: 'canvas', }, ], }, ]), ], } ``` The last component registered for an ID wins. A CanvasEngine replacement removes any Vue entry with the same ID, and a Vue replacement removes the built-in CanvasEngine component. Declare `renderer: 'canvas'` for `.ce` registrations; registrations without a renderer still use legacy component-shape detection for compatibility. This page focuses on CanvasEngine `.ce` replacements; for Vue-specific examples, see [Vue.js integration](/gui/vue-integration). ## Component Interface CanvasEngine GUI components receive: | Prop | Description | | - | - | | `data` | Signal containing the object sent by the server. Read it with `data()`. | | `onFinish(value?)` | Closes the GUI and resolves the server-side promise when the GUI was opened with `waitingAction`. | | `onInteraction(name, payload?)` | Sends an action to the server without closing the GUI. | For example, this CanvasEngine component can replace the built-in dialog box: ```html theme={null}
@if (speaker()) { {speaker()} }

{message()}

@if (choices().length) {
@for ((choice, index) of choices()) { }
} @else { }
``` Use `onFinish()` when the server is waiting for a final answer, such as a dialog choice. Use `onInteraction()` for actions that should keep the GUI open, such as buying an item or equipping gear. ## Character Select | Contract | Value | | - | - | | ID | `PrebuiltGui.CharacterSelect` / `rpg-character-select` | | Server API | `player.showCharacterSelect(actors, options)` | | Select with | `onInteraction('select', { id })` | | Cancel with | `onInteraction('cancel')` when `allowCancel` is true | Data contains `actors`, `title`, `subtitle`, `selectedActorId`, and `allowCancel`. Each Actor presentation contains `id`, optional `name`, optional `description`, and optional `graphic` and `faceset` spritesheet IDs. A custom renderer must return an Actor ID from this list; the server ignores unknown IDs and keeps the GUI open. The promise resolves to the original server-owned Actor, not the client payload, and never calls `setActor()` implicitly. ## Input | Contract | Value | | - | - | | ID | `PrebuiltGui.Input` / `rpg-input` | | Server API | `player.showInput(message, options)` | | Submit with | `onInteraction('submit', { value })` | | Cancel with | `onInteraction('cancel')` | The prebuilt input GUI renders either an HTML input or textarea. Server-owned validation keeps the GUI open when a submitted value is invalid. Projects can replace the component by registering another GUI with the same ID, but the replacement must use the interactions above so validation still runs on the authoritative server. Data: | Field | Type | Description | | - | - | - | | `message` | `string` | Label or question displayed above the field. | | `control` | `'input'` or `'textarea'` | HTML control to render. | | `type` | `'text'`, `'number'`, `'password'`, or `'email'` | Input and result type. Textareas always use text. | | `defaultValue` | `string` or `number` | Initial field value. | | `placeholder` | `string` | Optional placeholder. | | `required` | `boolean` | Whether an empty submission is invalid. | | `minLength`, `maxLength` | `number` | Text length constraints. | | `min`, `max`, `step` | `number` | Numeric constraints. | | `rows` | `number` | Visible textarea rows. | | `confirmText`, `cancelText` | `string` | Button labels. | | `cancelButton` | `boolean` | Whether the Cancel button is displayed. Defaults to `true`. | | `errorKey`, `errorParams` | `string`, `object` | Latest server validation message, resolved through the project i18n service. | ```ts theme={null} const age = await player.showInput('Your age', { type: 'number', required: true, min: 1, }) // number | null const biography = await player.showInput('Biography', { control: 'textarea', rows: 6, maxLength: 500, }) // string | null ``` An optional empty number and an explicit cancellation both resolve to `null`. Text, password, email, and textarea inputs resolve to strings. Default button labels and validation errors use `rpg.input.*` translation keys. Game-level i18n messages can override them; explicit `confirmText` and `cancelText` options take precedence for one form. ## Dialog Box | Contract | Value | | - | - | | ID | `PrebuiltGui.Dialog` / `rpg-dialog` | | Server APIs | `player.showText()`, `player.showChoices()` | | Close with | `onFinish(index?)` | Data: | Field | Type | Description | | - | - | - | | `message` | `string` | Text to display. | | `choices` | `{ text: string }[]` | Choice labels. The original server-side `value` is not sent to the client. | | `position` | `'top'`, `'middle'`, or `'bottom'` | Dialog placement. Defaults to `bottom`. | | `fullWidth` | `boolean` | Whether the dialog should use the full screen width. | | `autoClose` | `boolean` | Whether the dialog may close automatically. | | `typewriterEffect` | `boolean` | Whether the text should reveal progressively. | | `speaker` | `string` | Speaker label. | | `face` | `{ id: string; expression?: string }` | Faceset spritesheet ID and expression. | | `input` | `InputFormData` | Optional typed input displayed after the text. Mutually exclusive with `choices`. | To return a choice, call `onFinish(index)` where `index` is the selected choice index. For text without choices, call `onFinish()` when the player dismisses the dialog. For a dialog input, use `onInteraction('submit', { value })` and `onInteraction('cancel')`. This keeps parsing and validation on the server and allows invalid submissions to update `input.errorKey` without closing the GUI. CanvasEngine example: ```html theme={null}

{data().message}

@for ((choice, index) of data().choices || []) { } @if (!(data().choices || []).length) { }
``` ## Hotbar | Contract | Value | | - | - | | ID | `PrebuiltGui.Hotbar` / `rpg-hotbar` | | Server APIs | `player.showHotbar(options)`, `player.hideHotbar()` | | Keep open with | `onInteraction(name, payload)` | Data: | Field | Type | Description | | - | - | - | | `hotbar` | `HotbarState` | Persistent version, initialization state, capacity, active slot, and ten serialized assignments. | | `capacity` | `number` | Number of currently accessible slots, from 1 to 10. | | `activeSlot` | `number \| null` | Zero-based selected slot. | | `slots` | `HotbarDisplaySlot[]` | Ten resolved presentation slots. | | `feedback` | `HotbarActionFeedback` | Optional transient `used`, `selected`, or `rejected` animation state. | Each presentation slot contains `index`, `type`, `entry`, `name`, `description`, `icon`, `quantity`, `cost`, `badge`, `usable`, `cooldownMs`, `readyAt`, `activation`, `locked`, and `lockedHint` when applicable. Empty slots use `type: "empty"` and `entry: null`. Interactions: | Interaction | Payload | Server behavior | | - | - | - | | `selectSlot` | `{ slot }` | Validates capacity and persists the active slot. | | `useSlot` | `{ slot, target? }` | Selects and authoritatively uses the current entry. | | `useActiveSlot` | `{ target? }` | Uses the selected slot when one exists. | | `refresh` | none | Rebuilds presentation, quantities, costs, and cooldowns. | Custom components should render only the received presentation and send these interactions. They must not consume inventory, SP, or apply gameplay effects on the client. For color, spacing, and typography changes, prefer the native [hotbar theme variables](/guide/hotbar#theme-the-native-component) over a replacement. ## Main Menu | Contract | Value | | - | - | | ID | `PrebuiltGui.MainMenu` / `rpg-main-menu` | | Server API | `player.callMainMenu(options)` | | Close with | `onInteraction('exit')` or `onFinish()` | Data: | Field | Type | Description | | - | - | - | | `menus` | `{ id; label; disabled? }[]` | Menu entries. Default IDs are `items`, `skills`, `equip`, `options`, `save`, `exit`. | | `items` | `MenuItem[]` | Inventory entries with `id`, `name`, `description`, `quantity`, `icon`, stats, `type`, `usable`, `equipped`. | | `equips` | `MenuItem[]` | Inventory entries whose type is `weapon` or `armor`. | | `skills` | `{ id; name; description; spCost }[]` | Player skills. | | `saveLoad` | `object` | Save overlay data, usually `{ mode: 'save', canSave, showAutoSlot, autoSlotIndex, autoSlotLabel }`. | | `playerStats` | `object` | Current stats such as `atk`, `pdef`, `sdef`, `str`, `dex`, `int`, `agi`, `maxHp`, `maxSp`. | | `expForNextlevel` | `number` | EXP target for the next level. | Interactions: | Interaction | Payload | Server behavior | | - | - | - | | `useItem` | `{ id }` | Calls `player.useItem(id)` and refreshes menu data. | | `equipItem` | `{ id, equip }` | Calls `player.equip(id, equip)` and refreshes menu data. | | `openSave` | none | Closes the main menu and opens `rpg-save` in save mode. | | `exit` | none | Closes the menu and resolves with `'exit'`. | The built-in client also applies optimistic updates for `useItem` and `equipItem`. Custom Vue and CanvasEngine replacements can use the same interaction names and still benefit from those reducers. ## Shop | Contract | Value | | - | - | | ID | `PrebuiltGui.Shop` / `rpg-shop` | | Server API | `player.callShop(options)` | | Close with | `onFinish()` | Data: | Field | Type | Description | | - | - | - | | `items` | `ShopItem[]` | Items available to buy. | | `sellItems` | `ShopItem[]` | Player inventory items available to sell. | | `playerParams` | `object` | Current player stats used to compare item modifiers. | | `message` | `string` | Merchant message. | | `face` | `{ id: string; expression?: string }` | Merchant faceset. | `ShopItem` includes `id`, `name`, `description`, `price`, `icon`, `type`, optional `stats`, optional `quantity`, and `equipped`. Interactions: | Interaction | Payload | Server behavior | | - | - | - | | `buyItem` | `{ id, nb }` | Calls `player.buyItem(id, nb)` and refreshes shop data. | | `sellItem` | `{ id, nb }` | Adds gold, removes inventory items, and refreshes shop data. | ## Save and Load | Contract | Value | | - | - | | ID | `PrebuiltGui.Save` / `rpg-save` | | Server APIs | `player.showSave()`, `player.showLoad()`, `player.showSaveLoad()` | | Close with | `onFinish(index?)` | Data: | Field | Type | Description | | | - | - | - | - | | `slots` | \`(SaveSlot | null)\[]\` | Save metadata without the `snapshot` field. Empty slots are `null`. | | `mode` | `'save'` or `'load'` | Selects save or load behavior. | | | `showAutoSlot` | `boolean` | Whether to display an auto-save entry. | | | `autoSlotIndex` | `number` | Slot index used by the auto-save entry. | | | `autoSlotLabel` | `string` | Label for the auto-save entry. | | Server interactions: | Interaction | Payload | Server behavior | | - | - | - | | `save` | `{ index }` | Saves into the slot and closes with the selected index. | | `load` | `{ index }` | Loads the slot and closes with the selected index. | | `select` | `{ index }` | Alias handled according to the current mode. | The built-in `save-load.ce` also uses `SaveClientService` directly for local save/load UI flows. A replacement can use either the server interactions above or its own client service flow, depending on the game architecture. ## Title Screen | Contract | Value | | - | - | | ID | `PrebuiltGui.TitleScreen` / `rpg-title-screen` | | Server API | `player.gui('rpg-title-screen').open(options)` when used from the server | | Close with | `onInteraction('select', selection)` or local hide logic | Data: | Field | Type | Description | | - | - | - | | `entries` | `{ id; label; disabled? }[]` | Selectable actions. Defaults to `start` and `load`. | | `title` | `string` | Main title. | | `subtitle` | `string` | Subtitle. | | `version` | `string` | Version label. | | `showPressStart` | `boolean` | Option reserved by the server contract. | | `saveLoad` | `object` | Data passed to `rpg-save` when using local load behavior. | | `localActions` | `boolean` | If true, built-in title actions hide the title or open save/load locally. | Interaction: | Interaction | Payload | | - | - | | `select` | `{ id, index, entry }` | ## Game Over | Contract | Value | | - | - | | ID | `PrebuiltGui.Gameover` / `rpg-gameover` | | Server API | `player.callGameover(options)` | | Close with | `onInteraction('select', selection)` or local hide logic | Data: | Field | Type | Description | | - | - | - | | `entries` | `{ id; label; disabled? }[]` | Selectable actions. Defaults to `title` and `load`. | | `title` | `string` | Main title. | | `subtitle` | `string` | Subtitle. | | `saveLoad` | `object` | Data passed to `rpg-save` when using local load behavior. | | `localActions` | `boolean` | If true, built-in actions display the title screen or save/load locally. | Interaction: | Interaction | Payload | | - | - | | `select` | `{ id, index, entry }` | ## HUD and Notifications `HudComponent` and `NotificationComponent` are also GUI components, but they are less driven by a server payload than the modal GUIs above. HUD data: | Field | Type | Description | | - | - | - | | `faceset` | `{ id: string; expression?: string }` | Optional face displayed next to player status. | The HUD reads the current player from the client engine for `hp`, `sp`, max values, and level. Notifications use `PrebuiltGui.Notification` / `rpg-notification`, but the built-in component reads `engine.notificationManager.stack()` instead of a direct `data` payload. Custom notification UIs should use the same notification manager if they need the existing `player.showNotification()` behavior. # Customize GUI themes Source: https://v5.rpgjs.dev/gui/theming Style RPGJS primitives and complete game interfaces with CSS variables, scoped themes and Storybook. # Customize GUI themes `@rpgjs/ui-css` styles DOM interfaces independently of their renderer. Use it with HTML, CanvasEngine `DOMContainer` components or Vue. Theme changes run on the client in both standalone RPG and MMORPG modes; they do not change GUI IDs, server data or gameplay authority. Browse [the GUI catalogue](https://rsamaium.github.io/RPG-JS/) for foundations, interactive elements and complete compositions. The catalogue is published from `v5`; contributors can run it locally with `pnpm --filter @rpgjs/ui-css storybook`. ## Load a theme Load the base stylesheet first, a theme second and your overrides last: ```css theme={null} @import "@rpgjs/ui-css/index.css"; @import "@rpgjs/ui-css/theme-default.css"; @import "./my-theme.css"; ``` The default theme uses navy surfaces, silver borders, cyan accents and gold selection. No font service or remote artwork is required. Replace the second import with `theme-pixel.css` for the pixel theme. The existing `index.css` includes the legacy game-page reset (including body overflow). For an embedded widget on a normal webpage, import `@rpgjs/ui-css/components.css` instead: it includes tokens and components without resetting the page. Add your own box sizing within the widget. ## Use elements and compositions ```html theme={null}

Crystal sword

A blade carrying the light of the northern stars.

``` The text above is example game content. In a real RPGJS component, provide labels through [i18n](/guide/i18n). Bind actions through the existing [GUI contracts](/gui/prebuilt-contracts), rather than adding gameplay behavior to the CSS library. Use the same classes inside a CanvasEngine `` or a Vue template. Register the GUI normally; CSS does not replace registration or input handling. For interactive HTML, prefer native buttons and fields. ### Button contract | Attribute | Values / behavior | | - | - | | `data-variant` | `primary`, `secondary` (default), `ghost`, `danger`, `success`, `warning` | | `data-size` | `small`, `medium` (default), `large` | | `aria-pressed="true"` | Persistent selected/toggle appearance | | `disabled` | Native disabled behavior and muted appearance | | `aria-busy="true"` | Loading indicator; also use `disabled` to prevent activation | CSS does not disable a non-native element. If you use `aria-disabled`, your component must block its click and keyboard handlers. Keep accessible labels on icon-only buttons and preserve visible keyboard focus. Put an inline SVG before the label for an icon; no icon font is required. ### Layout helpers Use `rpg-ui-stack` for vertical groups, `rpg-ui-row` for wrapping horizontal groups, `rpg-ui-grid` for responsive panels and `rpg-ui-inventory-grid` for slots. Complete built-in screens keep their existing semantic classes and responsive layouts. Storybook compositions are presentation fixtures, not a server simulator. ## Customize globally or locally Override colors globally in `:root`. To limit changes to one game or widget, put a theme boundary on an ancestor: ```html theme={null}
``` ```css theme={null} [data-rpg-theme="forest"] { --rpg-ui-bg: #101e18; --rpg-ui-surface: #1b3328; --rpg-ui-surface-light: #2a4736; --rpg-ui-surface-overlay: #1b3328; --rpg-ui-surface-overlay-strong: #14271e; --rpg-ui-border: #729c79; --rpg-ui-border-light: #b5d5aa; --rpg-ui-border-dark: #09110b; --rpg-ui-text: #eef4df; --rpg-ui-text-muted: #b8cbb4; --rpg-ui-accent: #a4db9a; --rpg-ui-accent-hover: #d5f2c7; --rpg-ui-warning: #e7c89a; --rpg-ui-gradient-surface: linear-gradient( var(--rpg-ui-surface-light), var(--rpg-ui-surface) ); --rpg-ui-radius-sm: 16px; --rpg-ui-radius-md: 20px; --rpg-ui-radius-lg: 24px; --rpg-ui-ornament-opacity: 0; --rpg-ui-shadow-inset: none; --rpg-ui-shadow-sm: none; --rpg-ui-shadow-lg: none; --rpg-ui-backdrop-blur: none; } ``` A theme boundary resets base tokens and resolves semantic aliases locally. Declare the palette on that same boundary and load its rules after the theme imports. A nested boundary is a new theme, not a partial inheritance of its parent. The built-in boundary classes are `rpg-ui-theme-default` and `rpg-ui-theme-pixel`. Both theme imports may coexist: the last import controls the global default, while each explicit class selects its own local palette. Use one boundary per subtree. Storybook's **Theme isolation** story demonstrates all three themes simultaneously. ## Token reference Palette tokens describe values. Semantic tokens describe roles. Component tokens let you change one family without restyling every screen. | Token / family | Default crystal value or role | | - | - | | `--rpg-ui-bg`, `--rpg-ui-surface`, `--rpg-ui-surface-light` | `#080f21`, `#101d35`, `#233c60` | | `--rpg-ui-text`, `--rpg-ui-text-muted` | `#eef5ff`, `#b5c7df` | | `--rpg-ui-accent`, `--rpg-ui-selection` | Cyan `#82d7ff`; selection follows warning/gold | | `--rpg-ui-border`, `--rpg-ui-border-light` | `#6483ac`, `#c5def5` | | `--rpg-ui-danger`, `--rpg-ui-success`, `--rpg-ui-warning` | Error, success and reward colors | | `--rpg-ui-font`, `--rpg-ui-font-display` | System sans-serif body; Georgia headings | | `--rpg-ui-radius-sm/md/lg` | `5px`, `8px`, `12px` | | `--rpg-ui-border-width` | `1px` | | `--rpg-ui-spacing-sm/spacing/spacing-lg/spacing-xl` | `8px`, `12px`, `24px`, `32px` | | `--rpg-ui-gradient-surface` | Surface material, shared by controls and panels | | `--rpg-ui-panel-background`, `--rpg-ui-panel-shadow` | Surface gradient; inset highlight plus large shadow | | `--rpg-ui-control-background/border/shadow` | Button surface, edge and relief | | `--rpg-ui-control-height` | `44px` minimum touch target | | `--rpg-ui-control-padding` | `12px 22px` | | `--rpg-ui-control-radius` | Small radius | | `--rpg-ui-control-tracking` | `.08em` label spacing | | `--rpg-ui-selected-background` | Selection color mixed into the background | | `--rpg-ui-ornament-opacity` | `.5`; set `0` to remove decorative inner borders | | `--rpg-ui-motion-duration` | `140ms`; reduced-motion preferences disable effects | | `--rpg-ui-disabled-opacity` | `.48` | | `--rpg-ui-focus-color/width/offset` | Focus outline; `2px` width and offset | | `--rpg-ui-health-gradient/mana-gradient/xp-gradient` | Resource bar colors | The source `tokens.css` and `src/semantic.css` contain the full token sets, including existing HUD, chat and shop dimensions. [Hotbar customization](/guide/hotbar) retains its `--rpg-hotbar-*` API; semantic defaults now connect it to the active theme. ### Common recipes ```css theme={null} /* Add these after the theme imports. */ :root { /* Change the accent and selection independently. */ --rpg-ui-accent: #c5a8ff; --rpg-ui-selection: #f3cd87; /* Replace typography with your own locally loaded font. */ --rpg-ui-font: "My Game Font", system-ui, sans-serif; --rpg-ui-font-display: "My Game Font", Georgia, serif; } /* Make this screen more compact while preserving touch target height. */ .compact-menu { --rpg-ui-spacing: 8px; --rpg-ui-spacing-lg: 16px; --rpg-ui-control-padding: 8px 14px; } /* Square buttons and flat panels. */ .square-ui { --rpg-ui-control-radius: 0px; --rpg-ui-panel-background: var(--rpg-ui-surface); --rpg-ui-panel-shadow: none; --rpg-ui-ornament-opacity: 0; } ``` Component variants can override their own tokens. For example, customize primary buttons through `.rpg-ui-btn[data-variant="primary"]` after the library imports if their surface must differ from the theme's accent treatment. ## Canvas mobile controls The native [mobile controls](/gui/mobile) draw with CanvasEngine rather than DOM CSS. They read surface, background, text, accent, border and danger colors from `#rpg` (or the document root when unavailable). Changes to ancestor classes, inline styles or `data-rpg-theme` refresh these colors while mounted. Load stylesheets before mounting; stylesheet replacement alone is not observed. Explicit `withMobile({ buttons: ..., joystick: ... })` props remain authoritative. Use these props or custom CanvasEngine components for textures and specialized shapes. CSS panel shadows and gradients do not automatically become canvas effects. ## Migration and verification Existing imports and class names remain supported. The appearance of `theme-default.css` intentionally changes; the old Fredoka font is no longer required. Remove redundant overrides that were specific to the old glass theme. Check every theme with long translated text, empty inventories, unavailable actions, keyboard focus and narrow viewports. Keep portraits and item artwork in your game's asset library: the mockup's illustrations are not bundled UI assets. # Title Screen Source: https://v5.rpgjs.dev/gui/title-screen Guide for Title Screen in RPGJS. # Title Screen The "title screen" GUI provides a start screen with an action list (for example: Start, Load, etc.). The list is defined on the client by default and can be overridden by the server. ## Client-side usage (autoDisplay) Load the GUI on the client to display it without the server. Add it in the client config under `gui` with `autoDisplay: true`. ```ts theme={null} import { TitleScreenComponent } from '@rpgjs/client' export default { providers: [ provideClientModules([ { gui: [ { id: 'rpg-title-screen', component: TitleScreenComponent, autoDisplay: true, data: { title: 'Chronicles', subtitle: 'of the Ancients', version: 'v1.0.0', localActions: true, saveLoad: { mode: 'load', slots: [null, null, null] }, entries: [ { id: 'start', label: 'Start' }, { id: 'load', label: 'Load' }, { id: 'credits', label: 'Credits', disabled: true } ] } } ] } ]) ] } ``` The component emits a `select` interaction with `{ id, index, entry }`. When `localActions: true`: * selecting `start` hides the title screen * selecting `load` hides the title screen and displays the save/load GUI with `saveLoad` ## Client-side default list Si le serveur ne fournit pas `entries`, la liste par defaut du client est: ```ts theme={null} [ { id: 'start', label: 'Start' }, { id: 'load', label: 'Load' } ] ``` To replace the title screen completely, register your own GUI with the `rpg-title-screen` ID. The data and interaction contract is documented in [Prebuilt GUI Contracts](/gui/prebuilt-contracts). For example, a custom component should emit a `select` interaction with: ```ts theme={null} onInteraction('select', { id: entry.id, index, entry }) ``` ## Style Les classes CSS sont dans: * `packages/ui-css/src/primitives/title-screen.css` Vous pouvez changer la typo, le fond, ou les animations directement dans ce fichier. # Vue.js 3 Integration with RPGJS GUI System Source: https://v5.rpgjs.dev/gui/vue-integration Use Vue.js 3 components as RPGJS GUI components. # Vue.js 3 Integration with RPGJS GUI System `@rpgjs/vue` lets RPGJS v5 projects use Vue 3 components for GUI overlays while keeping the regular server API: ```ts theme={null} await player.gui('inventory').open({ items, gold }) player.gui('inventory').on('use-item', ({ itemId }) => { player.useItem(itemId) }) ``` CanvasEngine `.ce` GUI components still render inside the canvas. Vue GUI components render in a DOM overlay above the canvas. ## Setup Install Vue support: ```bash theme={null} npm install @rpgjs/vue vue ``` Enable Vue in Vite when importing `.vue` files: ```ts theme={null} import vue from '@vitejs/plugin-vue' export default { plugins: [ vue(), // RPGJS Vite plugins... ], } ``` Register the Vue GUI provider on the client: ```ts theme={null} import { provideClientModules } from '@rpgjs/client' import { provideVueGui, vueGui } from '@rpgjs/vue' import Inventory from './gui/inventory.vue' export default { providers: [ provideVueGui({ selector: '#vue-gui-overlay', createIfNotFound: true, }), provideClientModules([ { gui: [ vueGui({ id: 'inventory', component: Inventory, }), ], }, ]), ], } ``` `provideVueGui()` creates `#vue-gui-overlay` inside `#rpg` when the element does not exist and `createIfNotFound` is enabled. ## Replace Prebuilt GUI You can replace a prebuilt CanvasEngine GUI with a Vue component by registering a Vue GUI with the same ID. Server APIs keep using the same GUI ID, but the rendered component comes from Vue: ```ts theme={null} import { provideClientModules } from '@rpgjs/client' import { PrebuiltGui } from '@rpgjs/common' import { provideVueGui, vueGui } from '@rpgjs/vue' import Dialog from './gui/dialog.vue' export default { providers: [ provideVueGui({ selector: '#vue-gui-overlay', createIfNotFound: true, }), provideClientModules([ { gui: [ vueGui({ id: PrebuiltGui.Dialog, component: Dialog, }), ], }, ]), ], } ``` `vueGui()` marks the registration with `renderer: 'vue'`. Explicit renderer selection is recommended; component-shape detection remains available only for v4 compatibility. With this setup, `player.showText()` and `player.gui(PrebuiltGui.Dialog).open(data)` open the Vue dialog instead of the built-in CanvasEngine dialog. The same pattern works for other prebuilt IDs such as `PrebuiltGui.MainMenu`, `PrebuiltGui.Shop`, `PrebuiltGui.Save`, `PrebuiltGui.TitleScreen`, and `PrebuiltGui.Gameover`. Each prebuilt GUI has a defined data and interaction contract. See [Prebuilt GUI Contracts](/gui/prebuilt-contracts) for the full list of fields, actions, and expected close values. Use the regular Vue injections to close the GUI or send actions back to the server. For dialog boxes, closing with the selected choice index resolves `player.showChoices()`: ```vue theme={null} ``` ## Vue Components Data passed through `player.gui(id).open(data)` becomes Vue props: ```vue theme={null} ``` Use `v-propagate` when mouse, pointer, and wheel events should also be forwarded to the game canvas. ## Attached Vue GUI Vue GUI components can follow sprites or events from the DOM overlay. Configure them with `attachToSprite: true`: ```ts theme={null} import Tooltip from './gui/tooltip.vue' { gui: [ vueGui({ id: 'player-tooltip', component: Tooltip, attachToSprite: true, }), ], } ``` Then control visibility from the server: ```ts theme={null} player.showAttachedGui() player.hideAttachedGui() ``` The Vue component receives `object` and `spriteData` props: ```vue theme={null} ``` `rpgAttachToSprite: true` is supported for RPGJS v4 compatibility. Prefer `attachToSprite: true` in v5 GUI configuration. ## Injections Vue components can inject: | Injection | Description | | - | - | | `engine` / `rpgEngine` | `RpgClientEngine` instance | | `socket` / `rpgSocket` | WebSocket access | | `gui` / `rpgGui` | `RpgGui` service | | `rpgScene` | Function returning the current client scene | | `rpgStage` | PIXI stage when available | | `rpgResource` | `{ spritesheets, sounds }` | | `rpgObjects` | Observable of scene players and events | | `rpgCurrentPlayer` | Observable of the current player | | `rpgGuiClose` | Close a GUI and notify the server | | `rpgGuiInteraction` | Send a GUI action to the server | | `rpgKeypress` | Observable of mapped keypresses | | `rpgSound` | Sound helper with `get(id)` and `play(id)` | ## Vue Directives Use `v-propagate` on an element when events inside a Vue GUI must also be sent to the RPGJS canvas: ```vue theme={null} ``` The directive forwards mouse, pointer, and wheel events from the Vue element to `#rpg canvas`. ## Behavior Notes * Vue and CanvasEngine GUI components share `RpgGui.display()`, `RpgGui.hide()`, `player.gui(id).open()`, and GUI interactions. * CanvasEngine attached GUI components render in `character.ce`. * Vue attached GUI components render in the DOM overlay and are kept out of the CanvasEngine renderer. * Signal dependencies and `autoDisplay` are supported for Vue GUI entries. # Action Battle System Source: https://v5.rpgjs.dev/guide/battle-ai Use the real-time action battle AI system for RPGJS enemies. # Action Battle System Advanced real-time action combat AI system for RPGJS. The AI controller manages **behavior only**. All stats, HP, SP, skills, items, classes, and states are configured with the standard RPGJS API. ## Features * Adventure combat by default: three-hit player combo, charged attack, and dodge invulnerability * Reuses the standard RPGJS HUD and graphic-bound entity components * Impact visual preset with particles, animated combat typography, and camera shake * State machine AI with `Idle`, `Alert`, `Combat`, `Flee`, and `Stunned` * Multiple enemy types: `Aggressive`, `Defensive`, `Ranged`, `Tank`, `Berserker` * Attack patterns: `Melee`, `Combo`, `Charged`, `Zone`, `DashAttack` * Skill support with standard RPGJS skills * Dodge and counter-attack behaviors * Group behavior and waypoint patrols * Knockback driven by weapon configuration * Hook system with `onBeforeHit` and `onAfterHit` ## Installation ```bash theme={null} npm install @rpgjs/action-battle ``` ## Quick Start ```ts theme={null} import { EventMode, ATK, PDEF, MAXHP, type EventDefinition } from "@rpgjs/server"; import { provideActionBattle, BattleAi, EnemyType } from "@rpgjs/action-battle/server"; function GoblinEnemy(): EventDefinition { return { name: "Goblin", onInit() { this.setGraphic("goblin"); this.hp = 80; this.param[MAXHP] = 80; this.param[ATK] = 15; this.param[PDEF] = 5; new BattleAi(this, { enemyType: EnemyType.Aggressive }); } }; } ``` When you build an object-based event for a map, type the factory as `EventDefinition`. The returned object only describes the event behavior. Placement data such as `id`, `x`, and `y` still belongs to the outer `maps[].events` wrapper. ## Factions and targets `BattleAi` can target players, other battle events, every combat entity, hostile factions, or an explicit list of factions. ```ts theme={null} new BattleAi(this, { faction: "monsters", targets: "players" }); ``` Available target selectors: * `players`: target all players. * `events`: target action-battle events. * `all`: target players and action-battle events. * `hostile`: target entities with a different faction. * `["guards", "bandits"]`: target only these factions. * `(context) => boolean`: fully custom target rule. An allied event can share the player faction and attack only monster groups: ```ts theme={null} new BattleAi(this, { faction: "players", targets: ["monsters", "bandits"] }); ``` Two enemy groups can fight each other: ```ts theme={null} new BattleAi(this, { faction: "guards", targets: ["bandits"] }); ``` You can change faction and targets at runtime: ```ts theme={null} const ai = new BattleAi(this, { faction: "guards", targets: ["bandits"] }); ai.setFaction("bandits"); ai.setTargets("hostile"); ``` Players keep the previous behavior by default: their action attack targets action-battle events. To enable PvP or teams, set the player target selector and faction on the server: ```ts theme={null} player.actionBattleFaction = "red-team"; player.actionBattleTargets = ["blue-team"]; ``` ## Enable the module Register the module on the server: ```ts theme={null} import { createServer } from "@rpgjs/server"; import { provideActionBattle } from "@rpgjs/action-battle/server"; export default createServer({ providers: [ provideActionBattle({ animations: { attack: "attack" } }) ] }); ``` ## Adventure player combat Action Battle now uses the `adventure` preset by default. Press the action control for a buffered three-hit combo, hold E and release it for a charged attack, and use the normal dash control (Shift by default) to dodge. Damage, charge duration, cooldowns, and invulnerability remain server-authoritative. Use `preset: "classic"` to retain the previous single-attack behavior and UI: ```ts theme={null} provideActionBattle({ preset: "classic" }); ``` Every Adventure mechanic can be tuned independently: ```ts theme={null} provideActionBattle({ combat: { player: { combo: { bufferMs: 140, resetMs: 700 }, chargedAttack: { control: "e", minChargeMs: 300, maxChargeMs: 900 }, dodge: { durationMs: 180, cooldownMs: 650, invincibilityMs: 220 }, guard: { control: "f", parryWindowMs: 140, guardDamageReduction: 0.65, guardArcDegrees: 120 }, softTargeting: { range: 112, coneDegrees: 110 } } } }); ``` Guard is frontal and server-authoritative. A hit received during the opening parry window is cancelled, staggers the attacker, and opens a short empowered counterattack window. Soft targeting only adjusts the attack facing inside the configured cone; it never moves the player or replaces manual direction. Action Battle does not register another player HUD. Keep using the standard RPGJS `hud.ce` component so HP, SP, face, and level remain consistent with the rest of the game. For enemies, `BattleAi` reuses the regular server `Components.hpBar()` and merges it into `componentsTop`. That native component is positioned from the rendered graphic bounds, not only from the entity hitbox, so it remains above tall or scaled sprites. Generated four-direction Studio spritesheets also exclude transparent frame padding from those bounds. Customize it through `presentation.healthBar`: ```ts theme={null} new BattleAi(this, { presentation: { role: "boss", healthBar: { style: { width: 120, height: 9, fillColor: "#ff355d" }, text: null, layout: { marginTop: 4 } } } }); ``` Set `presentation.healthBar: false` for entities that should not display it. No additional HUD or custom HP-bar component is registered by Action Battle. Mobile games can expose the charged attack through `withMobile({ buttons: { heavy: true } })`; the heavy button dispatches the same authoritative charge start/release controls as the keyboard. ## Studio skills and keyboard shortcuts Skills loaded from RPGJS Studio can define their complete Action Battle presentation and gameplay contract. The client only requests a skill use; the server verifies that the skill is learned, that SP is available, that its cooldown has elapsed, and that the selected target is valid. ```ts theme={null} { id: "fireball", _type: "skill", key: "1", casterAnimation: "mage-cast", animation: "fire-impact", sound: "fire-cast", impactSound: "fire-hit", targeting: { range: 6 }, action: { mode: "projectile", target: "enemy", cooldownMs: 900, visual: { castFx: "magicBurst", trailFx: "torchFire", impactFx: "explosionSmall" }, projectile: { graphic: "fireball", speed: 240, scale: 1.2, rotateToDirection: true } } } ``` `action.visual` controls client-only particle presentation and never changes server gameplay: * `castFx` is a short preset attached to the caster. * `trailFx` is a continuous preset attached to the moving projectile. * `impactFx` is a short preset attached to the target on impact. Each field accepts a CanvasEngine built-in or custom FX preset name. Use `"auto"` to retain Action Battle defaults and `"none"` to disable that phase. The legacy `visual.fx` field remains supported as an impact alias, but `impactFx` takes precedence. Cast and impact FX finish naturally after their last particle disappears; the combat renderer does not impose a fixed timeout. Use finite, non-looping presets for these two phases, and looping presets for projectile trails. This lifecycle runs on the client in both standalone and multiplayer games and does not delay server damage, cooldowns, or subsequent actions. Custom component animations can still use `displayDuration` when an explicit cutoff is intended. In Studio, the active actor's animation overrides take precedence over its linked character spritesheet animations. The server synchronizes the resolved animation references to the client, so predicted attacks and server-triggered casts use the same actor presentation in standalone and multiplayer games. Project animations are only a client fallback before actor data arrives. Skill impact animations remain independent of the actor's casting animation. `key` accepts the same keyboard names as project input controls. If it is omitted, learned skills use the numeric slots `1` through `0`. The Adventure preset reserves its charged-attack and guard keys, so Studio reports `E` and `F` conflicts without blocking the save. For instant skills, a zero range uses the player's configured soft-targeting distance to select a nearby enemy in front of the caster. Positive ranges remain explicit limits. This fallback only applies to targets selected by the server; manually submitted target tiles still obey the configured range. Impact animations play on enemies actually affected by the skill. `targeting.range` is expressed in map tiles. A projectile can override its travel range in pixels; otherwise Action Battle derives it from the targeting range and tile width. When `action.mode` is `projectile` and no custom projectile `type` is supplied, the built-in `action-battle-skill` CanvasEngine renderer displays `projectile.graphic`, applies `scale`, and optionally rotates the graphic along its trajectory. Damage and the impact animation/sound occur only when the authoritative projectile collides. Area masks use `#` for affected tiles and `.` for empty tiles. Studio exposes this as a visual grid and canonicalizes legacy binary masks by treating `1` as affected and `0` as empty. Use `action.target` to select `enemy`, `ally`, `self`, or `any`. Ranged ally skills and area shapes enter the targeting overlay; a single-target enemy projectile can use the soft target in front of the player. `animations` is optional. If you omit it, attacks keep using the default `attack` animation and no extra hurt, death, or skill-cast animation is played. Adventure attacks lock movement and facing through their active frames by default, then allow movement or dodge to cancel recovery. Control locks are leased independently, so a hurt, guard, dodge, or follow-up attack cannot let an older timer restore stale animation or direction flags. ```ts theme={null} provideActionBattle({ attack: { lockMovement: true, lockDurationMs: 350, showPreview: true, previewDurationMs: 180, previewColor: 0xfff3b0, previewAccentColor: 0xffffff } }); ``` Set `lockMovement` to `false` if you want players to keep moving while attacking. The client stops local predicted movement as soon as the action input is pressed and shows a short slash preview by default. Disable `showPreview` when you provide your own client-side attack effect. For precise attack-by-attack control, use the profile's `control` block: ```ts theme={null} provideActionBattle({ attack: { profile: { startupMs: 55, activeMs: 100, recoveryMs: 180, control: { movementLock: "active", directionLock: "active", moveCancelsRecovery: true, dodgeCancelsRecovery: true, inputBufferMs: 160 } } } }); ``` The `impact` visual preset adds CanvasEngine particles, anchored floating damage typography, screen shake, and a short render-only hit-stop. Accessibility controls can disable these independently without changing server combat: ```ts theme={null} provideActionBattle({ visual: "impact", feedback: { hitStop: true, hitStopMs: 32, heavyHitStopMs: 52, parryHitStopMs: 68, flashes: true, screenShake: true, damageNumbers: true } }); ``` ## Recommended composable DX New action-battle configuration is grouped by responsibility: * `combat` owns gameplay rules: player attack profile, damage, knockback, and hit hooks. * `visual` owns temporary combat feedback: sprite animations, flashes, damage text, CanvasEngine effects, and previews. * `ui` owns client components: action bar, targeting overlay, attack preview, and custom GUI or sprite components. * `ai` owns reusable AI behavior functions. * `skills.targeting` owns action targeting metadata for skills. Use the same `visual` preset on the server and client when your project splits configuration by runtime. The server still decides when authoritative hit, hurt, skill, and enemy attack feedback should happen, but it sends one compact action-battle client visual event. The client resolves that visual locally and groups the flash, hit text, sound, component animation, or sprite animation. This keeps gameplay authority on the server while avoiding several visual packets for one combat moment. The client also triggers local input feedback such as the attack preview. ```ts theme={null} // config.server.ts import { createActionBattleVisual, provideActionBattle } from "@rpgjs/action-battle/server"; export default provideActionBattle({ combat: { attack: { lockMovement: true, lockDurationMs: 280, profile: { startupMs: 70, activeMs: 110, recoveryMs: 160, hitPolicy: "oncePerTarget" } }, hooks: { afterHit(result) { console.log(`Damage: ${result.damage}`); } } }, visual: createActionBattleVisual("fx"), ai: { behaviors: { aggressive({ hpPercent }) { return { mode: hpPercent !== null && hpPercent < 0.25 ? "retreat" : "assault", attackCooldown: 850 }; } } } }); ``` ```ts theme={null} // config.client.ts import { createActionBattleUi, createActionBattleVisual, provideActionBattle } from "@rpgjs/action-battle/client"; export default provideActionBattle({ visual: createActionBattleVisual("fx"), ui: createActionBattleUi({ hotbar: false, targeting: true, attackPreview: true }) }); ``` If your game installs action-battle as a combined module, you can import the same helpers from `@rpgjs/action-battle`. ### Visual composition `createActionBattleVisual()` accepts a preset or a map of visual parts. A visual part is a function receiving the current combat context and helper methods. ```ts theme={null} import { createActionBattleVisual } from "@rpgjs/action-battle/server"; const combatVisual = createActionBattleVisual({ attack({ entity }, fx) { fx.graphic(entity, "attack"); }, hit({ target, damage }, fx) { fx.flash(target, { tint: "red", duration: 120 }); fx.damageText(target, `-${damage}`); fx.component(target, "action-battle-hit-fx", { name: "hitSpark", scale: 0.8, zIndex: 1000 }); }, hurt({ target }, fx) { fx.graphic(target, "hurt"); }, preview({ entity }, fx) { fx.preview(entity, { durationMs: 180 }); } }); ``` Available presets: ```ts theme={null} visual: createActionBattleVisual("classic") // sprite animation + flash + hit text visual: createActionBattleVisual("fx") // classic + CanvasEngine Fx hit spark visual: createActionBattleVisual("impact") // contextual typography, particles + camera shake visual: createActionBattleVisual("none") // no built-in visual feedback ``` The `impact` preset selects an official CanvasEngine preset from the combat context: `hitSpark` for normal hits, `slashSpark` for combo finishers, `impactBurst` for charged or critical attacks, `magicBurst` for skills, and `healPulse` for healing. Its world-space damage popup changes font size, color, outline, movement, and duration for the same contexts. The built-in component animations are registered as `action-battle-hit-fx` and `action-battle-damage`. A skill or weapon action can override its impact presentation with serializable metadata: ```ts theme={null} const Fire = { id: "fire", name: "Fire", _type: "skill", action: { target: "enemy", mode: "instant", visual: { fx: "magicBurst", color: "#ffd166", accentColor: "#a62c21", scale: 1.2 } } }; ``` These hints only affect the client presentation. Damage, SP consumption, targeting, and hit resolution remain server-authoritative. You can also call any component animation registered by your own client modules with `fx.component(entity, id, params)`. Action-battle visual composition runs through the general [Client Visuals](/guide/client-visuals) mechanism for server-triggered combat feedback. Use direct server visual APIs for isolated one-off effects, and use `visual` here when you want action-battle to group combat presentation on the client. The legacy `animations` option still works for sprite animation names and temporary graphics. New orchestration should go through `visual`, while `animations` remains useful as the data source used by `fx.graphic(entity, "attack")`, `fx.graphic(entity, "hurt")`, and `fx.graphic(entity, "castSkill")`. ### Composable UI Action Battle uses RPGJS's generic, server-authoritative hotbar. Enable it on the server with `ui.hotbar`; targeting and attack-preview components remain client-owned and replaceable. ```ts theme={null} import { provideActionBattle } from "@rpgjs/action-battle/server"; export default provideActionBattle({ ui: { hotbar: { enabled: true, autoOpen: true, capacity: player => Math.min(10, 3 + player.level), lockedSlotHint: (_player, slot) => `Unlocks at level ${slot - 2}` } } }); ``` The client can still customize targeting and preview presentation: ```ts theme={null} import { ActionBattleUi, createActionBattleUi, provideActionBattle } from "@rpgjs/action-battle/client"; export default provideActionBattle({ ui: createActionBattleUi({ targeting: { enabled: true, component: ActionBattleUi.TargetingOverlay, showGrid: true }, attackPreview: { enabled: true, component: ActionBattleUi.AttackPreview }, gui: [ { id: "my-combat-panel", component: MyCombatPanel } ], spriteComponents: { front: [MySpriteOverlay], back: [MyShadow] } }) }); ``` The generic hotbar uses LT/RT to cycle through visible slots and LB to open the radial selector. Projects with a custom gamepad layout can override the standard Gamepad API button indexes: ```ts theme={null} export default { gamepadControls: { hotbarPreviousButton: 6, hotbarNextButton: 7, hotbarUseButton: 2, hotbarWheelButton: 4 } }; ``` Number keys use their assigned slot immediately. On a standard gamepad, LT/RT change the active slot and X uses it. Instant skills soft-target in front of the player; melee area skills keep the manual targeting confirmation. Shortcuts are accepted: ```ts theme={null} ui: { hotbar: false } ui: createActionBattleUi({ targeting: false, attackPreview: false }) ``` ### Compatibility The legacy `attack`, `systems.combat`, `systems.ai`, and `skills.getTargeting` options are still supported. New code should prefer `combat`, `ai`, and `skills.targeting` so each part of the action-battle module stays independently replaceable. ### Attack profile model Use `attack.profile` to describe the timing model of a player attack in one typed object. A profile separates the attack into startup, active, and recovery phases so combat systems can share the same vocabulary. ```ts theme={null} provideActionBattle({ attack: { profile: { id: "iron-sword", startupMs: 80, activeMs: 120, recoveryMs: 180, cooldownMs: 380, movementLock: true, directionLock: true, animationKey: "attack", hitPolicy: "oncePerTarget", reaction: { invincibilityMs: 250, hitstunMs: 150, staggerPower: 1 }, hitboxes: { right: { offsetX: 18, offsetY: -18, width: 42, height: 36 } } }, lockDurationMs: 380 } }); ``` The default profile mirrors the legacy attack lock: no startup, a short active window, and recovery that totals `350ms`. The player attack runtime uses `startupMs` before creating the hitbox, `activeMs` to keep the hitbox active, and `totalDurationMs` for movement and direction locks. | Field | Purpose | | - | - | | `id` | Stable name for this attack profile. | | `startupMs` | Wind-up time before the attack should become active. | | `activeMs` | Duration of the intended hit window. | | `recoveryMs` | Time after the active window before the action fully recovers. | | `cooldownMs` | Minimum delay before the same profile should be reused. | | `movementLock` | Whether the attack should lock movement. | | `directionLock` | Whether the attack should lock facing direction. | | `animationKey` | Animation key from `animations`, usually `attack`. | | `hitPolicy` | `oncePerTarget` blocks duplicate hits during one attack; `allowRepeatHits` allows repeated hits. | | `reaction.invincibilityMs` | Temporary invincibility after this hit connects. | | `reaction.hitstunMs` | Stun duration requested by this hit. | | `reaction.staggerPower` | Stagger value compared against enemy `poise`. | | `hitboxes` | Optional hitbox overrides for this profile. | Weapons can override the player attack profile from their database entry: ```ts theme={null} const Dagger = { id: "dagger", name: "Dagger", _type: "weapon" as const, atk: 8, knockbackForce: 20, attackProfile: { id: "dagger", startupMs: 40, activeMs: 70, recoveryMs: 110 } }; ``` ## Skill and weapon actions Skills and weapons can define an `action` block for action-battle selection, while their effect stays automatic by default. `BattleAi` evaluates every learned skill and uses `attackSkill` as a priority hint. A skill that is cooling down, too expensive, out of range, or unable to cover the target does not block a normal attack. Player hotbar skills, enemy skills, and configured equipped weapons use the same executor, so `onUse` receives the same context in every case. ```ts theme={null} const Fireball = { id: "fireball", name: "Fireball", _type: "skill" as const, spCost: 10, power: 40, hitRate: 0.95, action: { target: "enemy", range: 300, mode: "projectile", projectile: { type: "fireball", speed: 220, range: 300, spreadDegrees: 8 } } }; ``` With no `onUse`, action-battle applies the standard RPGJS skill effect: SP cost, hit rate, states, and damage formulas. For weapons, the default effect is a physical hit using the equipped weapon stats and action-battle hit hooks. `action.target` can be `"enemy"`, `"ally"`, `"self"`, or `"any"`; enemy resolution uses the attacker's action-battle faction and `targets` selector. Projectile direction uses the same generic projectile options as `map.projectiles.emit()`, including `spreadDegrees` and `accuracy`. Use `onUse(user, target, ctx)` only when the action needs custom logic: ```ts theme={null} const PoisonArrow = { id: "poison-arrow", _type: "skill" as const, spCost: 12, power: 20, action: { target: "enemy", range: 320, mode: "projectile", projectile: { type: "arrow", speed: 260, range: 320 } }, onUse(user, target, ctx) { ctx.projectile({ type: "arrow", speed: 260, range: 320, onImpact({ target }) { ctx.defaultEffect(target); target?.addState?.("poison"); } }); } }; ``` For full custom actions, skip `ctx.defaultEffect()`: ```ts theme={null} const Heal = { id: "heal", _type: "skill" as const, spCost: 8, action: { target: "ally", range: 180, mode: "instant" }, onUse(user, target, ctx) { ctx.heal(target, 35); } }; ``` Weapons use the same model: ```ts theme={null} const Claw = { id: "claw", _type: "weapon" as const, action: { target: "enemy", range: 45, mode: "instant" }, onUse(user, target, ctx) { ctx.defaultEffect(target); } }; ``` ## Plugin-first extension points Action battle is structured as replaceable systems. You can keep the default Zelda-like sword attack and only replace the pieces your game needs. ```ts theme={null} import { provideActionBattle } from "@rpgjs/action-battle/server"; export default provideActionBattle({ combat: { attack: { lockMovement: true, lockDurationMs: 280, hitboxes: { right: { offsetX: 18, offsetY: -18, width: 42, height: 36 } } }, damage({ attacker, target, skill }) { const raw = target.applyDamage(attacker, skill); return { damage: raw.damage, defeated: target.hp <= 0, raw }; }, hooks: { beforeHit(context) { return context; }, afterHit(result) { console.log(`Damage: ${result.damage}`); } } }, ai: { behaviors: { slime({ hpPercent }) { return { mode: hpPercent !== null && hpPercent < 0.25 ? "retreat" : "assault", attackCooldown: 900 }; } } } }); ``` The public extension contracts are exported from `@rpgjs/action-battle/server`: `ActionBattleCombatSystem`, `ActionBattleAiBehavior`, `ActionBattleHitHooks`, and `ActionBattleHitResult`. For data-driven enemies, use `createActionEnemy()`: ```ts theme={null} import { createActionEnemy, EnemyType } from "@rpgjs/action-battle/server"; const enemyPresets = { slime: { enemyType: EnemyType.Aggressive, behaviorKey: "slime", stats(event) { event.hp = 40; } } }; createActionEnemy(this, "slime", enemyPresets); ``` When the action targets a normal event with no `BattleAi`, the server lets the event handle `onAction` and does not create the combat hitbox. Enemy events with `BattleAi` still trigger the A-RPG attack. ## Configure stats with the standard RPGJS API The AI uses the event's existing data. ### Health and resources ```ts theme={null} this.hp = 100; this.param[MAXHP] = 100; this.sp = 50; this.param[MAXSP] = 50; ``` ### Parameters ```ts theme={null} import { ATK, PDEF, SDEF } from "@rpgjs/server"; this.param[ATK] = 20; this.param[PDEF] = 10; this.param[SDEF] = 8; ``` ### Skills ```ts theme={null} import { Fireball, Heal } from "./database/skills"; this.learnSkill(Fireball); this.learnSkill(Heal); ``` ### Items and equipment ```ts theme={null} import { Sword, Shield, Potion } from "./database/items"; this.addItem(Potion, 3); this.equip(Sword); this.equip(Shield); ``` ### Classes ```ts theme={null} import { WarriorClass } from "./database/classes"; this.setClass(WarriorClass); ``` ### States ```ts theme={null} import { PoisonState } from "./database/states"; this.addState(PoisonState); ``` ## AI configuration All AI options are optional: ```ts theme={null} new BattleAi(event, { enemyType: EnemyType.Aggressive, attackSkill: Fireball, attackCooldown: 1000, visionRange: 150, attackRange: 60, dodgeChance: 0.2, dodgeCooldown: 2000, fleeThreshold: 0.2, attackPatterns: [ AttackPattern.Melee, AttackPattern.Combo, AttackPattern.DashAttack ], attackProfiles: { charged: { startupMs: 900, activeMs: 140, recoveryMs: 300, reaction: { hitstunMs: 240, staggerPower: 2 } } }, poise: 1, hitstunMs: 150, invincibilityMs: 250, patrolWaypoints: [ { x: 100, y: 100 }, { x: 300, y: 100 } ], groupBehavior: true, animations: { attack: { animationName: "walk", graphic: "goblin_attack", repeat: 1 }, hurt: { animationName: "walk", graphic: "goblin_hurt", repeat: 1 }, die: { animationName: "walk", graphic: "goblin_die", repeat: 1, delayMs: 700 } }, rewards: { exp: 50, gold: 25, items: [{ itemId: "health_potion", amount: 1, chance: 30 }], showNotification: true }, onDefeated: ({ event, attacker }) => { const name = attacker?.name?.() ?? "Unknown"; console.log(`${event.name} was defeated by ${name}!`); } }); ``` Per-enemy `animations` override the global `provideActionBattle()` animations. Use a string for a simple animation name, an object to temporarily switch graphics, or a resolver function for data-driven events. Return `null` or `undefined` from a resolver to skip the animation. `attackProfiles` lets enemies telegraph attacks with `startupMs`, keep hitboxes active for `activeMs`, and apply hit reactions. `poise` controls interruption: an incoming hit only stuns the enemy when its `reaction.staggerPower` is greater than or equal to the enemy's `poise`. `rewards` are awarded once to the player who lands the killing blow. On defeat, Action Battle calls `event.remove({ reason: "defeated", transition })`. The server removes collision immediately while clients keep the sprite long enough to play its Studio `die` animation and a configurable CanvasEngine death effect. Use `presentation.death` to tune the effect, scale, shake, and duration, or set it to `false` for immediate removal. The legacy `onDefeated(event, attacker)` signature remains supported for two-argument callbacks. When combat spritesheets come from RPGJS Studio media fields, convert the media ids with `createStudioActionBattleAnimations()`. Studio-generated combat spritesheets are played with `setGraphicAnimation("attack", graphic, 1)` by default: ```ts theme={null} import { provideActionBattle } from "@rpgjs/action-battle/server"; import { createStudioActionBattleAnimations } from "@rpgjs/studio/server"; export default provideActionBattle({ animations: createStudioActionBattleAnimations() }); ``` Without arguments, the helper reads the Studio project animations attached to the player at runtime by `provideStudioGame()`. You can still pass a static object when you want to override the media ids manually. Animation values may be media ids or media objects returned by the Studio game API. Studio four-direction attack spritesheets play in `350ms` by default so their visual timing matches the default Adventure attack profile without changing the walk animation speed. A media record may override this client-side presentation with `metadata.attackDurationMs`; gameplay startup, active, and recovery windows remain server-authoritative and independent. For Studio enemies, the runtime reads `enemy.animations` automatically when an enemy is created from the Studio database. The supported Studio fields are `attack`, `hurt`, `die`, `castSpell`, `guard`, `parry`, and `stagger`; `castSkill` is also accepted when you configure action-battle directly. `stagger` falls back to the Studio `hurt` animation. These values are resolved on the server and sent to the client as plain animation data, so media IDs remain usable without transferring resolver functions. Global animation resolvers are also evaluated on the server for player skill casts that do not supply animation overrides. This uses the caster's configured Studio animations in both standalone and MMORPG games. ## Combat sounds and dynamic music Action Battle can play local action cues and crossfade the current map BGM into a looping battle track: ```ts theme={null} provideActionBattle({ audio: { attack: ["sword-swing-1", "sword-swing-2"], skill: "magic-cast", hit: "blade-hit", hurt: "hero-hurt", die: "enemy-die", music: { battle: "battle-theme", volume: 0.8, fadeInMs: 600, fadeOutMs: 900, exitDelayMs: 1500 } } }); ``` A cue accepts one sound id, several variants, or `{ id, volume, cooldownMs }`. Basic attacks are predicted locally and suppress the matching authoritative cue for 250ms. A skill's own `sound` field takes priority over the generic skill cue. Cue cooldowns prevent repeated area hits from producing an unusable wall of sound. Combat threat is authoritative on the server and isolated per player. Alert, combat, fleeing, and stunned enemies keep that player in combat. On equal priority the currently selected enemy stays stable; bosses default to priority `100` and ordinary enemies to `0`. Configure an enemy-specific track with: ```ts theme={null} new BattleAi(event, { presentation: { role: "boss", music: { battle: "boss-theme", priority: 120 } } }); ``` Track precedence is enemy, map, project/default, then silence. The ambient sound layer is never stopped. With RPGJS Studio, use the combined preset on both client and server: ```ts theme={null} import { provideActionBattle } from "@rpgjs/action-battle"; import { createStudioActionBattlePreset } from "@rpgjs/studio"; provideActionBattle(createStudioActionBattlePreset()); ``` Studio reads project `combatAudio`, map `combatMusic`, and enemy `combatMusic` plus `combatMusicPriority`. `createStudioActionBattleAudio(config)` can provide the same settings statically before these fields are available in a project. Studio media ids are resolved lazily; resolving a new effect does not stop music that is already playing. The Adventure AI director limits how many enemies attack one target simultaneously. Other enemies keep repositioning instead of stacking the same attack: ```ts theme={null} provideActionBattle({ ai: { director: { maxConcurrentAttackers: 1, slotDurationMs: 1200 } } }); ``` ## Composable AI behaviors Action Battle has three AI layers. They all run on the server and end in the same authoritative runtime for movement, attacks, skills, cooldowns, hit reactions, and rewards. * **Presets** are the fastest path for common enemies. * **Simplified behaviors** are readable rule lists that return intentions. * **Behavior trees** are the advanced API for bosses and custom enemy logic. ### Presets Use a built-in preset and override only what changes: ```ts theme={null} import { BattleAi, AttackPattern } from "@rpgjs/action-battle/server"; new BattleAi(event, { preset: "aggressive", attackRange: 56, attackPatterns: [AttackPattern.Melee, AttackPattern.DashAttack], rewards: { exp: 12, gold: 4 } }); ``` Built-in presets are `aggressive`, `defensive`, `ranged`, `tank`, and `berserker`. A preset is only behavior configuration. Stats still come from the event itself. Register project presets through `provideActionBattle()`: ```ts theme={null} import { AttackPattern, chase, flee, ifHpBelow, ifTargetInRange, provideActionBattle, useAttack } from "@rpgjs/action-battle/server"; export default provideActionBattle({ ai: { presets: { slime: { preset: "aggressive", attackCooldown: 900, attackPatterns: [AttackPattern.Melee], simpleBehavior: { when: [ ifHpBelow(0.25, flee()), ifTargetInRange(useAttack(AttackPattern.Melee)) ], otherwise: chase() } } } } }); ``` Then instantiate enemies from the preset: ```ts theme={null} new BattleAi(event, { preset: "slime" }); ``` Presets are composable. A project preset can extend a built-in preset or another project preset with `preset: "name"`. Local options passed to `new BattleAi()` override the preset values. ### Simplified behaviors Use `simpleBehavior` when you want expressive rules without writing a full behavior tree. Each rule checks a condition and returns an intent: ```ts theme={null} import { AttackPattern, BattleAi, action, chase, flee, ifDistanceLessThan, ifHpBelow, ifTargetInRange, keepDistance, useAttack } from "@rpgjs/action-battle/server"; new BattleAi(event, { preset: "ranged", simpleBehavior: { when: [ ifHpBelow(0.3, flee()), ifDistanceLessThan(80, keepDistance(120)), ifTargetInRange(useAttack(AttackPattern.Zone), 115) ], otherwise: chase() } }); ``` Common condition helpers: | Helper | Meaning | | - | - | | `ifHpBelow(ratio, intent)` | Run an intent when enemy HP is below a ratio. | | `ifTargetVisible(intent)` | Run an intent when the AI currently has a target. | | `ifTargetInRange(intent, range?)` | Run an intent when the target is within a range. | | `ifDistanceLessThan(distance, intent)` | Run an intent when the target is too close. | Common intention helpers: | Helper | Runtime behavior | | - | - | | `chase()` / `moveToTarget()` | Move toward the current target. | | `flee()` / `fleeFromTarget()` | Flee from the current target. | | `keepDistance(distance, tolerance?)` | Retreat or approach until the target is near the desired distance. | | `useAttack(pattern?)` | Use a configured attack pattern when cooldown and range allow it. | | `useSkill(skill)` | Cast a skill against the current target when cooldown and range allow it. | | `faceTarget()` | Face the current target. | | `patrol()` | Continue the configured patrol route. | | `idle()` | Stop movement for this AI tick. | ### Behavior trees Use `behaviorTree` when you need explicit tree control. A tree node returns `success`, `failure`, or `running`, optionally with an intent or decision. ```ts theme={null} import { AttackPattern, BattleAi, action, chase, condition, flee, hpBelow, selector, sequence, targetInRange, useAttack } from "@rpgjs/action-battle/server"; new BattleAi(event, { preset: "tank", attackRange: 60, attackCooldown: 1200, poise: 2, behaviorTree: selector([ sequence([ condition(hpBelow(0.18)), action(flee()) ]), sequence([ condition(targetInRange(60)), action(useAttack(AttackPattern.Charged)) ]), action(chase()) ]) }); ``` The built-in tree helpers are intentionally small: | Helper | Purpose | | - | - | | `selector([...])` | Try children in order and return the first non-failure result. | | `sequence([...])` | Run children in order and fail as soon as one child fails. | | `condition(predicate)` | Convert a predicate into a tree node. | | `action(intent)` | Convert an intent into a tree action. | | `decision(fnOrObject)` | Return low-level AI decisions such as `mode`, cooldowns, or attack patterns. | | `defineAiTree(input)` | Wrap a tree function or node. | | `defineAiBehavior({ when, otherwise })` | Compile simplified behavior rules to a tree. | ### Dynamic behavior and memory Intent functions receive the AI context and can use `memory` for per-enemy state: ```ts theme={null} import { BattleAi, useAttack } from "@rpgjs/action-battle/server"; new BattleAi(event, { simpleBehavior: { otherwise: ({ memory }) => { memory.comboStep = (memory.comboStep ?? 0) + 1; return useAttack(memory.comboStep % 3 === 0 ? "charged" : "melee"); } } }); ``` The context includes: | Field | Description | | - | - | | `event` / `self.event` | The controlled `RpgEvent`. | | `target` | The current `RpgPlayer` target, or `null`. | | `targetInfo` | Target distance, visibility, and attack-range status. | | `state` / `self.state` | Current `AiState`. | | `enemyType` / `self.enemyType` | Current `EnemyType`. | | `hpPercent` / `self.hpPercent` | Current HP ratio, or `null` if max HP is unavailable. | | `memory` | Mutable per-AI storage for custom behavior state. | ### Boss phases and delayed sequences Phase helpers keep scripted behavior in the existing behavior-tree runtime. Their state is stored per AI instance in `memory`, so a preset can safely be shared by several enemies: ```ts theme={null} import { AttackPattern, BattleAi, callAction, chase, phase, selector, sequenceWithDelay, setSpeed, teleportNearTarget, useAttack, visual, wait } from "@rpgjs/action-battle/server"; new BattleAi(event, { behaviorTree: selector([ phase("rage", 0.6, sequenceWithDelay("rage-sequence", [ visual({ kind: "bubble", text: "!", durationMs: 700 }), wait(700), setSpeed(5), teleportNearTarget({ distance: 160 }), callAction("summon-wave", { count: 3 }) ])), action(useAttack(AttackPattern.Melee)), action(chase()) ]) }); ``` `phase(key, hpRatio, action)` completes once after HP falls below the ratio. `once(key, action)` provides the same one-time behavior without an HP condition. `cooldown(key, ms, action)` starts its cooldown only after the wrapped action succeeds. `sequenceWithDelay(key, steps)` advances across AI ticks, and `wait(ms)` uses the authoritative server clock without creating client-owned gameplay timers. Use `run(callback)` when project logic is local to the tree. For reusable or module-provided behavior, register a named action: ```ts theme={null} provideActionBattle({ ai: { actions: { "summon-wave": ({ event, target, memory }, payload) => { const count = Number(payload?.count ?? 1); // Use normal RPGJS server APIs here. } } } }); ``` `callAction()` returns failure when its name is not registered, allowing a selector to continue to a fallback branch. ### Server-driven AI visuals `visual()` sends a JSON-shaped cue through the existing Action Battle client visual packet. The server decides when it happens; the client handler only renders it: ```ts theme={null} // Server visual({ kind: "ground-marker", durationMs: 900, position: { x: 320, y: 240 } }); // Client provideActionBattle({ ai: { visuals: { "ground-marker"({ visual }, fx) { fx.component( "boss-ground-marker", visual.position as { x: number; y: number }, { durationMs: visual.durationMs } ); } } } }); ``` Unknown visual kinds are ignored. Keep cue payloads serializable and use `once()` or `cooldown()` around cues selected by a tree branch so they are not sent on every 100 ms AI tick. Visual handlers must never apply damage, change stats, select targets, or otherwise own gameplay state. ### Movement and server actions The advanced intent helpers are thin wrappers over the controlled event: | Helper | Runtime behavior | | - | - | | `setSpeed(value)` | Set the event's synchronized speed; non-consuming by default. | | `moveToPoint({ x, y })` | Use RPGJS `moveTo()` with the AI movement throttle. | | `holdPosition()` | Stop the current movement. | | `teleportTo({ x, y })` | Use RPGJS `teleport()` at a fixed position. | | `teleportNearTarget({ distance, angleDegrees? })` | Compute a position around the current target on the server, then teleport. | | `run(callback)` | Execute project logic directly with the current tree context. | | `callAction(name, payload?)` | Execute a reusable action from `ai.actions`. | Teleport helpers deliberately use the normal RPGJS teleport primitive and do not search for a collision-free position. Projects that need safe placement should resolve it in `run()` or a registered action before calling `teleportTo()`. ### Sample project `samples/sample-dev` contains four AI demo enemies on `center-map`: * `Preset Rusher` uses a named preset. * `Simple Kiter` uses `simpleBehavior` and distance control. * `Tree Elite` uses a direct `behaviorTree`. * `Phase Boss` uses delayed phases, generic visuals, speed changes, teleportation, and a registered action. ## Enemy types Enemy types affect behavior, not stats: | Type | Attack Speed | Dodge | Behavior | | - | - | - | - | | Aggressive | Fast | Low | Rushes player | | Defensive | Slow | High | Counter-attacks | | Ranged | Medium | Medium | Keeps distance | | Tank | Slow | None | Stands ground | | Berserker | Variable | Low | Faster when hurt | ## Attack patterns | Pattern | Description | | - | - | | Melee | Single attack | | Combo | 2-3 rapid attacks | | Charged | Wind-up, stronger attack | | Zone | 360° area attack | | DashAttack | Rush toward target then attack | ## Use skills for attacks ```ts theme={null} import { Skill } from "@rpgjs/database"; @Skill({ name: "Slash", spCost: 5, power: 25, hitRate: 0.95 }) export class Slash {} onInit() { this.hp = 100; this.sp = 50; this.learnSkill(Slash); new BattleAi(this, { attackSkill: Slash }); } ``` All skills learned by the enemy are candidates. `attackSkill` makes one skill the preferred opener without excluding the others. Melee skills require contact, projectiles use their configured travel range, and instant area skills use `targeting.range` plus `aoeMask`. While no action is ready, the enemy approaches, retreats, or strafes toward the useful range of its next action. Self-targeted healing and support skills are used automatically below 60% HP. Ally-targeted skills are outside the current automatic planner. ## Debug enemy decisions Decision tracing is disabled by default. Enable it on the authoritative server and optionally filter one enemy or a set of categories: ```ts theme={null} import { AiDebug } from "@rpgjs/action-battle/server"; AiDebug.enabled = true; AiDebug.filterEventId = "dark-mage-1"; AiDebug.categories = ["decision", "movement"]; ``` `decision` logs include the distance, global cooldown, evaluated skills, effective ranges, rejection reasons, and the selected attack or repositioning request. Common rejection reasons are `cooldown`, `insufficientSp`, `outOfRange`, `invalidTarget`, `maskMiss`, and `notUseful`. For increasing levels of control, start with learned skills only, add `attackSkill` for an explicit priority, use `defineActionBattleAiPreset()` for reusable typed defaults, then use `behaviorKey` or a behavior tree for fully custom decisions. ## Examples ### Basic enemy ```ts theme={null} import { type EventDefinition, ATK, MAXHP } from "@rpgjs/server"; function Goblin(): EventDefinition { return { name: "Goblin", onInit() { this.setGraphic("goblin"); this.hp = 50; this.param[MAXHP] = 50; this.param[ATK] = 10; new BattleAi(this); } }; } ``` ### Mage with skills ```ts theme={null} import { ATK, MAXHP, MAXSP, type EventDefinition } from "@rpgjs/server"; function DarkMage(): EventDefinition { return { name: "Dark Mage", onInit() { this.setGraphic("mage"); this.hp = 60; this.sp = 100; this.param[MAXHP] = 60; this.param[MAXSP] = 100; this.param[ATK] = 25; this.learnSkill(Fireball); new BattleAi(this, { enemyType: EnemyType.Ranged, attackSkill: Fireball, visionRange: 200 }); } }; } ``` ### Patrol guard ```ts theme={null} import { ATK, MAXHP, type EventDefinition } from "@rpgjs/server"; function PatrolGuard(): EventDefinition { return { name: "Guard", onInit() { this.setGraphic("guard"); this.hp = 80; this.param[MAXHP] = 80; this.param[ATK] = 15; new BattleAi(this, { enemyType: EnemyType.Defensive, patrolWaypoints: [ { x: 100, y: 150 }, { x: 300, y: 150 }, { x: 300, y: 350 }, { x: 100, y: 350 } ] }); } }; } ``` ## Player combat The module handles player attacks via the `action` input: ```ts theme={null} // Player presses action key -> attack animation + hitbox // Hitbox detects enemy -> applyPlayerHitToEvent(player, event) // Damage uses RPGJS formula: target.applyDamage(attacker) ``` ## Knockback system Knockback force is driven by the equipped weapon's `knockbackForce` property: ```ts theme={null} const Warhammer = { id: "warhammer", name: "War Hammer", atk: 30, knockbackForce: 100, _type: "weapon" as const }; ``` ### Enemy attack cadence Enemy attacks separate preparation (`startupMs`), the active phase (`activeMs`) and recovery (`recoveryMs`). The enemy stops moving and keeps its attack direction during these phases. The telegraph runs during preparation; the attack animation then plays one complete cycle over active + recovery time. The client receives this duration as `animationDefaults.durationMs`; damage and action locks remain authoritative on the server, in both RPG and MMORPG. A new attack waits for the global `attackCooldown`, the selected profile's `cooldownMs`, and its complete phase duration. Combo strikes wait for their profile's duration and cooldown before continuing; the global cooldown separates attack sequences. Profile overrides allow faster or slower enemies without depending on sprite names or image dimensions. Default timings, in milliseconds: | Pattern | Preparation | Active | Recovery | Minimum interval | | - | -: | -: | -: | -: | | melee | 250 | 120 | 350 | 1100 | | combo | 140 | 80 | 200 | 500 | | charged | 800 | 140 | 500 | 1900 | | zone | 450 | 180 | 420 | 1500 | | dashAttack | 250 | 120 | 350 | 1200 | Override these values through `BattleAi` options, for example `attackProfiles: { melee: { startupMs: 180, recoveryMs: 250, cooldownMs: 800 } }`. Without a client animation mapping, combat timings and damage still work. Remote characters use the synchronized walk/stand state; local players retain input-driven locomotion while temporary attack animations finish. When `attackPatterns` is explicitly configured, archetype decisions choose only from that list. If an archetype suggests no permitted pattern, the configured list is retained. This prevents generic aggressive behavior from introducing a combo or dash that the enemy designer disabled. The elapsed-time playback correction also requires the companion CanvasEngine sprite scheduler fix (raw and reset-envelope ticks). Publish that patch and update the consumer's CanvasEngine lockfile alongside these RPGJS changes. # Client Visuals Source: https://v5.rpgjs.dev/guide/client-visuals Group client-side visual and audio primitives behind a server-triggered visual name. # 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: ```ts theme={null} target.flash() target.showHit("-25") target.showComponentAnimation("hit-spark", { scale: 1.2 }) player.playSound("hit") ``` With client visuals, the server sends one compact packet: ```ts theme={null} 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: ```ts theme={null} 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`: ```ts theme={null} import { defineModule, RpgClient } from "@rpgjs/client"; import HitSpark from "./hit-spark.ce"; export default defineModule({ 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: ```ts theme={null} 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: ```ts theme={null} map.clientVisual("explosion", { position: { x: 320, y: 180 }, power: 2, }); ``` And the client can render it at a position: ```ts theme={null} export default defineModule({ 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 ```ts theme={null} 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: ```ts theme={null} 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: ```ts theme={null} 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: ```ts theme={null} 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. ```ts theme={null} // client/module.ts import { defineModule, RpgClient } from "@rpgjs/client"; import HitSpark from "./hit-spark.ce"; export default defineModule({ 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"); }, }, }); ``` ```ts theme={null} // 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. # Authoritative Sprite Components Source: https://v5.rpgjs.dev/guide/component Display text, bars, shapes and custom CanvasEngine components around a player from the server. # Authoritative Sprite Components Use player components to display reusable UI around a player sprite: name tags, HP/SP bars, status indicators, badges, or custom CanvasEngine components. Use authoritative sprite components when server gameplay code decides what appears around a player. For client-only visual layers on sprites, use [Sprite Components](/guide/sprite-components). For interactive menus, dialogs, or widgets, use [GUI](/gui/index). The server is authoritative. It decides which component is displayed and sends a serializable description to every client on the map. The client renders the component and updates dynamic values such as `{name}`, `{hp}` or `{param.maxHp}` when synchronized player properties change. ## Display Name Above The Player Player name component In your server player file: ```ts theme={null} import { RpgPlayer, type RpgPlayerHooks, Components } from '@rpgjs/server' const player: RpgPlayerHooks = { onConnected(player: RpgPlayer) { player.setHitbox(16, 16) player.setGraphic('hero') player.name = 'Sam' player.setComponentsTop(Components.text('{name}')) } } export default player ``` `Components.text('{name}')` creates a text component. The `{name}` placeholder is resolved on the client from the synchronized player property. Prefer placeholders instead of injecting the current value yourself: ```ts theme={null} player.setComponentsTop(Components.text(player.name)) ``` With `{name}`, changing the player name only synchronizes the `name` property. With `player.name`, the server must send a new component structure every time. ## Text Style Styled player name component ```ts theme={null} player.setComponentsTop(Components.text('{name}', { fill: '#000000', fontSize: 20 })) ``` Text style supports common CanvasEngine text options such as `fill`, `fontSize`, `fontFamily`, `fontStyle`, `fontWeight`, `stroke`, `opacity`, `wordWrap` and `align`. ## Multiple Lines And Columns Multiple text components A one-dimensional array is rendered vertically: ```ts theme={null} player.setComponentsTop([ Components.text('HP: {hp}'), Components.text('{name}') ]) ``` A two-dimensional array is rendered as rows and columns: ```ts theme={null} player.setComponentsTop([ [Components.text('{hp}'), Components.text('{name}')] ]) ``` ## Layout Options ```ts theme={null} player.setComponentsTop([ Components.text('HP: {hp}'), Components.text('{name}') ], { width: 100, height: 30, marginBottom: 8 }) ``` Layout options configure the block that contains the components: * `width` and `height` define the layout box. * `marginBottom` moves a `top` or `bottom` component away from the sprite. * `marginRight` moves a `left` component away from the sprite. * `marginLeft` moves a `right` component away from the sprite. * `marginTop` can be used for additional vertical adjustment. `top`, `left` and `right` are placed outside the sprite graphic bounds, so they do not cover a graphic that is larger than the hitbox. `top` is centered on the graphic. `bottom` uses the hitbox as its positioning rectangle; a `32x32` shape inside a `32x32` hitbox matches the hitbox when centered. ## HP And SP Bars HP bar component ```ts theme={null} player.setComponentsTop(Components.hpBar(), { width: 42 }) ``` `Components.hpBar()` reads `hp` and `param.maxHp` and uses a red fill by default. `Components.spBar()` reads `sp` and `param.maxSp` and uses a blue fill by default. Pass `fillColor` to override the default color. ```ts theme={null} player.setComponentsTop(Components.spBar({ width: 42, fillColor: '#60a5fa' })) ``` ### Bar Text HP bar with text ```ts theme={null} player.setComponentsTop( Components.hpBar({}, '{$percent}%'), { width: 42 } ) ``` The text is displayed above the bar and aligned to the left edge of the bar. It does not move when the current value changes. Bar text can use normal player placeholders and bar-specific placeholders: * `{$current}`: current value * `{$max}`: maximum value * `{$percent}`: percentage without the `%` sign Set the text to `null` to hide it: ```ts theme={null} player.setComponentsTop(Components.hpBar({}, null)) ``` ## Other Positions Shape component below the player ```ts theme={null} player.setComponentsBottom( Components.shape({ type: 'rect', width: 32, height: 32, fill: '#ff0000', opacity: 0.5 }), { marginBottom: 16 } ) ``` Available positions are: ```ts theme={null} player.setComponentsTop(Components.text('{name}')) player.setComponentsBottom(Components.hpBar()) player.setComponentsLeft(Components.text('L')) player.setComponentsRight(Components.text('R')) player.setComponentsCenter(Components.shape({ type: 'circle', radius: 8, fill: '#ffffff' })) ``` Use `player.removeComponents(position)` to remove all components at a position: ```ts theme={null} player.removeComponents('top') ``` ## Custom Player Properties Any synchronized player property can be used in placeholders. ```ts theme={null} import { RpgPlayer, type RpgPlayerHooks, Components } from '@rpgjs/server' declare module '@rpgjs/server' { export interface RpgPlayer { wood: number } } const player: RpgPlayerHooks = { props: { wood: Number }, onConnected(player: RpgPlayer) { player.wood = 0 player.setComponentsTop(Components.text('Wood: {wood}')) } } export default player ``` You can also create a bar for a custom value: ```ts theme={null} player.addParameter('maxWood', { start: 100, end: 500 }) player.wood = player.param.maxWood player.setComponentsTop( Components.bar('wood', 'param.maxWood', {}, 'Wood: {$current}/{$max}') ) ``` ## Custom CanvasEngine Components Register a reusable CanvasEngine component on the client: ```ts theme={null} // client.ts import { defineModule, RpgClient } from '@rpgjs/client' import GuildBadge from './components/guild-badge.ce' export default defineModule({ sprite: { components: { guildBadge: GuildBadge } } }) ``` Create the component: ```html theme={null} ``` Then let the server decide when to display it: ```ts theme={null} player.setComponentsTop([ Components.custom('guildBadge', { guildName: '{guild.name}', color: '{guild.color}' }) ]) ``` Custom props must be serializable: strings, numbers, booleans, arrays and plain objects. They can contain placeholders, so the component receives updated values when synchronized player properties change. ## Custom Event Renderers Events can also be rendered by a client-side CanvasEngine component instead of the default graphic renderer. Register a resolver on the client: ```ts theme={null} // client.ts import { defineModule, RpgClient } from '@rpgjs/client' import ChestEvent from './components/chest-event.ce' export default defineModule({ sprite: { eventComponent(sprite) { if (sprite.name === 'CHEST') { return ChestEvent } return null } } }) ``` The custom component always receives the synced event object as the `sprite` prop: ```html theme={null} ``` Return a config object to add props or keep the regular graphic visible behind the custom component: ```ts theme={null} eventComponent(sprite) { if (sprite.name !== 'CHEST') return null return { component: ChestEvent, props: { variant: 'wood' }, renderGraphic: true } } ``` ## Graphic IDs And Legacy Tile IDs `player.setGraphic()` accepts a spritesheet id, a legacy tile id, or an array mixing both: ```ts theme={null} player.setGraphic('hero') player.setGraphic(3) player.setGraphic(['body', 'shield', 3]) ``` Numeric graphics are kept for compatibility with tile-based projects. Projects without a tile graphic resolver can ignore numeric ids and use spritesheet ids. ## Components Or Animations Use server-driven sprite components for persistent UI controlled by the server: name tags, HP bars, guild badges and status icons. Use `componentAnimations` and `showComponentAnimation()` for temporary effects: hit numbers, explosions, spell effects and transitions that remove themselves with `onFinish`. # Components overview Source: https://v5.rpgjs.dev/guide/components-overview Choose the right RPGJS component system for sprites, server-controlled visuals, and prebuilt effects. # Components overview RPGJS has several component systems. They all render visual elements, but they do not have the same owner or lifecycle. Use this page to choose the right one before creating a `.ce` component or a server-controlled sprite display. ## Choose a component type | Need | Use | | - | - | | Add a visual effect to every matching sprite from the client | [Sprite Components](/guide/sprite-components) | | Display player-specific visuals decided by the server | [Authoritative Sprite Components](/guide/component) | | Reuse an RPGJS-provided visual effect | [Prebuilt Components](/guide/prebuilt-components/index) | | Show an interactive interface that opens, closes, or sends actions | [GUI](/gui/index) | | Show an interface that follows a sprite and is controlled like a GUI | [Attach GUI to Sprites](/guide/gui/attach-gui) | ## Sprite components Sprite components are client-side CanvasEngine components attached to sprite rendering. Use them for visual layers such as shadows, auras, simple status indicators, or other effects that should render behind or in front of sprites. They are configured from the client module with `componentsBehind` and `componentsInFront`. ## Authoritative sprite components Authoritative sprite components are controlled by the server. Use them when the server must decide what appears around a player, such as a name tag, HP bar, badge, or server-driven status marker. They are configured from server gameplay code with methods such as `setComponentsTop()`, `setComponentsBottom()`, and `removeComponents()`. ## Prebuilt components Prebuilt components are ready-to-use components shipped by RPGJS. Use them when an existing component matches the visual effect you need before creating a custom `.ce` component. ## GUI is separate GUI components are application interfaces, not sprite render layers. Use GUI pages for menus, dialogs, HUDs, title screens, Vue integration, optimistic actions, and GUI components attached to sprites. Start with [GUI overview](/gui/index) when the component should behave like an interface rather than a passive sprite visual. # Create database Source: https://v5.rpgjs.dev/guide/create-database Register items, skills, states, and other game data. # Create database The database is where you register reusable game data such as items, skills, classes, states, or custom objects. ## Register data in `provideServerModules` ```ts theme={null} import { provideServerModules } from "@rpgjs/server"; import { Potion } from "./database/Potion"; provideServerModules([ { database: { Potion } } ]); ``` Once registered, you can use string IDs from the server: ```ts theme={null} player.addItem("Potion", 1); ``` ## Add data dynamically on a map You can also register data at runtime: ```ts theme={null} map.addInDatabase("treasure-coin", { id: "treasure-coin", name: "Treasure Coin", price: 500 }); player.addItem("treasure-coin", 1); ``` ## What belongs in the database * items * skills * states * classes * custom server-side objects used by your game ## Recommended flow 1. Put shared game data in `database` 2. Keep map-specific or temporary content dynamic with `map.addInDatabase()` 3. Use string IDs from gameplay code when possible ## More details See [Items System](/guide/items) for a complete example with item hooks and dynamic items. ## Next step Continue with [Create sounds](/guide/create-sounds). # Create an event Source: https://v5.rpgjs.dev/guide/create-event Create your first NPC or interactive event. # Create an event An event is any interactive object on the map: an NPC, a chest, a switch, an enemy, or a trigger. ## Create a simple event You can define an event as an object with hooks: ```ts theme={null} import { RpgPlayer, type EventDefinition } from "@rpgjs/server"; export function ChestEvent(): EventDefinition { return { onInit() { this.setGraphic("chest-closed"); }, onChanges(player: RpgPlayer) { const isOpened = player.getVariable("chest-1-opened"); this.setGraphic(isOpened ? "chest-opened" : "chest-closed"); }, async onAction(player: RpgPlayer) { if (player.getVariable("chest-1-opened")) { await player.showText("The chest is already open."); return; } player.setVariable("chest-1-opened", true); await player.showText("You found a potion."); } }; } ``` ## Attach the event to a map Register it in your server module: ```ts theme={null} import { defineModule, type RpgServer } from "@rpgjs/server"; import { ChestEvent } from "./events/chest"; export default defineModule({ maps: [ { id: "simplemap", events: [ { id: "chest-1", x: 200, y: 120, event: ChestEvent() } ] } ] }); ``` `EventDefinition` only describes the event behavior. Map placement fields such as `id`, `x`, and `y` belong to the outer wrapper in `maps[].events`. Inside object-based hooks, `this` is typed as `RpgEvent`, so methods like `this.setGraphic()` are inferred correctly by TypeScript. Events block players without being pushed by player collisions by default. They can still move through scripted movement such as `moveRoutes()`. Set `pushable: true` when an event should move from physical collision response, then use `mass` to tune how hard it is to push. A mass of `0` or `Infinity` still makes the body immovable. ```ts theme={null} { id: "crate-1", x: 200, y: 120, event: { name: "Crate", pushable: true, mass: 20 } } ``` You can also change these values at runtime: ```ts theme={null} this.pushable = true; this.setMass(20); ``` When using Tiled, you can omit `x` and `y` and place the event with a point object. Set the point name to the event name, for example `chest-1`, and RPGJS will use that point as the event position. ## Event hooks The most common hooks are: * `onInit()` for base event setup when the instance is created * `onChanges(player)` for reactive updates based on player state * `onAction(player)` when the player interacts with the event * `onPlayerTouch(player)` when the player touches the event Other hooks are also available for shape-based behaviors: * `onInShape(zone, player)` * `onOutShape(zone, player)` * `onDetectInShape(player, shape)` * `onDetectOutShape(player, shape)` See the complete reference in [Event hooks for created events](/api/map/hooks). ## `onInit()` vs `onChanges(player)` The most important distinction is `onChanges(player)`. `onInit()` runs when the event instance is created. Use it for default data that is not tied to a specific player interaction yet: a base graphic, speed, direction, or movement route. `onChanges(player)` runs during the change-detection cycle for that player. When player state changes, especially player variables, RPGJS re-runs this cycle and the event can react to it. You can also trigger it manually with `player.syncChanges()`. This means that some code can legitimately appear in both hooks: * `onInit()` gives the event a correct initial state * `onChanges(player)` keeps that state in sync when the player data changes For a chest, `onInit()` can set an initial graphic so the chest is immediately visible in the right state. Then `onChanges(player)` can recompute the same condition from a player variable and switch the graphic to opened or closed whenever that variable changes. Use player variables for this kind of per-player state. They are persisted with the player and are available again after a save, a reconnect, or a map change. ## Shared or scenario When building an MMORPG, choose the right mode: * use shared for global world state * use scenario for per-player progression See [Event modes](/guide/event-modes) for the full behavior. ## Next step Continue with [Create database](/guide/create-database). # Create hero in map Source: https://v5.rpgjs.dev/guide/create-hero-in-map Spawn the player in a map and assign the first hero graphic. # Create hero in map Once your first map exists, the next step is to put the player inside it and assign a spritesheet. ## Spawn the player on connection In your player hooks: ```ts theme={null} import { RpgPlayer, type RpgPlayerHooks } from "@rpgjs/server"; export const player: RpgPlayerHooks = { onConnected(player: RpgPlayer) { player.initializeDefaultStats(); player.changeMap("simplemap"); player.name = "YourName"; player.setGraphic("hero"); } }; ``` This does three things: 1. Sends the player to `simplemap` 2. Places the player at the Tiled point named `start`, if it exists 3. Uses the `hero` spritesheet on the client It also initializes the built-in default stats so HP, SP, and parameters such as `maxHp` are available immediately on the client. To use the automatic start position, create an object layer in Tiled, add a point, and set its name to `start`. You can also target any named point explicitly: ```ts theme={null} player.changeMap("simplemap", "entrance"); ``` If you prefer coordinates, pass them directly: ```ts theme={null} player.changeMap("simplemap", { x: 100, y: 100 }); ``` ## Avoid spawning inside an obstacle A start point can end up under an obstacle, for example when a map is generated or when an event stands on it. `map.findSpawnPosition()` returns the closest position where the player hitbox overlaps no wall, tile collision, static shape or blocking event, or `null` when nothing is free nearby. It never disables collisions. Resolve the position once the player has joined the map, on the server: ```ts theme={null} import { RpgPlayer, type RpgPlayerHooks } from "@rpgjs/server"; export const player: RpgPlayerHooks = { async onJoinMap(player: RpgPlayer, map) { const hitbox = player.hitbox(); const spawn = map.findSpawnPosition({ preferred: { x: player.x(), y: player.y() }, hitbox: { width: hitbox.w, height: hitbox.h }, z: player.z(), ignoreIds: [player.id], maxDistance: 256, }); if (spawn) { await player.teleport(spawn); } } }; ``` The search is deterministic: tested positions grow in rings around `preferred` (every `step` pixels, half the hitbox by default), and the closest free position wins. By default a position is only accepted when the player can also move to one neighboring position. Pass `requireClearance: false` to accept any free position. The method exists on the client map as well, but only the server result is authoritative. It works in standalone RPG and MMORPG modes. ## Initialize default stats only when needed Use `player.initializeDefaultStats()` if you want RPGJS to define the default parameter curves (`maxHp`, `maxSp`, `str`, `int`, `dex`, `agi`) and initialize HP/SP from those values. This is useful if: * you define the starting stats directly in your game * you rely on RPGJS built-in default values * you want HP/SP and parameter displays to be ready on the client as soon as the game starts Do not call `player.initializeDefaultStats()` after loading player data from: * your own database * a save slot * a snapshot In those cases, restore your saved data instead, otherwise you may overwrite the existing values. If you only need the built-in parameter curves without restoring HP/SP, use `player.applyDefaultParameters()`. ## Use `onStart()` when the game begins after a GUI If your game starts after a title screen or another GUI interaction, initialize the default stats in `onStart()` instead of `onConnected()`. `onStart()` is executed after a GUI interaction returns `data.id === 'start'`. ```ts theme={null} import { RpgPlayer, type RpgPlayerHooks } from "@rpgjs/server"; export const player: RpgPlayerHooks = { async onConnected(player: RpgPlayer) { await player.gui("rpg-title-screen").open(); }, onStart(player: RpgPlayer) { player.initializeDefaultStats(); player.changeMap("simplemap"); player.name = "YourName"; player.setGraphic("hero"); } }; ``` ## Make sure the `hero` graphic exists The `hero` identifier must exist in your client configuration: ```ts theme={null} import { provideClientModules, Presets } from "@rpgjs/client"; provideClientModules([ { spritesheets: [ { id: "hero", image: "spritesheets/hero.png", ...Presets.RMSpritesheet(3, 4) } ] } ]); ``` ## Open the main menu on input You can also add simple input behavior directly in the same hook: ```ts theme={null} onInput(player, { action }) { if (action == "escape") { player.callMainMenu(); } } ``` ## Next step Continue with [Create spritesheet](/guide/create-spritesheet). # Creating Modules in RPGJS Source: https://v5.rpgjs.dev/guide/create-module Guide for Creating Modules in RPGJS in RPGJS. # Creating Modules in RPGJS This guide explains the canonical RPGJS module pattern. `defineModule()` authors runtime behavior and `provideServerModules()` or `provideClientModules()` installs it. ## Module Structure A module that runs on both sides consists of two explicit files: * `server.ts` - Server-side logic and hooks * `client.ts` - Client-side logic and hooks Code used by both runtimes can live in a separate `shared.ts` file. ## Bundle ownership The entry point that imports a module determines which bundle contains it. The file name makes ownership clear, but it does not override an explicit import. | Module file | MMORPG browser bundle | MMORPG server bundle | Standalone browser bundle | | - | - | - | - | | `client.ts` | Included | Excluded | Included | | `server.ts` | Excluded | Included | Included | | `shared.ts` | Included when imported | Included when imported | Included when imported | In MMORPG mode, install `server.ts` only from the server bootstrap and `client.ts` only from the client configuration. The browser build then excludes the server module and its imports. Standalone mode runs the RPGJS server in the browser. Its bundle therefore contains both client and server modules. Never place credentials, private keys, service tokens, or other secrets in a game compiled for standalone mode. Follow these boundaries when structuring a module: * do not import `server.ts` from `client.ts` * do not import an `index.ts` that eagerly imports both runtimes from browser code * keep `shared.ts` limited to types, serializable data, constants, and logic safe to expose to a browser * keep authoritative validation for movement, combat, inventory, and saves on the server, even when the implementation is absent from the client bundle Reusable packages should expose separate `/client` and `/server` entry points instead of asking applications to import a combined runtime entry. ## Step 1: Define Server-Side Module Create a `server.ts` file using `defineModule` to define server-side behavior: ```typescript theme={null} import { RpgPlayer, defineModule, type RpgServer } from "@rpgjs/server"; export default defineModule({ player: {}, }) ``` ## Step 2: Define Client-Side Module Create a `client.ts` file using `defineModule` for client-side logic: ```typescript theme={null} import { defineModule, type RpgClient } from "@rpgjs/client"; export default defineModule({ // Client-side hooks and logic // Add your client-side event handlers here }) ``` ## Step 3: Install Each Runtime Module Install the server definition in `server.ts`: ```typescript theme={null} import { createServer, provideServerModules } from "@rpgjs/server"; import battleServer from "./modules/battle/server"; export default createServer({ providers: [provideServerModules([battleServer])] }); ``` Install the client definition in the shared client configuration: ```typescript theme={null} import { provideClientModules } from "@rpgjs/client"; import battleClient from "./modules/battle/client"; export default { providers: [provideClientModules([battleClient])] }; ``` Standalone and MMORPG clients use this same client configuration. In MMORPG mode, the server definition is built separately and remains authoritative for gameplay state. In standalone mode, that definition executes in the browser. Reusable packages should expose a `provideFeature(options)` function from explicit `/server` and `/client` entry points. Direct DI composition with `createModule()` remains available for advanced integrations, but is not needed for ordinary gameplay modules. # Movement System Guide Source: https://v5.rpgjs.dev/guide/create-movement Guide for Movement System Guide in RPGJS. # Movement System Guide The RPGJS movement system provides a flexible and extensible way to handle entity movement in your game. This guide covers how to use the built-in movement strategies and how to create custom ones. ## Overview The movement system is built around the **Strategy Pattern**, separating movement logic from physics simulation. This allows you to: * Combine multiple movement strategies on a single entity * Create complex movement patterns by composing simple ones * Add custom movement behaviors without modifying existing code * Handle time-based movements that work regardless of frame rate ## Architecture ``` ┌─────────────────┐ ┌───────────────────┐ ┌───────────────┐ │ Game Logic / │ uses │ MovementStrategy │ applies │ Matter.js │ │ Input / AI │──────▶│ (calculates dx,dy)│─────────▶ Physics │ └─────────────────┘ └───────────────────┘ └───────────────┘ ``` ## Using the Movement System The movement system can be used in two ways: ```typescript theme={null} import { RpgPlayer, type RpgServer } from "@rpgjs/server"; import { LinearMove, Dash, Knockback, PathFollow, Oscillate, SeekAvoid, ProjectileType } from "@rpgjs/common"; // In hooks or player events export default defineModule({ onInput(player: RpgPlayer, { input }) { if (input.includes('action')) { player.knockback({ x: -1, y: 0 }, 5, 300); } } }) ``` ### Interrupting client-predicted movement When client prediction is enabled, movement inputs can already be buffered when another action starts. For blocking actions such as a Zelda-like sword attack, stop local movement and discard pending predicted inputs before the action animation starts: ```ts theme={null} import { RpgClient, RpgClientEngine, defineModule } from "@rpgjs/client"; export default defineModule({ engine: { onInput(engine: RpgClientEngine, { input }) { if (input === "action") { engine.interruptCurrentPlayerMovement(); } } } }); ``` This prevents held directional inputs from being replayed during server reconciliation. Once movement is allowed again, normal keyboard repeat can send new movement inputs. ### Core Movement Methods #### Required Imports ```typescript theme={null} import { RpgPlayer } from "@rpgjs/server"; import { LinearMove, Dash, Knockback, PathFollow, Oscillate, CompositeMovement, SeekAvoid, LinearRepulsion, IceMovement, ProjectileMovement, ProjectileType, MovementStrategy } from "@rpgjs/common"; ``` #### Managing Movement Strategies ```typescript theme={null} // Add a custom movement strategy player.addMovement(new LinearMove(5, 0, 1000)); // Remove a specific movement const dashMove = new Dash(8, { x: 1, y: 0 }, 200); player.addMovement(dashMove); player.removeMovement(dashMove); // Clear all movements player.clearMovements(); // Check if entity has active movements if (player.hasActiveMovements()) { console.log("Player is moving"); } // Get all active movements const movements = player.getActiveMovements(); console.log(`Player has ${movements.length} active movements`); ``` ## Built-in Movement Strategies ### 1. Linear Movement Constant velocity movement in a specified direction. ```typescript theme={null} // Move right at 5 pixels per frame for 1 second player.addMovement(new LinearMove(5, 0, 1000)); // Move diagonally indefinitely player.addMovement(new LinearMove(3, 3)); ``` ### 2. Dash Movement Quick burst of speed in a direction for a limited time. ```typescript theme={null} // Dash right for 200ms at speed 8 player.dash({ x: 1, y: 0 }, 8, 200); // Dash diagonally with custom parameters player.dash({ x: 0.7, y: 0.7 }, 12, 300); ``` ### 3. Knockback Effect Push effect that gradually decreases over time. ```typescript theme={null} // Knockback from explosion player.knockback({ x: -1, y: 0 }, 8, 400); // Light knockback from attack player.knockback(attackDirection, 3, 200); ``` Recoil owns movement for its duration. Directional input received during the impact is discarded rather than replayed after landing. A held direction resumes through the normal input loop once recoil ends. `canMove` is not changed by recoil, so a dialog or attack lock remains owned by the system that created it. The impulse decays without accumulating velocity each tick. Completion and cancellation release the recoil phase; overlapping hits keep it active until the last recoil ends. Collision detection still runs through the physics engine. This behavior is shared by standalone RPG and MMORPG runtimes. The authoritative runtime synchronizes the transient `player.knockbackActive()` phase. Game code may read it, but should start recoil with `player.knockback(...)`, not write the phase. During this phase the client accepts server positions without replaying predicted movement or old trajectory frames. Visual position changes ease over 80 ms, with a short settling window after landing; jumps exceeding 128 pixels on an axis are applied immediately. Smoothing affects only rendering, never the collision body. The client does not predict damage or a recoil before the server confirms it. ### 4. Path Following Follow a sequence of waypoints. ```typescript theme={null} // Create a patrol route const patrolPoints = [ { x: 100, y: 100 }, { x: 300, y: 100 }, { x: 300, y: 300 }, { x: 100, y: 300 } ]; // Follow path once player.followPath(patrolPoints, 3); // Loop patrol indefinitely player.followPath(patrolPoints, 3, true); ``` ### 5. Oscillation Back-and-forth movement patterns. ```typescript theme={null} // Horizontal oscillation player.oscillate({ x: 1, y: 0 }, 100, 3000); // Vertical oscillation player.oscillate({ x: 0, y: 1 }, 50, 2000); // Circular oscillation player.oscillate({ x: 1, y: 0 }, 100, 4000, 'circular'); ``` ### 6. AI Target Seeking Intelligent movement toward targets with local obstacle avoidance. `moveTo` does not compute a full path; it steers around nearby blocking hitboxes and events while moving toward the target. ```typescript theme={null} // Move toward another player const targetPlayer = game.getPlayer('player2'); player.moveTo(targetPlayer); // Move toward a specific position player.moveTo({ x: 300, y: 200 }); // Stop the movement player.stopMoveTo(); ``` ### 7. Ice Movement Slippery surface physics with gradual acceleration and inertia. ```typescript theme={null} // Apply ice physics when on ice terrain if (onIceTerrain) { player.applyIceMovement(inputDirection, 5); } ``` ### 8. Projectile Movement Ballistic trajectories for projectiles. ```typescript theme={null} // Shoot arrow (straight line) player.shootProjectile(ProjectileType.Straight, { x: 1, y: 0 }, 300); // Throw grenade (arc trajectory) player.shootProjectile(ProjectileType.Arc, { x: 0.7, y: 0.7 }, 150); // Bouncing projectile player.shootProjectile(ProjectileType.Bounce, { x: 1, y: 0 }, 100); ``` ## Combining Movements ### Parallel Movements Multiple movements can be active simultaneously: ```typescript theme={null} // Apply dash and oscillation at the same time player.dash({ x: 1, y: 0 }, 8, 200); player.oscillate({ x: 0, y: 1 }, 10, 1000); ``` ### Sequential Movements Chain movements using `CompositeMovement`: ```typescript theme={null} import { CompositeMovement, Dash, Knockback } from "@rpgjs/common"; // Create a sequence: dash forward, then knockback const sequence = new CompositeMovement('sequence', [ new Dash(8, { x: 1, y: 0 }, 200), new Knockback({ x: -0.5, y: 0 }, 3, 300) ]); player.addMovement(sequence); ``` ### Complex Combinations ```typescript theme={null} // Parallel movement: path following with oscillation const complexMovement = new CompositeMovement('parallel', [ new PathFollow(waypoints, 2), new Oscillate({ x: 0, y: 1 }, 10, 1000) ]); player.addMovement(complexMovement); ``` ## Creating Custom Movement Strategies ### Basic Custom Movement Implement the `MovementStrategy` interface: ```typescript theme={null} import { MovementStrategy } from "@rpgjs/common"; import * as Matter from 'matter-js'; class CircularMovement implements MovementStrategy { private elapsed: number = 0; private centerX: number; private centerY: number; constructor( center: { x: number, y: number }, private radius: number = 50, private speed: number = 0.05, private duration?: number ) { this.centerX = center.x; this.centerY = center.y; } update(body: Matter.Body, dt: number): void { this.elapsed += dt * this.speed; // Calculate circular position const x = this.centerX + Math.cos(this.elapsed) * this.radius; const y = this.centerY + Math.sin(this.elapsed) * this.radius; // Set velocity to reach target position const vx = (x - body.position.x) * 0.1; const vy = (y - body.position.y) * 0.1; Matter.Body.setVelocity(body, { x: vx, y: vy }); } isFinished(): boolean { return this.duration !== undefined && this.elapsed >= this.duration; } onFinished?(): void { console.log("Circular movement completed"); } } // Usage const center = { x: 200, y: 200 }; player.addMovement(new CircularMovement(center, 75, 0.03, 5000)); ``` ## Best Practices ### Performance Considerations 1. **Limit Active Movements**: Too many simultaneous movements can impact performance ```typescript theme={null} // Check movement count before adding new ones if (player.getActiveMovements().length < 3) { player.addMovement(newMovement); } ``` 2. **Clean Up Finished Movements**: The system automatically removes finished movements, but you can manually clean up if needed ```typescript theme={null} // Clear movements when changing states player.clearMovements(); ``` ### Movement Coordination 1. **Check Before Adding**: Prevent conflicting movements ```typescript theme={null} if (!player.hasActiveMovements()) { player.dash(direction); } ``` 2. **State-Based Movement**: Use different movements for different game states ```typescript theme={null} switch (gameState) { case 'normal': player.addMovement(new LinearMove(vx, vy)); break; case 'combat': player.dash(direction, 8, 200); break; case 'ice': player.applyIceMovement(direction, 4); break; } ``` ### Debugging Movements ```typescript theme={null} // Log active movements for debugging const movements = player.getActiveMovements(); console.log('Active movements:', movements.map(m => m.constructor.name)); // Check specific movement types const hasDash = movements.some(m => m instanceof Dash); const hasKnockback = movements.some(m => m instanceof Knockback); ``` # Create Shape Source: https://v5.rpgjs.dev/guide/create-shape Guide for Create Shape in RPGJS. # Create Shape ## Why ? A shape is an invisible zone on the map (it can be made visible, but not too interesting), which allows to trigger actions if you enter or leave this zone. The shape can be fixed or it can move on the map. It is also possible to attach an area to a player/event. For example, create a vision system # Create sounds Source: https://v5.rpgjs.dev/guide/create-sounds Register sounds on the client and play them from gameplay code. # Create sounds Sounds are declared on the client, then triggered from the client or the server. ## Register sounds on the client ```ts theme={null} import { provideClientModules } from "@rpgjs/client"; provideClientModules([ { sounds: [ { id: "item-pickup", src: "sounds/item-pickup.mp3" }, { id: "battle-theme", src: "sounds/battle-theme.mp3" } ] } ]); ``` ## Play a sound for one player ```ts theme={null} player.playSound("item-pickup"); ``` ## Play a sound for the whole map ```ts theme={null} map.playSound("battle-theme", { volume: 0.8, loop: true }); ``` ## Add map ambience Maps can automatically play sounds when the player joins: ```ts theme={null} provideServerModules([ { maps: [ { id: "town", sounds: ["town-bgm", "town-ambience"] } ] } ]); ``` ## More details See [Sounds Guide](/guide/sounds) for dynamic sound loading, stop strategies, and full audio examples. # Create spritesheet Source: https://v5.rpgjs.dev/guide/create-spritesheet Register your first character spritesheet in RPGJS. # Create spritesheet Spritesheets are registered on the client side. ## Basic setup Add your first spritesheet in `config/config.client.ts`: ```ts theme={null} import { provideClientModules, Presets } from "@rpgjs/client"; provideClientModules([ { spritesheets: [ { id: "hero", image: "spritesheets/hero.png", ...Presets.RMSpritesheet(3, 4) } ] } ]); ``` ## File location Place the image where the client can load it, for example: ```text theme={null} public/spritesheets/hero.png ``` ## Use the spritesheet Once registered, you can assign it from the server: ```ts theme={null} player.setGraphic("hero"); ``` ## Recommended flow 1. Create the image file 2. Register it in `config.client.ts` 3. Use `player.setGraphic("hero")` 4. Test it in your first map ## More options For advanced spritesheet setup, dynamic resolution, and presets, see [Spritesheets Guide](/guide/spritesheets). ## Next step Continue with [Create a world](/guide/create-world). # Create a world Source: https://v5.rpgjs.dev/guide/create-world Connect several maps together inside a world. # Create a world A world links several maps together with world coordinates. Use this when you want map-to-map transitions, overworld navigation, or a large connected game world. ## Declare world maps in a module You can register a world directly with `provideServerModules`: ```ts theme={null} import { provideServerModules } from "@rpgjs/server"; provideServerModules([ { worldMaps: [ { id: "main-world", maps: [ { id: "town", worldX: 0, worldY: 0, width: 800, height: 600 }, { id: "forest", worldX: 800, worldY: 0, width: 800, height: 600 } ] } ] } ]); ``` ## Create or update a world dynamically You can also do it from the server map API: ```ts theme={null} const manager = map.createDynamicWorldMaps({ id: "main-world", maps: [ { id: "town", worldX: 0, worldY: 0, width: 800, height: 600 }, { id: "forest", worldX: 800, worldY: 0, width: 800, height: 600 } ] }); ``` Or update it later: ```ts theme={null} await map.updateWorldMaps("main-world", [ { id: "town", worldX: 0, worldY: 0, width: 800, height: 600 }, { id: "forest", worldX: 800, worldY: 0, width: 800, height: 600 } ]); ``` ## Keep this page focused on creation This page covers the creation workflow only. For the full runtime API, see [Map API / World Maps](/api/map/world-maps). ## Next step Continue with [Create an event](/guide/create-event). # Create your first map Source: https://v5.rpgjs.dev/guide/create-your-first-map Add your first playable map to an RPGJS project. # Create your first map The simplest workflow is to use the Tiled integration. ## 1. Register the Tiled providers On the server: ```ts theme={null} import { createServer, provideServerModules } from "@rpgjs/server"; import mainServerModule from "./modules/server"; import { provideTiledMap } from "@rpgjs/tiledmap/server"; export default createServer({ providers: [ provideServerModules([mainServerModule]), provideTiledMap() ] }); ``` On the client: ```ts theme={null} import { provideClientGlobalConfig } from "@rpgjs/client"; import { provideTiledMap } from "@rpgjs/tiledmap/client"; export default { providers: [ provideTiledMap({ basePath: "map" }), provideClientGlobalConfig() ] }; ``` ## 2. Create the map file Create a TMX file in your Tiled folder, for example: ```text theme={null} src/tiled/simplemap.tmx ``` The map ID used by RPGJS must match the file name, so `simplemap.tmx` becomes the map id `simplemap`. ## 3. Declare the map in your module In your server module: ```ts theme={null} import { defineModule, type RpgServer } from "@rpgjs/server"; import { player } from "./player"; export default defineModule({ player, maps: [ { id: "simplemap" } ] }); ``` At this point, the map exists in the server and can be loaded by players. ## 4. Run the game When the player changes to `simplemap`, the client will load the matching Tiled map automatically. ## Tiled is optional `@rpgjs/tiledmap` is the easiest way to create maps with [Tiled Map Editor](https://www.mapeditor.org/), but it is not required. If you want your own renderer or your own map format, use [Custom map rendering with provideLoadMap](/advanced/provide-load-map). ## Next step Continue with [Create hero in map](/guide/create-hero-in-map). # Put an MMORPG online Source: https://v5.rpgjs.dev/guide/deploy-mmorpg Take an RPGJS starter from local MMORPG mode to a public Node or Cloudflare deployment. # Put an MMORPG online This guide starts with the RPGJS v5 starter and ends with a public URL that two players can open at the same time. You can deploy the same game server to Node in a Docker container or to Cloudflare Workers with Durable Objects. RPGJS starts in standalone RPG mode when `RPG_TYPE` is not set. Standalone mode runs the server in the browser. Always set `RPG_TYPE=mmorpg` when developing or building the multiplayer version. ## What changes in MMORPG mode An online RPGJS game has three parts: * the browser client renders the game and sends player input * an authoritative server validates gameplay and owns map state * a trusted publisher sends complete maps to that server Gameplay clients are never allowed to publish a map. The publisher uses a private `RPGJS_MAP_UPDATE_TOKEN`, compiles your local map source, and sends the authoritative payload to each map room. Keep this token out of `VITE_` variables, browser code, Git, and public build artifacts. ## 1. Create and test the project ```bash theme={null} npx degit rpgjs/starter#v5 my-rpg-game cd my-rpg-game npm install npm install --save-dev cross-env tsx ``` Add explicit scripts to `package.json`: ```json theme={null} { "scripts": { "dev": "vite", "dev:mmorpg": "cross-env RPG_TYPE=mmorpg vite", "build": "vite build", "build:mmorpg": "cross-env RPG_TYPE=mmorpg vite build", "publish:maps": "tsx src/entries/publish-maps.ts" } } ``` Start the MMORPG locally: ```bash theme={null} npm run dev:mmorpg ``` Open `http://localhost:5173` in two different browsers, or in one normal window and one private window. Both players must reach the same map and see each other before you continue. ## 2. Separate public and private map files In `vite.config.ts`, declare the MMORPG entries and publish only map images to the browser build: ```ts theme={null} import { defineConfig } from "vite"; import { rpgjs, tiledMapFolderPlugin } from "@rpgjs/vite"; import serverModule from "./src/server"; export default defineConfig({ plugins: [ tiledMapFolderPlugin({ sourceFolder: "./src/tiled", publicPath: "/map", buildOutputPath: "assets/data", allowedExtensions: [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"], }), ...rpgjs({ server: serverModule, entryPoints: { rpg: "./src/standalone.ts", mmorpg: { client: "./src/client.ts", server: "./src/server.ts", }, }, }), ], }); ``` The MMORPG build produces `dist/client` and `dist/server`; `.tmx` and `.tsx` files must not appear under `dist/client`. The Node path adds its Express adapter to this configuration later. Cloudflare compiles its Worker entry with Wrangler. ## 3. Add the trusted map publisher Create `src/entries/publish-maps.ts`: ```ts theme={null} import { createRpgServerTransport } from "@rpgjs/server/node"; import serverModule from "../server"; const target = process.env.RPGJS_PUBLISH_TARGET; const token = process.env.RPGJS_MAP_UPDATE_TOKEN; const mapIds = (process.env.RPGJS_MAP_IDS ?? "simplemap") .split(",") .map((id) => id.trim()) .filter(Boolean); if (!target) throw new Error("RPGJS_PUBLISH_TARGET is required"); if (!token) throw new Error("RPGJS_MAP_UPDATE_TOKEN is required"); if (mapIds.length === 0) throw new Error("RPGJS_MAP_IDS must contain a map id"); const publisher = createRpgServerTransport(serverModule, { initializeMaps: false, mapUpdateToken: token, tiledBasePaths: ["src/tiled"], }); for (const mapId of mapIds) { const response = await publisher.publishMap(mapId, { target }); if (!response.ok) { throw new Error( `Unable to publish ${mapId}: ${response.status} ${await response.text()}` ); } console.log(`Published map: ${mapId}`); } process.exit(0); ``` Add the publisher environment files to `.gitignore` before storing any real secret: ```gitignore theme={null} .env.production .env.publisher ``` The starter map id is `simplemap`. Replace it or set `RPGJS_MAP_IDS` to a comma-separated list for your project. `publishMap()` also publishes every `worldUpdates` entry in the generated payload, so connected rooms receive the same world topology. ## 4. Choose a deployment target Both targets run the same `src/server.ts` game module and use the publisher from the previous step. | Target | Runtime | Persistent room state | Best fit | | - | - | - | - | | [Node and Docker](/advanced/node-server-production) | One persistent Node process | SQLite file on a mounted volume | A container host where you control the process and disk | | [Cloudflare](/advanced/cloudflare-server-production) | Worker plus one Durable Object per room | Durable Object SQLite | Managed edge hosting without maintaining a Node process | Follow either page through its **First production deployment** section. Do not open the game to players until the initial map publication succeeds. ## 5. Verify the public game After deploying and publishing the maps: 1. Open the public HTTPS URL in two independent browser sessions. 2. Confirm that both players join the start map and see each other. 3. Move both players and interact with a server-controlled event. 4. Refresh one browser and confirm that it reconnects. 5. Check the server logs for authorization, map loading, or WebSocket errors. If the page loads but the map does not, check map publication first. A deployed client and server do not automatically mean that the authoritative map has been published. ## 6. Add accounts and long-term saves A deployed room and a saved character are two different things. Before opening an MMORPG to players, choose how accounts are authenticated and where character saves are stored. | Data | Purpose | Long-term character save? | | - | - | - | | WebSocket session | Reconnect a browser and transfer it between rooms | No | | Node or Durable Object room storage | Preserve synchronized room and map state | No | | `SaveStorageStrategy` | Store a player's save slots | Yes | The starter's `LocalStorageSaveStorageStrategy` is intended for a standalone server running inside the browser. It does not persist MMORPG saves on Node or Cloudflare. Without another strategy, the server falls back to memory-only storage, which is lost on restart and is not shared by several server instances. Use this beginner flow for persistent characters: 1. Implement [`auth()`](/advanced/auth) so the same account always receives the same `player.id`. 2. Implement a server-side [`SaveStorageStrategy`](/guide/save-load) backed by a shared database or trusted HTTP API. 3. Register it with `provideSaveStorage()` in the server configuration. 4. After authentication, either show the account's slots or explicitly call `player.load(slot)` for the slot your game selected. 5. Call `player.save(slot)` from server-controlled game logic or let the player request a manual save through the built-in save GUI. Authentication identifies the account, but it does not automatically load a save slot. Likewise, refreshing a browser may restore its current room session, but it does not call `player.load()` for you. Do not load the long-term save again on every map change. RPGJS already transfers the current player between map rooms. Read the save when starting or resuming a character; use room transfer for normal movement through the game world. # Display Animations Source: https://v5.rpgjs.dev/guide/display-animations Guide for Display Animations in RPGJS. # Display Animations This guide explains how to display animations in RPG-JS. There are three common types of animations you can create and display: 1. **Spritesheet Animations** - Using image spritesheets with the built-in animation system 2. **Custom Component Animations** - Creating your own Canvas Engine components for complex effects 3. **CanvasEngine FX Animations** - Using the prebuilt `Fx` component animation for particle effects If one server-side gameplay moment should trigger several client visuals at once, such as a flash, sound, hit text, and component animation, use [Client Visuals](/guide/client-visuals) to group those primitives on the client and send only one compact server event. ## Spritesheet Animations Finite character animations keep the direction they started with until playback finishes. Incoming locomotion updates do not interrupt them; afterward, a stationary character returns to idle, while a moving character resumes walking. This client rendering behavior applies to standalone and MMORPG games. Action Battle also preserves an ongoing predicted attack when the server confirms the same animation and graphic, instead of restarting it. > **See also:** [Spritesheets Guide](/guide/spritesheets) for comprehensive information about spritesheets, including dynamic spritesheet resolution. ### 1. Creating the Animation Spritesheet First, you need to create an animation spritesheet and add it to your module's spritesheets configuration. #### Using AnimationSpritesheetPreset The easiest way is to use the built-in `AnimationSpritesheetPreset` helper: ```ts theme={null} import { Presets } from "@rpgjs/client"; // In your client module configuration spritesheets: [ { id: "explosion", width: 1024, height: 1024, image: "explosion.png", ...Presets.AnimationSpritesheetPreset(4, 4), // 4x4 grid = 16 frames } ] ``` The `AnimationSpritesheetPreset(framesWidth, framesHeight)` automatically generates frame coordinates for a grid-based spritesheet. For example, `AnimationSpritesheetPreset(4, 4)` creates 16 frames arranged in a 4x4 grid. #### Manual Spritesheet Configuration You can also manually configure your spritesheet: ```ts theme={null} spritesheets: [ { id: "custom-animation", image: "my-animation.png", width: 512, height: 512, framesWidth: 8, framesHeight: 2, textures: { default: { animations: () => [ [ { time: 0, frameX: 0, frameY: 0 }, { time: 100, frameX: 1, frameY: 0 }, { time: 200, frameX: 2, frameY: 0 }, // ... more frames ] ], } } } ] ``` ### 2. Displaying Spritesheet Animations #### On a Player Use the `showAnimation` method on a player instance: ```ts theme={null} /** * Display an animation on a player * @param graphic - The spritesheet ID * @param animationName - Animation name (default: 'default') * @param replaceGraphic - Whether to replace the player's graphic (default: false) */ player.showAnimation(graphic: string, animationName: string = 'default', replaceGraphic: boolean = false) ``` **Examples:** ```ts theme={null} // Show explosion animation on player player.showAnimation("explosion"); // Show specific animation from spritesheet player.showAnimation("spell-effects", "fireball"); // Replace player graphic with animation player.showAnimation("transformation", "default", true); ``` #### On a Map Use the `showAnimation` method on a map instance: ```ts theme={null} /** * Display an animation at a specific position on the map * @param position - The x, y coordinates where to display the animation * @param graphic - The spritesheet ID * @param animationName - Animation name (default: 'default') */ map.showAnimation(position: { x: number, y: number }, graphic: string, animationName: string = 'default') ``` **Examples:** ```ts theme={null} // Show explosion at specific coordinates map.showAnimation({ x: 100, y: 200 }, "explosion"); // Show spell effect at player position const playerPos = { x: player.x, y: player.y }; map.showAnimation(playerPos, "spell-effects", "lightning"); ``` ## Custom Component Animations For more complex animations with custom logic, you can create Canvas Engine components. There are two different prop shapes in the client rendering pipeline: * scene components loaded with `provideLoadMap()` receive `data` and `params` * component animations rendered with `showComponentAnimation()` receive their props directly (`x`, `y`, `onFinish`, and your custom params) The examples below are for `componentAnimations`, not for custom map scene components. ### 1. Creating a Custom Animation Component Create a Canvas Engine component file (e.g., `ExplosionComponent.ce`): ```ts theme={null} ``` This direct prop shape matches how component animations are rendered in `draw-map.ce`: ```html theme={null} ``` By contrast, custom map scene components are mounted from `canvas.ce` with a `data` object: ```html theme={null} ``` ### 2. Registering the Component Animation Add your component to the module configuration: ```ts theme={null} import ExplosionComponent from "./components/ExplosionComponent.ce"; import { RpgClient, defineModule } from "@rpgjs/client"; export default defineModule({ componentAnimations: [ { id: "explosion", component: ExplosionComponent } ] }); ``` ### 3. Using the Prebuilt FX Component RPGJS includes a ready-to-use component animation wrapper for the CanvasEngine `Fx` preset system. Register it like any other component animation: ```ts theme={null} import { RpgClient, defineModule, PrebuiltComponentAnimations } from "@rpgjs/client"; export default defineModule({ componentAnimations: [ { id: "explosion", component: PrebuiltComponentAnimations.Fx }, { id: "heal", component: PrebuiltComponentAnimations.Fx } ] }); ``` Then call it with `showComponentAnimation()` and pass CanvasEngine `Fx` props: ```ts theme={null} map.showComponentAnimation("explosion", { x: 300, y: 200 }, { name: "explosionSmall", scale: 1.2, zIndex: 1000 }); player.showComponentAnimation("heal", { name: "healPulse", scale: 0.8 }); ``` Useful built-in `Fx` names include `hitSpark`, `slashSpark`, `impactBurst`, `smokePuff`, `dustStep`, `dashDust`, `magicBurst`, `healPulse`, `pickup`, `levelUp`, and `explosionSmall`. For looped FX, pass `displayDuration` to tell RPGJS when to remove the temporary component: ```ts theme={null} player.showComponentAnimation("aura", { name: "magicBurst", loop: true, displayDuration: 1200 }); ``` You can also pass a custom CanvasEngine `preset` object from client-side code. When triggering an animation from the server, keep params serializable; register a client wrapper component if the preset uses functions, Pixi textures, or other client-only objects. ### 4. Displaying Custom Component Animations #### On a Player Use the `showComponentAnimation` method. On a client sprite, it returns a promise that resolves when the component animation calls `onFinish`. ```ts theme={null} /** * Display a component animation on a player * @param id - The component animation ID * @param params - Parameters to pass to the component */ sprite.showComponentAnimation(id: string, params: any): Promise ``` **Examples:** ```ts theme={null} // Basic explosion on player await sprite.showComponentAnimation("explosion", {}); // Explosion with custom parameters await sprite.showComponentAnimation("explosion", { intensity: 2.5, color: "red", duration: 1500 }); // Hit indicator with damage text await sprite.showComponentAnimation("hit", { text: "150", color: "red" }); // Heal animation await sprite.showComponentAnimation("heal", { amount: 50, color: "green" }); ``` For a spritesheet animation on the same sprite, use `showAnimation()`: ```ts theme={null} await sprite.showAnimation("explosion", "default") ``` Server-side methods such as `player.showComponentAnimation()` and `map.showComponentAnimation()` only send the animation request to clients and remain fire-and-forget. #### On a Map Use the `showComponentAnimation` method on the map: ```ts theme={null} /** * Display a component animation at a specific position * @param id - The component animation ID * @param position - The x, y coordinates * @param params - Parameters to pass to the component */ map.showComponentAnimation(id: string, position: { x: number, y: number }, params: any) ``` **Examples:** ```ts theme={null} // Explosion at specific location map.showComponentAnimation("explosion", { x: 300, y: 400 }, { intensity: 3, duration: 2000 }); // Area effect at multiple positions const positions = [ { x: 100, y: 100 }, { x: 150, y: 120 }, { x: 200, y: 140 } ]; positions.forEach(pos => { map.showComponentAnimation("sparkle", pos, { color: "gold", size: 0.8 }); }); ``` ## Best Practices ### 1. Animation Performance * Keep spritesheet sizes reasonable (max 2048x2048) * Limit the number of simultaneous animations * Use `onFinish` callback to properly clean up component animations ### 2. Parameter Validation Always provide default values for component parameters: ```ts theme={null} const { duration, intensity } = defineProps({ duration: { default: 1000 }, intensity: { default: 1 } }); ``` ### 3. Memory Management Component animations are automatically cleaned up when `onFinish` is called. Always call this callback when your animation completes: ```ts theme={null} // In your component animation tick(({ deltaTime }) => { elapsedTime += deltaTime; if (elapsedTime >= duration()) { if (onFinish) { onFinish(); // Essential for cleanup } } }); ``` ### Fit a temporary client animation to a duration Use `sprite.setAnimation('attack', 1, { durationMs: 620 })` to play all poses of one cycle over 620 ms. The alternate-graphic overload also accepts this option: `sprite.setAnimation('attack', 'enemy-attack', 1, { durationMs: 620 })`. Without `durationMs`, the spritesheet's original frame timing is preserved. This changes client presentation only; server gameplay must control its own timing. Action Battle supplies this option from the enemy's active + recovery phase duration, through the serializable visual context's `animationDefaults.durationMs`. # Event Modes: Shared vs Scenario Source: https://v5.rpgjs.dev/guide/event-modes Guide for Event Modes: Shared vs Scenario in RPGJS. # Event Modes: Shared vs Scenario Events can run in two different modes: * `shared`: one authoritative event instance for everyone on the map. * `scenario`: one event instance per player (isolated view and state). Use `shared` when the world state must be global (enemies, public switches, moving NPCs).\ Use `scenario` when each player needs a personal progression (private chest, personal puzzle, solo cutscene state). ## Shared Mode In `shared` mode, all players see the same event: * same position * same direction * same movement route * same graphic/state * same collisions If one player moves or triggers that event, other players observe the same world change. ## Scenario Mode In `scenario` mode, each player gets their own event instance: * each player sees their own position/state for that event * each player can progress independently * event interactions and movement are isolated per player Typical case: a chest that can be opened individually for each player. ## How to Set the Mode (Function Style) You can define events with a simple function returning an object: ```ts theme={null} import { EventMode } from "@rpgjs/server"; export function SharedMonsterEvent() { return { name: "MonsterA", mode: EventMode.Shared, onInit() { this.setGraphic("monster"); }, }; } export function ScenarioChestEvent() { return { name: "ChestA", mode: EventMode.Scenario, onAction(player) { // each player opens their own chest instance player.gold += 100; }, }; } ``` `shared` is the default mode if you omit `mode`. ## Practical Rule of Thumb * Pick `shared` for gameplay that should affect everyone. * Pick `scenario` for content that should feel private per player. ## Performance Notes `scenario` duplicates events per connected player.\ If a map contains many scenario events, CPU/memory and collision cost increase with player count. Prefer: * only essential events in scenario mode * lightweight logic per scenario event * shared mode whenever isolation is not required # Event Touch and Map Variables Source: https://v5.rpgjs.dev/guide/event-touch-map-variables Build shared map interactions with onTouch, onTouchEnd, and persistent map variables. # Event Touch and Map Variables `onTouch` and `onTouchEnd` let an event react to any entity that collides with it: a player or another event. They are useful for pressure plates, push blocks, sensors, traps, doors, and shared MMO world state. Use map variables for state that belongs to the map room and should be shared by every player on that map. Use player variables for state that belongs only to one player. ## Touch Context ```ts theme={null} import type { EventDefinition, RpgTouchContext } from '@rpgjs/server' import type { RpgEvent, RpgPlayer } from '@rpgjs/server' export const PressurePlate: EventDefinition = { name: 'PressurePlate', onTouch(other: RpgPlayer | RpgEvent, context: RpgTouchContext) { context.map.setVariable('door.open', true) if (context.player) { context.player.setVariable('lastPlateTouched', this.id) } }, onTouchEnd(other: RpgPlayer | RpgEvent, context: RpgTouchContext) { context.map.setVariable('door.open', false) } } ``` The context contains: * `self`: the event running the hook * `other`: the player or event touching it * `otherType`: `'player'` or `'event'` * `player`: the player when this is a player/event touch * `phase`: `'start'` or `'end'` * `pairId`: stable id for that collision pair * `map`: the current `RpgMap` For player/event collisions, `onTouch` runs before the legacy `onPlayerTouch` hook. For event/event collisions, both events receive `onTouch` and `onTouchEnd`. ## Shared Map State ```ts theme={null} export const Door: EventDefinition = { name: 'Door', onInit() { this.through = false }, onChanges() { const map = this.getCurrentMap() const open = map?.getVariable('door.open') === true this.through = open this.setGraphic(open ? 'door-open' : 'door-closed') } } ``` Map variables are persistent room state: ```ts theme={null} map.setVariable('door.open', true) map.getVariable('door.open') map.hasVariable('door.open') map.removeVariable('door.open') map.getVariableKeys() map.clearVariables() ``` Writing, removing, or clearing a map variable automatically re-runs `onChanges` for visible players and events on that map. Player variables do the same for the player that owns the variable. Variable writes inside `onChanges` are guarded against recursive loops. Prefer making `onChanges` compute display and collision state from existing variables, and write variables from gameplay hooks such as `onTouch`, `onTouchEnd`, or `onAction`. # Getting Started Source: https://v5.rpgjs.dev/guide/get-started Understand what RPGJS gives you before building your first game. # Getting Started RPGJS is a TypeScript framework for building browser RPGs and MMORPGs with the same core architecture. The default stack is: * TypeScript for game code * Vite for dev and build * CanvasEngine for rendering * RPGJS server and client packages for gameplay and networking ## RPG or MMORPG With the same project structure, you can run: * a standalone RPG with `provideRpg(startServer)` * an MMORPG with `provideMmorpg()` That is why the starter contains both `client.ts` and `standalone.ts`. ## Recommended learning path * [Quick Start](/guide/quick-start) * [Structure](/guide/structure) * [Create your first map](/guide/create-your-first-map) * [Create hero in map](/guide/create-hero-in-map) * [Create spritesheet](/guide/create-spritesheet) * [Create a world](/guide/create-world) * [Create an event](/guide/create-event) * [Create database](/guide/create-database) * [Create sounds](/guide/create-sounds) ## Browser support The client targets modern browsers such as Chrome, Firefox, Edge, and Brave. Internet Explorer is not supported. # Attach GUI to Sprites Guide Source: https://v5.rpgjs.dev/guide/gui/attach-gui Guide for Attach GUI to Sprites Guide in RPGJS. # Attach GUI to Sprites Guide This guide explains how to use the `attachToSprite` option to attach GUI components directly to sprites in the game world. Use attached GUI when an interface should follow a sprite and still use GUI lifecycle and interaction APIs. For passive sprite visuals, use [Sprite Components](/guide/sprite-components) or [Authoritative Sprite Components](/guide/component). ## Overview By default, GUI components are displayed as fixed overlays on the screen. However, you can attach GUI components to sprites so they follow the sprite's position in the game world. This is useful for: * Tooltips that appear when hovering over a sprite * Name tags above characters * Interactive UI elements attached to specific sprites * Contextual menus that follow a sprite * Status indicators that move with the sprite Attached GUI differs from sprite components because it is opened, hidden, and driven through the GUI system. Sprite components are better for passive visual layers such as shadows, halos, labels, and bars. ## Basic Usage To attach a GUI component to sprites, set `attachToSprite: true` in your GUI configuration: ```typescript theme={null} import { defineModule, RpgClient } from '@rpgjs/client' import TooltipComponent from './components/tooltip.ce' defineModule({ gui: [ { id: "my-tooltip", component: TooltipComponent, attachToSprite: true } ] }) ``` ## Creating an Attached GUI Component Attached GUI components are Canvas Engine components (`.ce` files) that receive the sprite object as a prop. They are rendered within the sprite's container, so they automatically follow the sprite's position. ### Example: Simple Tooltip Component ```javascript theme={null} ``` The component receives the sprite `object` as a prop automatically, allowing you to access sprite properties: ```javascript theme={null}

Player: {object.name}

HP: {object.hp()} / {object.param.maxHp()}

``` > For more information about DOMContainer, see the [Canvas Engine documentation](https://canvasengine.net/components/dom-container.html) ## Controlling Display from Server Side Attached GUIs are controlled from the server side using the `showAttachedGui()` and `hideAttachedGui()` methods on the player object. ### Show Attached GUI Display the attached GUI for a player: ```typescript theme={null} // Show the attached GUI for this player player.showAttachedGui() // Show the attached GUI for other players player.showAttachedGui([otherPlayer1, otherPlayer2]) // Show for multiple players player.showAttachedGui([player1, player2, player3]) ``` ### Hide Attached GUI Hide the attached GUI for a player: ```typescript theme={null} // Hide the attached GUI for this player player.hideAttachedGui() // Hide the attached GUI for other players player.hideAttachedGui([otherPlayer1, otherPlayer2]) ``` ### Example: Show Tooltip on Hover ```typescript theme={null} // server.ts import { RpgPlayer, RpgMap } from '@rpgjs/server' export default { player: { onJoinMap(player: RpgPlayer, map: RpgMap) { // Show tooltip when player joins the map player.gui('my-tooltip').open() player.showAttachedGui() }, onLeaveMap(player: RpgPlayer, map: RpgMap) { // Hide tooltip when player leaves player.hideAttachedGui() } } } ``` ## Component Props Attached GUI components receive the following props: * `object`: The sprite object (RpgClientObject) - contains all sprite properties like position, health, name, etc. * `onFinish`: Callback function when the GUI finishes its action * `onInteraction`: Callback function for GUI interactions ### Example: Using Component Props ```javascript theme={null}
``` ### Example: Using Reactive Signals with Form Elements DOMContainer supports reactive two-way data binding for form elements using signals: ```javascript theme={null}

Player: {object.name}

You typed: {inputValue()}

``` ### Example: Using Event Handlers DOMContainer supports all standard DOM events. Use the event name directly (without "on" prefix): ```javascript theme={null}

Player: {object.name}

``` ### Example: Using CSS Classes You can apply CSS classes using different formats: ```javascript theme={null}

Player: {object.name}

Player: {object.name}

Player: {object.name}

``` ### Example: Using Inline Styles You can apply styles using string or object format: ```javascript theme={null}

Player: {object.name}

Player: {object.name}

``` ### Example: Using Forms with Automatic Data Collection DOMContainer automatically collects form data when a form is submitted: ```javascript theme={null}

Player: {object.name}

``` ## Supported Events DOMContainer supports all standard DOM events. Use the event name directly (without "on" prefix): * **Mouse events**: `click`, `mouseover`, `mouseout`, `mouseenter`, `mouseleave`, `mousemove`, `mouseup`, `mousedown` * **Touch events**: `touchstart`, `touchend`, `touchmove`, `touchcancel` * **Keyboard events**: `keydown`, `keyup`, `keypress` * **Form events**: `submit`, `reset`, `change`, `input`, `focus`, `blur` * **Drag events**: `drag`, `dragend`, `dragenter`, `dragleave`, `dragover`, `drop`, `dragstart` * **Other events**: `wheel`, `scroll`, `resize`, `contextmenu`, `select` For more details, see the [Canvas Engine DOMContainer documentation](https://canvasengine.net/components/dom-container.html#supported-events). # Skill and item hotbar Source: https://v5.rpgjs.dev/guide/hotbar Assign, theme, show, and hide persistent server-authoritative skill and item slots. # Skill and item hotbar RPGJS provides a persistent, server-authoritative ten-slot hotbar for learned skills, usable regular items, and plugin-provided entry types. Slot contents are synchronized to clients and included in player snapshots. The server remains responsible for validating ownership, consuming SP or items, and applying gameplay effects in standalone and MMORPG modes. ## Display and hide the built-in hotbar Open the prebuilt CanvasEngine GUI from a server player hook: ```ts theme={null} import type { RpgPlayer, RpgPlayerHooks } from "@rpgjs/server" const player: RpgPlayerHooks = { onJoinMap(player: RpgPlayer) { player.initializeHotbar() player.showHotbar() } } ``` `showHotbar()` initializes from the current loadout by default. Pass `autoInitialize: false` when the game needs to seed it later. Hiding the GUI does not clear persistent assignments: ```ts theme={null} player.hideHotbar() await player.showHotbar() ``` The default keyboard controls are `1` through `0`. Holding the left gamepad bumper opens the radial selector; choose with the left stick and release the bumper to use the slot. Games can replace these client bindings through their normal RPGJS control configuration. Bind the actions `hotbar1`, `hotbar2`, …, `hotbar0`; slot state never stores a physical keyboard or gamepad key. ## Start with items, skills, or both Pass an ordered list to initialize a specific loadout: ```ts theme={null} player.initializeHotbar([ { type: "item", id: "berry-snack" }, { type: "skill", id: "water-crops" }, { type: "item", id: "field-tonic" }, ]) ``` A skill must be learned. An item must be present in the inventory, usable, and consumable. Invalid initial entries are ignored. Calling `initializeHotbar()` again preserves player choices, so use assignment methods for later changes: ```ts theme={null} player.assignHotbarSlot(0, { type: "skill", id: "harvest" }) player.assignHotbarSlot(1, { type: "item", id: "berry-snack" }) player.clearHotbarSlot(1) ``` Slots use zero-based indexes. Assigning the same entry elsewhere moves it instead of duplicating it. When no explicit list is supplied, initialization places skills with numeric database keys first, then fills free slots with the remaining learned skills and usable consumable items. If the loadout is still empty, the state remains uninitialized so a later skill or inventory refresh can seed it. ## Assign from the main menu The built-in main menu supplies Items and Skills lists. Selecting an eligible entry exposes **Assign to hotbar**, followed by the slot picker: ```ts theme={null} player.callMainMenu() ``` The Items screen keeps separate Item, Weapon, and Armor tabs for browsing. Weapons and armors are managed from the Equip screen and never expose Use or Assign to hotbar. Regular items that are non-consumable, absent, or otherwise unusable cannot be assigned through the native item entry type. Register a custom entry type when a game needs tools, emotes, quests, or another authoritative action. The assignment action is also omitted from Items or Skills when that entry type is excluded by `allowedEntryTypes`. ## Configure capacity The persistent state always retains ten slots, while `capacity` controls how many are currently accessible: ```ts theme={null} player.configureHotbar({ capacity: current => Math.min(10, current.level + 2), lockedSlotHint: (_current, slot) => `Reach level ${slot + 1}`, }) ``` Reducing capacity preserves assignments in locked slots. Call `player.refreshHotbar()` after game-specific state changes that can affect a custom capacity resolver. Restrict a hotbar to items, skills, or another registered entry type with `allowedEntryTypes`: ```ts theme={null} player.configureHotbar({ capacity: 6, allowedEntryTypes: ["skill", "item"], }) ``` The option also accepts a per-player resolver. Entries excluded by the current configuration stay in the persistent ten-slot state, but are rendered as empty and cannot be assigned or used. Allowing their type again restores them without changing the player's saved layout. When the last copy of a consumable item is used from the native hotbar, its assignment is cleared automatically. Remaining stacks keep their assignment and update the quantity badge. Action Battle forwards the same configuration and can resolve visibility per player: ```ts theme={null} provideActionBattle({ ui: { hotbar: { enabled: player => player.level >= 2, autoOpen: true, capacity: player => Math.min(10, player.level + 2), allowedEntryTypes: ["skill"], }, }, }) ``` With `autoOpen`, the module re-evaluates `enabled` on map changes and closes an open hotbar when the resolver returns `false`. ## Theme the native component The built-in component uses CSS custom properties with defaults. Set them from a client stylesheet; replacing the component is not required: ```css theme={null} :root { --rpg-hotbar-font-family: "Trebuchet MS", sans-serif; --rpg-hotbar-gold: #7f9b55; --rpg-hotbar-gold-bright: #f8e7a8; --rpg-hotbar-wood: #8a542f; --rpg-hotbar-wood-dark: #3f261b; --rpg-hotbar-gap: 5px; --rpg-hotbar-slot-size: 58px; --rpg-hotbar-slot-radius: 4px; --rpg-hotbar-plate-radius: 8px; --rpg-hotbar-key-background: #f5d98b; } ``` ### Theme variables | Variable | Controls | | - | - | | `--rpg-hotbar-font-family` | Hotbar typography | | `--rpg-hotbar-dock-inset`, `--rpg-hotbar-dock-bottom` | Screen position | | `--rpg-hotbar-dock-shadow` | Complete dock filter | | `--rpg-hotbar-gap` | Space between slots | | `--rpg-hotbar-slot-size`, `--rpg-hotbar-mobile-slot-size` | Desktop and mobile slot size | | `--rpg-hotbar-gold`, `--rpg-hotbar-gold-bright` | Accent and active accent | | `--rpg-hotbar-wood`, `--rpg-hotbar-wood-dark` | Default plate gradient colors | | `--rpg-hotbar-plate-padding`, `--rpg-hotbar-plate-border`, `--rpg-hotbar-plate-radius` | Plate geometry | | `--rpg-hotbar-plate-background`, `--rpg-hotbar-plate-shadow` | Complete plate paint | | `--rpg-hotbar-slot-border`, `--rpg-hotbar-slot-radius` | Slot geometry | | `--rpg-hotbar-slot-background`, `--rpg-hotbar-empty-slot-background` | Filled and empty slot paint | | `--rpg-hotbar-slot-color`, `--rpg-hotbar-active-shadow` | Slot text and active effect | | `--rpg-hotbar-key-border`, `--rpg-hotbar-key-radius` | Shortcut badge geometry | | `--rpg-hotbar-key-background`, `--rpg-hotbar-key-color` | Shortcut badge paint | | `--rpg-hotbar-nameplate-*` | Active entry label border, radius, background, and colors | | `--rpg-hotbar-badge-background`, `--rpg-hotbar-badge-color` | Quantity and status badges | | `--rpg-hotbar-cost-color` | Resource cost | | `--rpg-hotbar-wheel-background` | Gamepad radial selector paint | The semantic `@rpgjs/ui-css` hotbar primitives remain available through the `--rpg-ui-hotbar-*` variables. Component-specific `--rpg-hotbar-*` values take precedence when both are provided. ## Replace the visual component For a different layout or markup, register another GUI using the same prebuilt id. The server APIs and persistent state remain unchanged: ```ts theme={null} import { PrebuiltGui } from "@rpgjs/common" import MyHotbar from "./gui/my-hotbar.ce" export default { gui: [{ id: PrebuiltGui.Hotbar, component: MyHotbar, renderer: "canvas", }] } ``` The last component registered for `PrebuiltGui.Hotbar` wins. See [Prebuilt GUI Contracts](/gui/prebuilt-contracts#hotbar) for the complete data and interaction contract. ## Use and observe slots Standard entries can be used directly on the authoritative server: ```ts theme={null} player.selectHotbarSlot(0) await player.useHotbarSlot(0, target) await player.useActiveHotbarSlot(target) ``` Battle or targeting modules can inspect `player.getHotbar()` and route the selected entry through their own authoritative action. `@rpgjs/action-battle` does this automatically, including cooldowns, projectiles, and targeting. Modules can observe every state transition: ```ts theme={null} export default { player: { onHotbarChange(player, change) { console.log(change.action, change.slot, change.entry, change.state) } } } ``` `change.action` is `initialize`, `assign`, `clear`, `select`, or `refresh`. `change.state` is a detached snapshot safe to inspect or send to another service. ## Add an entry type Plugins can register an authoritative type without changing the generic GUI: ```ts theme={null} import { registerHotbarEntryType } from "@rpgjs/server" const unregister = registerHotbarEntryType({ type: "tool", validate(player, id) { if (!player.getVariable(`tool.${id}`)) throw new Error("Tool unavailable") }, resolve(_player, id) { return { id, type: "tool", name: id, usable: true, activation: { mode: "instant" }, } }, use(player, id) { player.setVariable("lastTool", id) }, }) ``` The returned function restores the previous definition for that type. Register shared types during module/server setup and call the cleanup function when the owning integration is disposed. # Internationalization Source: https://v5.rpgjs.dev/guide/i18n Provide translations for server gameplay text, client menus, and reusable modules. # Internationalization RPGJS uses the same i18n shape on the server and the client. Modules can ship default translations, and the game can override them with `provideI18n()`. ## Configure Translations ```ts theme={null} // src/i18n.ts export default { defaultLocale: "fr", fallbackLocale: "en", messages: { fr: { "npc.guard.hello": "Bonjour {name}", "choice.fight": "Combattre", "rpg.menu.items": "Objets" }, en: { "npc.guard.hello": "Hello {name}", "choice.fight": "Fight", "rpg.menu.items": "Items" } } } ``` Use the same config on both sides when the game needs translated gameplay text on the server and translated local UI on the client. ```ts theme={null} // src/server.ts import { createServer, provideI18n, provideServerModules } from "@rpgjs/server" import i18n from "./i18n" export default createServer({ providers: [ provideI18n(i18n), provideServerModules([]) ] }) ``` ```ts theme={null} // src/config.client.ts import { provideI18n, provideClientModules } from "@rpgjs/client" import i18n from "./i18n" export default { providers: [ provideI18n(i18n), provideClientModules([]) ] } ``` ## Translate Gameplay Text The GUI APIs still receive final strings. Translate explicitly with `player.i18n()` or `player.t()`. ```ts theme={null} async onAction(player) { const { t } = player.i18n() await player.showText(t("npc.guard.hello", { name: player.name })) const choice = await player.showChoices(t("npc.guard.question"), [ { text: t("choice.fight"), value: "fight" }, { text: t("choice.run"), value: "run" } ]) } ``` ## Player Locale ```ts theme={null} player.setLocale("fr") player.getLocale() ``` The locale is stored in the player snapshot, so `save()`, `load()`, and session restore keep the selected language. ## Module Defaults and Game Overrides Reusable modules can provide default translations: ```ts theme={null} export const QuestModule = { server: { i18n: { en: { "quest.accepted": "Quest accepted" }, fr: { "quest.accepted": "Quête acceptée" } } }, client: { i18n: { en: { "quest.menu.title": "Quest Log" }, fr: { "quest.menu.title": "Journal de quêtes" } } } } ``` If the game provides the same key through `provideI18n()`, the game translation wins. Use namespaces such as `quest.*` or `my-module.*` for module keys. ## Client Menus Built-in RPGJS menus use `rpg.*` keys internally, such as `rpg.menu.items` and `rpg.save.title`. Override these keys in the game config to rename or translate the default menus. ## Choosing a language in Options The built-in Options menu lists only languages registered by the **game** through `provideI18n({ messages })`. Module/framework catalogues do not add choices. With no game catalogue, only `defaultLocale` is listed. Configure the same catalogues on client and server for both standalone RPG and MMORPG games. RPGJS ships English and French interface labels; provide `rpg.*` translations for other game languages. ```ts theme={null} client.getAvailableLocales() // e.g. ['en', 'fr'] client.setLocale('fr') // save an explicit choice for this project client.setLocale('auto') // return to browser-language selection client.getLocale() // resolved language, e.g. 'fr' client.getLocalePreference() // 'auto' or the explicit catalogue locale ``` `I18nService.getAvailableLocales()` exposes the same game-only list. Language names use their native spelling. Automatic mode tries each `navigator.languages` entry in order: exact locale first, then an available base language (`fr-CA` → `fr`). If none matches, it uses `defaultLocale`. An unavailable stored choice returns to automatic mode. Storage is scoped by `globalConfig.projectId` (or `_id`, then the page pathname); assign a unique project id when hosting several games together. Unavailable browser storage simply makes the preference session-only. Client `getLocale()` and `t()` participate in CanvasEngine reactive computations. Use `computed(() => client.t('menu.title'))` for derived labels, not a string translated once at component creation. Open built-in menus retain their current selection while their labels change. The selected language is included in each connection before gameplay join hooks, and subsequent changes use the `player.locale` action. The server validates the locale against its game catalogues (or its configured `defaultLocale` fallback) and changes only the sending player's locale. Reconnection and room transfers carry the current preference; loading an older save does not override a connected player's preference. Future `player.t()` calls use it. An already displayed dialogue and literal authored strings are not automatically retranslated. An offline snapshot still retains its saved locale until a current connection preference is applied. # Client interactions Source: https://v5.rpgjs.dev/guide/interactions Register client-side pointer behaviors for RPGJS map sprites. # Client interactions Register client-only pointer behaviors for map sprites. Use interactions for hover popovers, selection feedback, drag previews, cursor changes, and other CanvasEngine overlays. Nothing is sent to the server unless a behavior calls `ctx.action(...)`. ### Mental Model `interactions` is a client-side layer attached to rendered map sprites. It does not change gameplay state by itself. Use it for: * hover popovers * selection states * cursor changes * local highlights and drag previews * hitbox-based pointer filtering * explicit pointer-driven actions Do not use it as the authority for gameplay. When an interaction must change the world, call `ctx.action(...)` and validate the request on the server. ### Registering A Behavior ```ts theme={null} engine.interactions.use(target, behavior) ``` `target` can be: * a sprite/event id * an event name * a sprite `_type`, such as `"event"` or `"player"` * `"*"` for every sprite * a function receiving `{ client, target, sprite }` The returned function unregisters the behavior: ```ts theme={null} const stop = engine.interactions.use('Guard', hoverPopover(GuardPopover)) stop() ``` ### From A Client Module Use `interactions.setup()` when registering behaviors from a module: ```ts theme={null} import { defineModule, hoverPopover } from '@rpgjs/client' import GuardPopover from './components/GuardPopover.ce' export default defineModule({ client: { interactions: { setup(engine) { engine.interactions.use('Guard', hoverPopover(GuardPopover)) } } } }) ``` For simple lists, `interactions.use` is also accepted: ```ts theme={null} export default defineModule({ client: { interactions: { use: [ ['Guard', hoverPopover(GuardPopover)] ] } } }) ``` ### Hover Popover Register a CanvasEngine component as an overlay for an existing event: ```ts theme={null} import { hoverPopover } from '@rpgjs/client' import GuardPopover from './components/GuardPopover.ce' engine.interactions.use('Guard', hoverPopover(GuardPopover)) ``` The component receives: * `target` / `sprite`: the RPGJS client sprite * `state`: `{ hovered, pressed, selected, dragging, data, overlays }` * `bounds`: the default visual bounds * `hitboxBounds`: the gameplay hitbox bounds * `graphicBounds`: the rendered graphic bounds * `pointer`: the client pointer helper * `client`: the current `RpgClientEngine` In CanvasEngine components, values returned by `defineProps()` are prop accessors. Read the sprite with `sprite()` or `target()` before accessing its fields. Component bounds are local to the sprite, so `bounds()` can be used directly to draw overlays attached to that sprite. Example: ```html theme={null} @if (state().hovered) {
Parler a {target().name}
}
``` The popover is local only. Hovering the guard does not call the server. ### Client-Only Selection `selectable()` stores selection state locally. It does not send an action. ```ts theme={null} import { selectable } from '@rpgjs/client' engine.interactions.use('Chest', selectable()) ``` The overlay component can read `state().selected`: ```html theme={null} @if (state().selected) { } ``` ### Explicit Server Action Call `ctx.action(...)` only when the pointer gesture is meant to perform gameplay. This delegates to `engine.processAction(...)`. ```ts theme={null} engine.interactions.use('Guard', { cursor: 'pointer', click(ctx) { ctx.action('guard:talk', { eventId: ctx.target.id }) } }) ``` On the server, validate the request in the player input handler or a registered action. The client-sent `eventId` should be treated as intent, not authority. ### Hitbox-Based Interactions Use `hitTest()` to choose the clickable or draggable area. This is useful when a sprite graphic is larger than its gameplay body. Inside handlers and `hitTest()`, `ctx.bounds()` returns world-space bounds so it can be compared directly with `ctx.pointer.world()`. ```ts theme={null} engine.interactions.use('Tree', { cursor: 'pointer', hitTest(ctx) { return ctx.bounds('hitbox').contains(ctx.pointer.world()) } }) ``` Available bounds: * `ctx.bounds('hitbox')`: RPGJS gameplay hitbox * `ctx.bounds('graphic')`: rendered graphic bounds * `ctx.bounds()`: default bounds, currently graphic-first You can also implement custom areas: ```ts theme={null} engine.interactions.use('Tree', { hitTest(ctx) { const point = ctx.pointer.world() const box = ctx.bounds('graphic') if (!point) return false return ( point.x >= box.left && point.x <= box.right && point.y >= box.bottom - 24 && point.y <= box.bottom ) }, }) ``` ### Drag And Drop To A Tile `dragToTile()` starts a local drag state and sends an action on drop. ```ts theme={null} import { dragToTile } from '@rpgjs/client' engine.interactions.use('Crate', dragToTile({ action: 'crate:move' })) ``` The default payload is: ```ts theme={null} { eventId: ctx.target.id, position: ctx.pointer.tile() } ``` `ctx.pointer.tile()` returns: ```ts theme={null} { x: number, y: number, worldX: number, worldY: number, width: number, height: number } ``` Customize the payload with `data`: ```ts theme={null} engine.interactions.use('Crate', dragToTile({ action: 'crate:move', data(ctx) { return { crateId: ctx.target.id, tile: ctx.pointer.tile(), source: 'mouse' } } })) ``` Or handle the drop yourself: ```ts theme={null} engine.interactions.use('Crate', dragToTile({ onDrop(ctx) { const tile = ctx.pointer.tile() if (!tile) { ctx.cancel() return } ctx.action('crate:move', { eventId: ctx.target.id, tile }) } })) ``` ### Custom Drag Preview For full control, use low-level handlers: ```ts theme={null} engine.interactions.use('Crate', { cursor: 'grab', hitTest(ctx) { return ctx.bounds('hitbox').contains(ctx.pointer.world()) }, dragstart(ctx) { ctx.overlay.render(CrateGhost, { position: ctx.pointer.world() }) }, dragmove(ctx) { ctx.overlay.update({ position: ctx.pointer.world(), tile: ctx.pointer.tile() }) }, drop(ctx) { ctx.overlay.clear() ctx.action('crate:move', { eventId: ctx.target.id, position: ctx.pointer.tile() }) }, cancel(ctx) { ctx.overlay.clear() } }) ``` The overlay component can use any CanvasEngine primitive, including `DOMContainer`, `Graphics`, `Sprite`, or `Text`. ```html theme={null} @if (position()) { } ``` ### Low-Level Behavior API A behavior can define these handlers: ```ts theme={null} engine.interactions.use('Chest', { cursor: 'pointer', pointerenter(ctx) { ctx.overlay.render(ChestHint) }, pointerleave(ctx) { ctx.overlay.clear() }, pointerdown(ctx) { ctx.state.patch({ pressed: true }) }, pointerup(ctx) { ctx.state.patch({ pressed: false }) }, click(ctx) { ctx.select() } }) ``` Supported handler names: * `pointerenter` * `pointerleave` * `pointerover` * `pointerout` * `pointerdown` * `pointerup` * `pointermove` * `click` * `dragstart` * `dragmove` * `drop` * `cancel` ### Interaction Context Every handler receives `ctx`: ```ts theme={null} type ctx = { client: RpgClientEngine target: RpgClientObject sprite: RpgClientObject event?: unknown pointer: { screen(): { x: number, y: number } | null world(): { x: number, y: number } | null tile(): { x: number y: number worldX: number worldY: number width: number height: number } | null } bounds(kind?: 'bounds' | 'hitbox' | 'graphic' | string): Bounds state: { value(): InteractionState get(key: string): unknown set(key: string, value: unknown): void patch(patch: Partial): void } overlay: { render(component: any, props?: Record): void update(props?: Record): void clear(): void } select(selected?: boolean): void action(action: string | number, data?: any): void cancel(): void } ``` ### Helpers RPGJS exports small helpers for common cases: ```ts theme={null} hoverPopover(component, props?) selectable({ cursor?, onSelect? }) draggable({ cursor?, start?, move?, drop?, cancel? }) dragToTile({ action?, data?, onDrop?, cursor? }) ``` These helpers are only shortcuts. For project-specific UX, pass a behavior object directly to `engine.interactions.use(...)`. ### Network Rules * Pointer movement, hover, overlays, selection, drag previews, and cursor changes are client-only. * `ctx.overlay.*`, `ctx.state.*`, and `ctx.select()` do not send packets. * `ctx.action(...)` is the only interaction helper that sends an action to the server. * Server code must validate distance, permissions, target visibility, and map state before applying gameplay changes. # Items System Source: https://v5.rpgjs.dev/guide/items Guide for Items System in RPGJS. # Items System The items system in RPGJS allows you to manage player inventory, equipment, and item usage. You can add items to players in three different ways: using string IDs, item classes, or item objects. Studio regular items can also select a spritesheet animation, a personal sound, and/or a built-in particle preset for successful use. This feedback runs from the authoritative native `onUse` hook and can be combined with an item workflow. ## Adding Items ### Method 1: String ID (Pre-registered Items) Items must be registered in the `database` property of `provideServerModules` before they can be used with string IDs. ```ts theme={null} import { createServer, provideServerModules, RpgPlayer } from '@rpgjs/server' import { Item } from '@rpgjs/database' @Item({ name: 'Potion', description: 'Restores 100 HP', price: 200, hpValue: 100, consumable: true }) export class Potion { onAdd(player: RpgPlayer) { console.log('Potion added to inventory'); } onUse(player: RpgPlayer) { player.hp += 100; } } export default createServer({ providers: [ provideServerModules([ { database: { Potion // Register the item in the database }, player: { // ... your player hooks } } ]) ] }); ``` Then you can use it: ```ts theme={null} // In your server hooks or events player.addItem('Potion', 5); // Add 5 potions ``` > **Note**: The `@RpgModule` decorator is available for backward compatibility with v4, but the modern approach is to use `provideServerModules` with the `database` property directly. ### Method 2: Item Class (Auto-registered) When using an item class directly, it will be automatically added to the map's database if not already present. ```ts theme={null} import Potion from './database/Potion' // The class is automatically registered in the map database player.addItem(Potion, 3); ``` ### Method 3: Item Object (Dynamic Items) You can create items dynamically using objects. These are automatically added to the map's database. ```ts theme={null} // Create a dynamic item player.addItem({ id: 'custom-potion', name: 'Custom Potion', description: 'A special potion', price: 150, hpValue: 50, consumable: true, onAdd(player) { console.log('Custom potion added!'); }, onUse(player) { player.hp += 50; player.showText('You feel better!'); } }, 2); ``` If you don't provide an `id`, one will be auto-generated: ```ts theme={null} // Auto-generated ID player.addItem({ name: 'Dynamic Item', price: 100, onUse(player) { console.log('Dynamic item used!'); } }); ``` ## Dynamic Item Creation in Events When creating items dynamically (especially in events), you should first add them to the map's database, then add them to the player. ### Example: Event Giving an Item ```ts theme={null} import { RpgPlayer, RpgMap } from '@rpgjs/server' export function ChestEvent() { return { name: "CHEST-1", onInit() { this.setGraphic("chest"); }, async onAction(player: RpgPlayer) { const map = player.getCurrentMap(); // Step 1: Add the item to the map database const itemData = { id: 'treasure-coin', name: 'Treasure Coin', description: 'A rare coin found in a chest', price: 500, onAdd(player) { player.showText('You found a rare coin!'); }, onUse(player) { player.gold += 500; player.showText('You sold the coin for 500 gold!'); } }; // Add to database first map.addInDatabase('treasure-coin', itemData); // Step 2: Add the item to the player player.addItem('treasure-coin', 1); // Or add directly with the object (it will auto-register) // player.addItem(itemData, 1); } }; } ``` ### Using String ID After Dynamic Registration If you want to use a string ID after dynamically adding an item: ```ts theme={null} // In an event async onAction(player: RpgPlayer) { const map = player.getCurrentMap(); // Create and register the item const specialItem = { id: 'quest-item-1', name: 'Ancient Artifact', description: 'A mysterious artifact', price: 0, // Cannot be sold consumable: false }; // Register in database map.addInDatabase('quest-item-1', specialItem); // Now you can use string ID player.addItem('quest-item-1', 1); // Later, you can also use the string ID if (player.hasItem('quest-item-1')) { console.log('Player has the quest item!'); } } ``` ## Item Hooks Items can implement lifecycle hooks that are called at specific moments: * **`onAdd(player)`**: Called when the item is added to inventory * **`onUse(player)`**: Called when the item is successfully used * **`onUseFailed(player)`**: Called when item usage fails (e.g., chance roll) * **`onRemove(player)`**: Called when the item is removed from inventory * **`onEquip(player, equip)`**: Called when the item is equipped/unequipped Use `onUse` and `onUseFailed` for consumable items. Weapons and armors use `onEquip` instead and should not declare consumable-use hooks. When an item is used by string ID, RPGJS resolves object-based lifecycle hooks from the current map database. Updating that database entry therefore replaces the hook used by an item that is already present in the player's inventory. Class-based items keep using their instantiated class hooks. ### Using Classes ```ts theme={null} import { Item } from '@rpgjs/database' import { createServer, provideServerModules, RpgPlayer } from '@rpgjs/server' @Item({ name: 'Magic Sword', price: 1000 }) export class MagicSword { onAdd(player: RpgPlayer) { player.showText('You obtained the Magic Sword!'); } onEquip(player: RpgPlayer, equip: boolean) { if (equip) { player.showText('Magic Sword equipped!'); // Add special effects } else { player.showText('Magic Sword unequipped!'); } } } // Register in provideServerModules export default createServer({ providers: [ provideServerModules([ { database: { MagicSword } } ]) ] }); ``` ### Using Objects ```ts theme={null} import { createServer, provideServerModules, RpgPlayer } from '@rpgjs/server' const magicSword = { id: 'magic-sword', name: 'Magic Sword', price: 1000, onAdd(player: RpgPlayer) { player.showText('You obtained the Magic Sword!'); }, onEquip(player: RpgPlayer, equip: boolean) { if (equip) { player.showText('Magic Sword equipped!'); } else { player.showText('Magic Sword unequipped!'); } } }; // Register in provideServerModules export default createServer({ providers: [ provideServerModules([ { database: { 'magic-sword': magicSword // Use string key for object items } } ]) ] }); ``` ## Managing Database ### Adding Items to Database ```ts theme={null} const map = player.getCurrentMap(); // Add item class map.addInDatabase('Potion', PotionClass); // Add item object map.addInDatabase('custom-item', { name: 'Custom Item', price: 100 }); // Force overwrite existing item map.addInDatabase('Potion', UpdatedPotionClass, { force: true }); ``` ### Removing Items from Database ```ts theme={null} const map = player.getCurrentMap(); // Remove item const removed = map.removeInDatabase('Potion'); if (removed) { console.log('Item removed successfully'); } ``` ## Complete Example: Shop Event ```ts theme={null} import { createServer, provideServerModules, RpgPlayer, RpgMap } from '@rpgjs/server' import { Item } from '@rpgjs/database' // Define shop items as classes (optional, can also be objects) @Item({ name: 'Shop Potion', description: 'A potion sold in shops', price: 200, hpValue: 100, consumable: true }) export class ShopPotion { onUse(player: RpgPlayer) { player.hp += 100; player.showText('HP restored!'); } } @Item({ name: 'Iron Sword', description: 'A basic sword', price: 500 }) export class IronSword { onEquip(player: RpgPlayer, equip: boolean) { if (equip) { player.showText('Iron Sword equipped!'); } } } export function ShopEvent() { return { name: "SHOP-1", onInit() { this.setGraphic("shopkeeper"); }, async onAction(player: RpgPlayer) { const map = player.getCurrentMap(); // Option 1: Use pre-registered items from database await player.showText('Welcome to my shop!'); try { player.buyItem('ShopPotion', 1); player.showText('Thank you for your purchase!'); } catch (err) { player.showText('Not enough gold!'); } // Option 2: Create dynamic items on the fly const dynamicItem = { id: 'limited-edition-potion', name: 'Limited Edition Potion', description: 'A rare potion', price: 500, hpValue: 200, consumable: true, onUse(player) { player.hp += 200; player.showText('Super HP restored!'); } }; // Register in database first map.addInDatabase('limited-edition-potion', dynamicItem); // Then add to player player.addItem('limited-edition-potion', 1); } }; } export default createServer({ providers: [ provideServerModules([ { database: { ShopPotion, // Pre-registered items IronSword }, player: { // ... your player hooks }, maps: [ { id: "town", events: [{ event: ShopEvent() }] } ] } ]) ] }); ``` # Lighting Source: https://v5.rpgjs.dev/guide/lighting Guide for map lighting, light spots, shadows, and day/night transitions in RPGJS. # Lighting Manage map lighting from the server and add temporary client-only spots for local effects. Lighting is rendered by the engine with CanvasEngine presets. You do not need to import `NightAmbient` or `SpriteShadows` in your map components. ## Shared Types Lighting types are exported by `@rpgjs/common`: ```ts theme={null} import type { LightingState, LightSpot } from '@rpgjs/common' ``` ## Initial Map Lighting Set initial lighting in `@MapData()` or in a map entry inside your server module: ```ts theme={null} import { MapData, RpgMap } from '@rpgjs/server' @MapData({ id: 'forest-night', file: require('./tmx/forest.tmx'), lighting: { ambient: { darkness: 0.45, darkColor: '#0a1020', fogColor: '#141a2a', fogRadius: 0.5, fogSoftness: 0.35, fogOpacity: 0.35 }, spots: [ { x: 320, y: 180, radius: 160, intensity: 1, flicker: true } ], sun: { intensity: 0.35 }, shadows: { enabled: true, ambientLight: { x: -0.18, y: -1, z: 420, intensity: 0.18 }, minInfluence: 0.16, falloffPower: 1.2, scanHz: 8, cullToViewport: true } } }) export class ForestNightMap extends RpgMap {} ``` ## Day Night Cycle The `DayNightCycle` preset of CanvasEngine color grades the scene by the hour of the day (blue moonlight, pink dawn, neutral day, golden hour, purple dusk) and lights the light spots of the map when it is dark. Enable it on the map and describe the lights with `spots`: ```ts theme={null} { id: 'town', file: require('./tmx/town.tmx'), lighting: { dayNight: { enabled: true, lightIntensity: 1, vignette: 1 }, spots: [ // Lit when it is dark { id: 'lamp', x: 300, y: 320, radius: 180, flicker: true, halo: 0.4 }, // Lit between 19:00 and 23:00 (the hours may cross midnight) { id: 'shop', x: 500, y: 200, color: '#ffd9a0', schedule: [19, 23] } ] } } ``` * `halo` (`0` to `1`): glow of the light in the air. * `schedule` (`[on, off]`): hours the light is on. Without it, the light follows the darkness. * While `dayNight` is enabled, the `ambient` darkness overlay is not drawn. **Where does the hour come from?** Register the [Time Manager](./time-manager.md) module with `lighting: { dayNight: true }`: the server owns the hour and every player sees the same one. The `time`, `speed` and `paused` keys of `dayNight` are ignored then. **Without the Time Manager**, the map has its own local clock (the same on every client, but not synchronized by the server): `dayNight.time` (start hour, `18.5` is 18:30), `dayNight.speed` (game minutes per real **second**) and `dayNight.paused`. To keep a map out of the cycle, set `dayNight: { enabled: false }` on that map. ## Runtime API (Server) `RpgMap` exposes: * `map.getLighting(): LightingState | null` * `map.setLighting(next: LightingState | null, options?: { sync?: boolean })` * `map.patchLighting(patch: Partial, options?: { sync?: boolean })` * `map.clearLighting(options?: { sync?: boolean })` * `map.setDay(options?: { sync?: boolean })` * `map.setNight(options?: { sync?: boolean })` * `map.transitionLighting(toLighting, options?: { duration?: number, easing?: 'linear' | 'easeInOut' })` Example: ```ts theme={null} map.setNight() map.patchLighting({ spots: [ { id: 'campfire', x: 500, y: 420, radius: 190, intensity: 1.2, flicker: true } ] }) map.transitionLighting({ ambient: { darkness: 0.15 }, sun: { intensity: 1 }, shadows: { enabled: false } }, { duration: 3000, easing: 'easeInOut' }) ``` When `sync` is not `false`, lighting is broadcast to players in the map. ## Runtime API (Client) `RpgClientMap` exposes local light spot helpers: * `sceneMap.addLightSpot(id, spot)` * `sceneMap.patchLightSpot(id, partialSpot)` * `sceneMap.removeLightSpot(id)` * `sceneMap.clearLightSpots()` Client spots are not synchronized. They are useful for player torches, spells, cursor effects, or cutscenes. ```ts theme={null} const sceneMap = engine.sceneMap sceneMap.addLightSpot('player-torch', { x: player.position.x, y: player.position.y, radius: 180, intensity: 1, flicker: true }) sceneMap.patchLightSpot('player-torch', { x: player.position.x, y: player.position.y }) ``` Local spots are merged with synchronized map spots for rendering and are cleared when the client changes map. Spots do not make the map night by themselves; `NightAmbient` is rendered only when synchronized lighting provides an ambient darkness value greater than `0`, for example through `map.setNight()` or `map.setLighting({ ambient: { darkness: ... } })`. Ambient lighting fields map to CanvasEngine `NightAmbient` props: `darkness` and `darkColor` become the darkness overlay, while `fogColor`, `fogRadius`, `fogSoftness`, and `fogOpacity` configure the haze overlay. ## Shadows Shadows are opt-in: set `lighting.shadows.enabled: true` together with `lighting.sun` to get sun-driven shadows. Neither an active sun nor a light spot turns shadows on by itself. Once enabled, the engine automatically marks character sprites as shadow casters and derives default shadow sizing from the sprite bounds and hitbox. ```ts theme={null} map.patchLighting({ sun: { x: -0.45, y: -1, z: 520, intensity: 0.95 }, shadows: { enabled: true, mode: 'strongest', updateHz: 30, scanHz: 8, cullToViewport: true, minInfluence: 0.16, falloffPower: 1.2 } }) ``` When shadows are enabled, map light spots affect them even when the map is not in night mode. Sun shadows are rendered as a directional ambient source, so sprite, character, and Studio element shadows keep the same projection across the map instead of changing with distance. Spot lights are still passed as point lights to `SpriteShadows` for character shadows, so nearby torches or element `lightSpot` entries can pull character shadows opposite to the local source according to intensity, radius, and `shadowWeight`. Studio element shadows keep the sun angle whenever an active sun exists, which keeps trees and props visually aligned across the map. `ambientLight` overrides the derived sun direction; set it to `null` or `{ enabled: false }` to disable that directional baseline. In Studio maps, an element with a `lightSpot` is automatically registered as a local light spot, then used by `NightAmbient` when night is active and by shadows whenever the spot exists. For sun-driven shadows, the default sun behaves like a directional top-left light so sprite shadows project toward the bottom-right with minimal distance variation. Studio v2 rerenders element shadows when map lighting changes. Studio elements use cached Pixi shadow textures projected from the element sprite silhouette and clipped around the bottom of the hitbox, so the cast shadow follows the object shape instead of a generic blurred circle. The projected tail is tuned shorter than a low-sun cast shadow and the contact occlusion is anchored from the opaque pixels at the base of the sprite, including a compressed footprint from the sprite base with a small overlap behind the sprite so trunks and props stay visually attached to the ground without a large circular blob. Studio v2 renders terrain wall shadows as cached Pixi sprites generated from the bottom of each wall face, with a solid contact base and a softer projected tail. These wall shadows do not use `SpriteShadows`, so large wall shapes do not become dynamic shadow casters. Studio elements receive automatic sun shadows even when generated map data has `hasShadow: false`. # Move Routes Source: https://v5.rpgjs.dev/guide/move-routes Guide for Move Routes in RPGJS. # Move Routes The `moveRoutes` method allows you to give a character (player or event) a sequence of movement instructions to follow. This is useful for creating patrol paths, scripted movements, or complex movement patterns. ## Basic Usage The `moveRoutes` method accepts an array of movement instructions and returns a Promise that resolves when all routes are completed. ```ts theme={null} import { Move, Direction } from '@rpgjs/server' // Move right, then down, then left await player.moveRoutes([ Move.right(), Move.down(), Move.left() ]) ``` ## Movement Instructions ### Direction Enums You can use the `Direction` enum directly: ```ts theme={null} import { Direction } from '@rpgjs/common' await player.moveRoutes([ Direction.Right, Direction.Down, Direction.Left, Direction.Up ]) ``` ### Move Helper Functions The `Move` object provides convenient helper functions for common movements: ```ts theme={null} import { Move } from '@rpgjs/server' // Basic movements await player.moveRoutes([ Move.right(), // Move right 1 step Move.left(), // Move left 1 step Move.up(), // Move up 1 step Move.down() // Move down 1 step ]) // Move multiple steps await player.moveRoutes([ Move.right(3), // Move right 3 steps Move.down(2) // Move down 2 steps ]) // Random movement await player.moveRoutes([ Move.random(5) // Move randomly 5 times ]) ``` ### Tile-Based Movements Tile movements calculate the distance based on tile size and player speed: ```ts theme={null} // Move by tiles instead of steps await player.moveRoutes([ Move.tileRight(), // Move right by 1 tile Move.tileDown(2), // Move down by 2 tiles Move.tileLeft(), // Move left by 1 tile Move.tileUp() // Move up by 1 tile ]) ``` ### Turn Commands Turn commands change the character's direction without moving: ```ts theme={null} await player.moveRoutes([ Move.turnRight(), // Turn right Move.turnDown(), // Turn down Move.turnLeft(), // Turn left Move.turnUp() // Turn up ]) ``` ### Wait/Delay You can add delays between movements: ```ts theme={null} await player.moveRoutes([ Move.right(), Move.wait(0.5), // Wait 0.5 seconds Move.down(), Move.wait(1.0) // Wait 1 second ]) ``` ### Callback Functions You can use callback functions to generate dynamic routes: ```ts theme={null} await player.moveRoutes([ (player, map) => { // Generate route based on current state if (player.hp < 50) { return Move.left(3) // Retreat if low HP } return Move.right(2) // Advance otherwise }, Move.down() ]) ``` ### Nested Arrays Routes can be nested and will be automatically flattened: ```ts theme={null} await player.moveRoutes([ [Move.right(), Move.right()], // Move right twice [Move.down(), Move.down()] // Move down twice ]) ``` ## Advanced Features ### Stuck Detection When a character gets blocked by an obstacle or collision, you can handle it using the `onStuck` callback: ```ts theme={null} await player.moveRoutes([Move.right(5)], { onStuck: (player, target, currentPosition) => { console.log('Player is stuck!') console.log('Target:', target) console.log('Current position:', currentPosition) // Return false to cancel the route return false }, stuckTimeout: 500, // Time in ms before considering stuck (default: 500) stuckThreshold: 1 // Minimum distance change to consider progress (default: 1 pixel) }) ``` #### Stuck Detection Options * **`onStuck`**: Callback function called when the player is blocked * Parameters: * `player`: The player instance that is stuck * `target`: The target position the player was trying to reach * `currentPosition`: The current position of the player * Return value: * `false` or `undefined`: Cancel the route * `true`: Continue trying to reach the target * **`stuckTimeout`**: Time in milliseconds to wait before considering the player stuck (default: 500ms) * The player must be unable to make progress for this duration before `onStuck` is called * **`stuckThreshold`**: Minimum distance change in pixels to consider movement progress (default: 1 pixel) * If the player moves less than this distance over the `stuckTimeout` period, they are considered stuck #### Example: Handling Obstacles ```ts theme={null} // Player tries to move but gets blocked by a wall await player.moveRoutes([Move.right(10)], { onStuck: (player, target, currentPos) => { // Try an alternative path player.moveRoutes([Move.up(5), Move.right(10), Move.down(5)]) return false // Cancel the original route }, stuckTimeout: 300 }) ``` #### Example: Continue After Obstacle Removal ```ts theme={null} let obstacleRemoved = false await player.moveRoutes([Move.right(10)], { onStuck: (player, target, currentPos) => { // Remove obstacle and continue if (!obstacleRemoved) { removeObstacle() obstacleRemoved = true return true // Continue the route } return false // Give up if still stuck } }) ``` ## Combining Movement Types You can combine different movement types in a single route: ```ts theme={null} await player.moveRoutes([ Move.turnRight(), // Turn right Move.right(3), // Move right 3 steps Move.wait(0.5), // Wait half a second Move.turnDown(), // Turn down Move.tileDown(2), // Move down 2 tiles Move.turnLeft(), // Turn left Move.left() // Move left 1 step ]) ``` ## Player Movement For player-specific movements, you can use callbacks to check player state: ```ts theme={null} await player.moveRoutes([ Move.right(), (player, map) => { // Check if player can see an enemy const enemies = map.getPlayers().filter(p => p.team !== player.team) if (enemies.length > 0) { return Move.turnTowardPlayer(enemies[0]) // Turn toward nearest enemy } return Move.down() } ]) ``` ## Event Movement Events can also use moveRoutes for patrol patterns: ```ts theme={null} // In your event definition const GuardEvent = () => { return { name: 'Guard', onInit(event) { // Start infinite patrol event.infiniteMoveRoute([ Move.right(5), Move.turnDown(), Move.down(5), Move.turnLeft(), Move.left(5), Move.turnUp(), Move.up(5), Move.turnRight() ]) } } } ``` ## Infinite Routes For characters that need to repeat a movement pattern indefinitely: ```ts theme={null} // Start infinite route player.infiniteMoveRoute([ Move.right(3), Move.down(3), Move.left(3), Move.up(3) ]) // Stop the infinite route player.breakRoutes() // Force stop immediately player.breakRoutes(true) // Replay the infinite route player.replayRoutes() ``` ## Route Completion The `moveRoutes` method returns a Promise that resolves when all routes are completed: ```ts theme={null} const routePromise = player.moveRoutes([Move.right(5)]) routePromise.then(() => { console.log('Route completed!') }).catch((error) => { console.error('Route failed:', error) }) // Or use async/await try { await player.moveRoutes([Move.right(5)]) console.log('Route completed!') } catch (error) { console.error('Route failed:', error) } ``` # Prebuilt Components Source: https://v5.rpgjs.dev/guide/prebuilt-components/index Guide for Prebuilt Components in RPGJS. # Prebuilt Components RPGJS includes a collection of prebuilt sprite components that you can use directly in your game. These components are ready to use out of the box and can be customized through props to match your game's aesthetic. Use prebuilt components when RPGJS already provides the sprite visual effect you need. For custom client-side effects, see [Sprite Components](/guide/sprite-components). For server-controlled player visuals, see [Authoritative Sprite Components](/guide/component). ## Available Components ### Light Halo An animated light effect that creates a beautiful pulsing glow around sprites. Perfect for: * Atmospheric lighting effects * Magical auras and enchantments * Ambient glow for characters and objects * Dynamic lighting that responds to sprite state [Learn more about Light Halo →](./light-halo) ## Using Prebuilt Components All prebuilt components follow the same pattern as custom sprite components. You can use them in your module configuration: ```typescript theme={null} import { RpgClient, defineModule } from '@rpgjs/client'; import LightHalo from '@rpgjs/client'; export default defineModule({ sprite: { componentsBehind: [LightHalo] } }) ``` Or configure them with custom props: ```typescript theme={null} export default defineModule({ sprite: { componentsBehind: [{ component: LightHalo, props: { baseRadius: 50, lightColor: 0xffaaaa } }] } }) ``` ## See Also * [Components overview](/guide/components-overview) - Choose the right component system * [Sprite Components Guide](/guide/sprite-components) - Learn how to create and use sprite components * [CanvasEngine Documentation](https://canvasengine.net/) - Deep dive into the rendering engine # Light Halo Component Source: https://v5.rpgjs.dev/guide/prebuilt-components/light-halo Guide for Light Halo Component in RPGJS. # Light Halo Component The Light Halo component creates a beautiful animated light effect that pulses and moves around sprites. It's perfect for adding atmospheric lighting effects, magical auras, or ambient glow to characters and objects in your game. ## Overview The Light Halo component uses a radial gradient with blur effects to simulate realistic light diffusion. It features: * **Organic pulsing animation** - Uses multiple sine waves for natural breathing effects * **Opacity variations** - Smooth fade in/out with subtle flicker (candle-like effect) * **Customizable appearance** - Configurable size, color, opacity, and animation speeds * **Reactive props** - Supports both static and dynamic configuration based on sprite state ## Basic Usage ### Import the Component First, import the LightHalo component from the prebuilt components: ```typescript theme={null} import { RpgClient, defineModule } from '@rpgjs/client'; import LightHalo from '@rpgjs/client'; ``` ### Simple Configuration with Defaults Use the component with default settings (warm yellow-white light): ```typescript theme={null} export default defineModule({ sprite: { componentsBehind: [LightHalo] } }) ``` This will create a light halo behind all sprites with: * Base radius: 30 pixels * Warm yellow-white color (0xffffaa) * Smooth pulsing and fading animations ## Configuration Options All props are optional and have sensible defaults. You can customize any aspect of the light halo: ### Available Props | Prop | Type | Default | Description | | - | - | - | - | | `baseRadius` | `number` | `30` | Base radius of the light halo in pixels | | `radiusVariation` | `number` | `10` | Pulse variation range (halo pulses between `baseRadius ± radiusVariation`) | | `baseOpacity` | `number` | `0.6` | Base opacity of the light halo (0 to 1) | | `opacityVariation` | `number` | `0.3` | Opacity fade range (halo fades between `baseOpacity ± opacityVariation`) | | `sizeSpeed` | `number` | `0.002` | Animation speed for size pulsing (higher = faster) | | `opacitySpeed` | `number` | `0.003` | Animation speed for opacity fading (higher = faster) | | `lightColor` | `number` | `0xffffaa` | Color of the light halo (hex value, warm yellow-white by default) | ### Static Props Configuration Configure the component with static values for all sprites: ```typescript theme={null} import { RpgClient, defineModule } from '@rpgjs/client'; import LightHalo from '@rpgjs/client/src/components/prebuilt/light-halo.ce'; export default defineModule({ sprite: { componentsBehind: [{ component: LightHalo, props: { baseRadius: 50, radiusVariation: 15, baseOpacity: 0.8, opacityVariation: 0.2, lightColor: 0xffaaaa, // Reddish light sizeSpeed: 0.003, opacitySpeed: 0.004 } }] } }) ``` ### Dynamic Props Configuration Use a function to compute props based on sprite state. The function receives the sprite object as a parameter and can access reactive properties: ```typescript theme={null} import { RpgClient, defineModule } from '@rpgjs/client'; import { RpgClientObject } from '@rpgjs/client'; import LightHalo from '@rpgjs/client/src/components/prebuilt/light-halo.ce'; export default defineModule({ sprite: { componentsBehind: [{ component: LightHalo, props: (object: RpgClientObject) => { const level = object.param.level(); // Increase radius and change color based on level return { baseRadius: 30 + (level * 2), radiusVariation: 10 + (level * 0.5), lightColor: level > 10 ? 0xffaa00 : 0xffffaa, // Orange for high level, yellow for low baseOpacity: 0.6 + (level * 0.01) }; } }] } }) ``` ### Color Examples Here are some common color values you can use: ```typescript theme={null} // Warm candlelight lightColor: 0xffffaa // Cool blue light (magical) lightColor: 0xaaaaff // Reddish fire glow lightColor: 0xffaaaa // Green toxic glow lightColor: 0xaaffaa // Purple mystical aura lightColor: 0xffaaff // White pure light lightColor: 0xffffff // Orange intense flame lightColor: 0xffaa00 ``` ## Examples ### Example 1: Subtle Ambient Light A gentle, barely visible light for atmospheric effect: ```typescript theme={null} export default defineModule({ sprite: { componentsBehind: [{ component: LightHalo, props: { baseRadius: 20, radiusVariation: 5, baseOpacity: 0.3, opacityVariation: 0.1, sizeSpeed: 0.001, opacitySpeed: 0.002 } }] } }) ``` ### Example 2: Intense Magical Aura A vibrant, pulsing magical aura: ```typescript theme={null} export default defineModule({ sprite: { componentsBehind: [{ component: LightHalo, props: { baseRadius: 60, radiusVariation: 20, baseOpacity: 0.8, opacityVariation: 0.4, lightColor: 0xaaaaff, // Cool blue sizeSpeed: 0.004, opacitySpeed: 0.005 } }] } }) ``` ### Example 3: Level-Based Dynamic Light Light intensity increases with character level: ```typescript theme={null} export default defineModule({ sprite: { componentsBehind: [{ component: LightHalo, props: (object: RpgClientObject) => { const level = object.param.level(); const maxLevel = 50; // Scale from 20px to 80px based on level const progress = level / maxLevel; const baseRadius = 20 + (progress * 60); // Change color as level increases let lightColor; if (level < 10) lightColor = 0xffffaa; // Yellow else if (level < 25) lightColor = 0xffaa00; // Orange else if (level < 40) lightColor = 0xffaaaa; // Red else lightColor = 0xffaaff; // Purple return { baseRadius, radiusVariation: baseRadius * 0.3, baseOpacity: 0.5 + (progress * 0.3), lightColor }; } }] } }) ``` ### Example 4: Conditional Light Based on State Only show light when sprite has a certain state: ```typescript theme={null} export default defineModule({ sprite: { componentsBehind: [{ component: LightHalo, props: (object: RpgClientObject) => { // Only show light if sprite has "magical" state const hasMagic = object.states?.some(state => state.name === 'magical'); if (!hasMagic) { return { baseRadius: 0, // Hide by setting radius to 0 baseOpacity: 0 }; } return { baseRadius: 40, lightColor: 0xaaaaff, baseOpacity: 0.7 }; } }] } }) ``` ## Positioning The Light Halo is automatically centered on the sprite by default. It uses the sprite's hitbox to calculate the center position: * **X Position**: `hitbox.w / 2` (center horizontally) * **Y Position**: `hitbox.h / 2` (center vertically) The component is rendered behind the sprite (using `componentsBehind`), so it appears as a background glow effect. ## Performance Considerations * The component uses efficient blur filters that are created once and reused * Animation calculations are optimized using computed signals * The drawing function only renders when opacity and radius are greater than 0 For best performance when using multiple light halos: * Use lower `radiusVariation` values for subtler effects * Reduce `baseOpacity` if you don't need bright lights * Consider using dynamic props to conditionally disable lights when not needed ## Technical Details ### Animation Algorithm The component uses a sophisticated animation system: * **Size Animation**: Combines two sine waves with different frequencies for organic, less predictable pulsing * **Opacity Animation**: Main breathing cycle with a subtle high-frequency flicker for realistic candle-like effects * **Blur Effect**: Uses PixiJS BlurFilter with additive blend mode for realistic light diffusion ### Reactive System All props are reactive signals, meaning: * Static props are automatically converted to signals * Dynamic props (functions) can access reactive sprite properties * Changes to props automatically update the visual effect in real-time ## See Also * [Sprite Components Guide](/guide/sprite-components) - General guide on using sprite components * [CanvasEngine Reactive Programming](https://canvasengine.net/concepts/reactive.md) - Understanding signals and reactivity # Projectiles Source: https://v5.rpgjs.dev/guide/projectiles Emit server-authoritative projectiles and render them with CanvasEngine components. # Projectiles Use projectiles for arrows, spells, bullets, beams, and other temporary moving effects that need server-side impact validation without synchronizing their position every frame. The server is authoritative. It emits compact projectile spawn, impact, and destroy batches. The client predicts the visual movement locally, including a single local raycast at spawn time to avoid drawing projectiles past obvious client-side hitboxes while it waits for the server impact, and renders each projectile with a registered CanvasEngine component. When a projectile uses a custom server-side `canHit` filter, the client keeps predicting movement but skips local impact raycast clamping by default because arbitrary server gameplay rules cannot be represented safely on the client. If the filter still matches client-visible physics, opt back in with `collision.predictImpact: true`. ## Server Usage Emit a projectile from a player: ```ts theme={null} import { Direction, RpgPlayerHooks } from '@rpgjs/server' export const player: RpgPlayerHooks = { onInput(player, input) { if (input.action !== 'attack') return player.projectiles.emit({ type: 'fireball', direction: player.getDirection(), spreadDegrees: 8, trajectory: { type: 'linear', speed: 420, range: 600, ttl: 1.5 }, payload: { damage: 20, element: 'fire' }, params: { sprite: 'fireball', radius: 8, trail: true } }) } } ``` `spreadDegrees` applies a random direction offset of `+/- spreadDegrees / 2`. You can use `accuracy` instead when you prefer a normalized precision value from `0` to `1`; `1` is perfectly accurate, `0` can deviate up to 30 degrees. `payload` stays server-side and is useful for damage, states, knockback, or any other gameplay data. `params` is sent to clients and should contain only visual data used by the projectile component. You can also emit from the map: ```ts theme={null} map.projectiles.emit({ type: 'arrow', origin: { x: 100, y: 200 }, direction: Direction.Right, trajectory: { type: 'linear', speed: 500, range: 700 } }) ``` ## Shoot Toward The Pointer Projectiles can also use a direction vector. For pointer-based attacks, send the target position through the normal action input flow, then validate and resolve the shot on the server. ```ts theme={null} // client map component const target = client.pointer.world() if (target) { client.processAction('projectile:shoot', { source: 'map-click', target }) } ``` For keyboard attacks, configure the action key to send the same payload: ```ts theme={null} import { provideClientGlobalConfig } from '@rpgjs/client' provideClientGlobalConfig({ keyboardControls: { up: 'up', down: 'down', left: 'left', right: 'right', action: { bind: 'space', action: 'projectile:shoot', data: (client) => ({ source: 'keyboard', target: client.pointer.world() }) }, escape: 'escape' } }) ``` ```ts theme={null} // server player hook import type { RpgPlayerHooks } from '@rpgjs/server' function normalize(vector: { x: number, y: number }) { const length = Math.hypot(vector.x, vector.y) if (!Number.isFinite(length) || length <= 0) return null return { x: vector.x / length, y: vector.y / length } } export const player: RpgPlayerHooks = { onInput(player, input) { if (input?.action !== 'projectile:shoot') return const target = input.data?.target if (!target || !Number.isFinite(target.x) || !Number.isFinite(target.y)) { return } const hitbox = player.hitbox() const origin = { x: player.x() + hitbox.w / 2, y: player.y() + hitbox.h / 2 } const direction = normalize({ x: target.x - origin.x, y: target.y - origin.y }) if (!direction) return player.projectiles.emit({ type: 'fireball', origin, direction, trajectory: { type: 'linear', speed: 420, range: 600 }, payload: { damage: 20 } }) } } ``` This same input shape can be reused for future map or event interactions, for example `map:click` with a world position or `event:click` with an event id. The client sends intent and context; the server still owns validation, collisions, damage, and impacts. ## Repeats And Patterns Use `repeat` for a compact burst. The server broadcasts one spawn batch with per-projectile delays, so clients can render the whole burst without receiving a position update every frame. ```ts theme={null} player.projectiles.emit({ type: 'spark', direction: player.getDirection(), trajectory: { type: 'linear', speed: 700, range: 500 }, repeat: { count: 8, interval: 50, spread: 12, seed: true } }) ``` Use `pattern` for common shapes: ```ts theme={null} player.projectiles.emit({ type: 'ice-shard', direction: player.getDirection(), trajectory: { type: 'linear', speed: 480, range: 500 }, pattern: { type: 'cone', count: 5, angle: 45 } }) ``` ```ts theme={null} player.projectiles.emit({ type: 'magic-orb', trajectory: { type: 'linear', speed: 260, range: 400 }, pattern: { type: 'circle', count: 12 } }) ``` ## Impact Hooks Use projectile hooks to apply gameplay effects when the server confirms an impact: ```ts theme={null} import { RpgServer } from '@rpgjs/server' export const server: RpgServer = { projectiles: { onImpact({ projectile, target }) { if (!target) return const damage = Number(projectile.payload?.damage ?? 0) target.hp -= damage }, onDestroy({ projectile, reason }) { console.log(projectile.id, reason) } } } ``` You can also filter hits per projectile: ```ts theme={null} player.projectiles.emit({ type: 'holy-bolt', direction: player.getDirection(), trajectory: { type: 'linear', speed: 450, range: 700 }, canHit({ owner, target }) { return Boolean(target && target.team !== owner?.team) } }) ``` If the `canHit` filter only narrows server gameplay targets and the normal map physics hitboxes are still safe for visual prediction, enable local clamping: ```ts theme={null} player.projectiles.emit({ type: 'bolt', direction: player.getDirection(), trajectory: { type: 'linear', speed: 450, range: 700 }, collision: { ignoreOwner: true, predictImpact: true }, canHit({ target }) { return target?.id === 'target' || !target } }) ``` ## Client Registration Register a CanvasEngine component for each projectile type: ```ts theme={null} import { RpgClient } from '@rpgjs/client' import FireballProjectile from './components/fireball-projectile.ce' export const client: RpgClient = { projectiles: { components: { fireball: FireballProjectile } } } ``` ## Projectile Component Projectile components receive predicted movement props from the client runtime. ```html theme={null} ``` Common props: * `id`, `type`, `ownerId` * `x`, `y`, `origin`, `direction`, `angle` * `speed`, `range`, `ttl`, `distance`, `elapsed`, `progress` * `impactProgress` from `0` to `1` while the client keeps an impacted projectile visible for its impact animation * `index`, `count` for repeated or patterned projectiles * `params` for visual customization * `impact` when the server confirms a collision. While `impact` is present, `x`, `y`, and `distance` are pinned to the authoritative impact point. Before the server confirmation arrives, the client may also pin `x`, `y`, and `distance` to a locally predicted impact point; `impact` remains undefined until the server confirms the collision. Position and progress props (`x`, `y`, `angle`, `distance`, `elapsed`, `progress`, `impact`, `impactElapsed`, `impactProgress`, `destroyed`) are updated on every frame without rebuilding the component. Before an impact, `impact` is `null` and `impactElapsed` / `impactProgress` are `0`. Do not apply damage in the component. Components are visual only; gameplay effects belong in server projectile hooks. # Quick Start Source: https://v5.rpgjs.dev/guide/quick-start Create and run your first RPGJS project in a few minutes. # Quick Start Use the starter, install dependencies, and launch the dev server: ```bash theme={null} npx degit rpgjs/starter#v5 my-rpg-game cd my-rpg-game npm install npm run dev ``` Open `http://localhost:5173`. ## Choose MMORPG or standalone RPG By default, the starter runs as a standalone RPG. The server runs inside the browser in this mode, so other players cannot connect to it over the internet. Choose MMORPG mode explicitly when you want a separate authoritative server: ```bash theme={null} RPG_TYPE=mmorpg npm run dev ``` On Windows, use `cross-env`: ```bash theme={null} npm install --save-dev cross-env npx cross-env RPG_TYPE=mmorpg npm run dev ``` ## What to read next Follow this order: 1. [Getting Started](/guide/get-started) 2. [Structure](/guide/structure) 3. [Create your first map](/guide/create-your-first-map) 4. [Create hero in map](/guide/create-hero-in-map) 5. [Put an MMORPG online](/guide/deploy-mmorpg) # Save and load player data Source: https://v5.rpgjs.dev/guide/save-load Persist RPGJS save slots in standalone games and authenticated MMORPGs. # Save/Load Strategy (Client-Driven) This guide explains how to use the client-driven save/load flow introduced for RPGJS. The client requests the slot list and triggers save/load actions, while the server executes `player.save(slot)` and `player.load(slot)` using a pluggable storage strategy. ## Concepts * **Save storage strategy (server)**: decides where snapshots are stored (DB, file, memory). * **Auto-save strategy (server)**: decides when saving/loading is allowed and the auto slot. * **Save client service (client)**: requests slot list and triggers save/load actions. * **Slots metadata**: what the UI displays (level, exp, map, date, custom fields). ## Standalone and MMORPG storage The same save API works in both modes, but the correct storage is different: | Runtime | Suitable storage | | - | - | | Standalone RPG in the browser | `LocalStorageSaveStorageStrategy` | | Local test or short-lived server | Default memory-only strategy | | Production MMORPG | A server-side database or trusted HTTP API | MMORPG room storage is not save-slot storage. Room storage keeps synchronized room state and session mappings. A `SaveStorageStrategy` keeps long-term character snapshots and must be available to every server instance that can receive the player. In an authenticated MMORPG, use the stable `player.id` returned by [`auth()`](/advanced/auth) as the account key. Never accept the account id from a save request sent by the browser. ## Server: provide a storage strategy The server exposes `save.list`, `save.save`, and `save.load` actions. A storage strategy is injected via DI; if none is provided, a memory-only strategy is used. The server can also control whether saving/loading is allowed. ### Strategy contract ```ts theme={null} interface SaveStorageStrategy { list(player): Promise>; get(player, index): Promise; save(player, index, snapshot, meta): Promise; delete?(player, index): Promise; // storage only, no auto-save policy here } ``` ## Server: provide an auto-save strategy ```ts theme={null} interface AutoSaveStrategy { canSave?(player, context): boolean; canLoad?(player, context): boolean; shouldAutoSave?(player, context): boolean; getDefaultSlot?(player, context): number | null; } ``` The `getDefaultSlot()` function is the **auto slot**. When you call `player.save("auto")`, this strategy decides which slot is used (e.g. always slot 0, or last used slot). ### Register auto-save strategy Auto-save is disabled by default. During `player.syncChanges()`, RPGJS calls `shouldAutoSave()` and saves to the default slot only when it returns `true`. Do not return `true` on every synchronization: network storage could otherwise receive many overlapping writes. This example limits automatic saves to one attempt per player per minute: ```ts theme={null} import { createServer, provideAutoSave } from "@rpgjs/server"; const lastSaveAt = new Map(); const server = createServer({ providers: [ provideAutoSave({ canSave: (player) => player.hp > 0, getDefaultSlot: () => 0, shouldAutoSave(player) { const now = Date.now(); const previous = lastSaveAt.get(player.id) ?? 0; if (now - previous < 60_000) return false; lastSaveAt.set(player.id, now); return true; }, }), ], }); ``` For checkpoints, quest completion, or logout logic, you can instead call `await player.save("auto")` explicitly from the corresponding server hook. The auto-save policy selects the slot and can still allow or deny that operation; the storage strategy only decides where the snapshot is written. ### Save points (server authority) If you want to restrict saving to specific points, you can deny saves by default and only allow them when the player interacts with a save point. ```ts theme={null} import { provideAutoSave } from "@rpgjs/server"; let saveEnabled = false; const autoSave = { canSave: () => saveEnabled, getDefaultSlot: () => 0, }; // Somewhere in your event logic: // saveEnabled = true; player.showSave(); saveEnabled = false; ``` ### Register the strategy ```ts theme={null} import { createServer, provideSaveStorage } from "@rpgjs/server"; const server = createServer({ providers: [provideSaveStorage(new MyStorageStrategy())], }); ``` The server will build slot metadata automatically (level, exp, map, date), and you can extend it by passing custom fields from the client. ### Example: store MMORPG saves through an HTTP API The following example shows the complete adapter shape for a trusted remote API, such as an API hosted by your own backend or by Studio in the future. The `/saves/list`, `/saves/get`, `/saves/upsert`, and `/saves/delete` routes are illustrative: they are **not existing RPGJS Studio endpoints**. Replace them with the real contract of your service. ```ts theme={null} import type { RpgPlayer, SaveStorageStrategy } from "@rpgjs/server"; import type { SaveSlot, SaveSlotList, SaveSlotMeta } from "@rpgjs/common"; type StudioSaveOptions = { baseUrl: string; projectId: string; apiKey: string; }; export class StudioHttpSaveStorage implements SaveStorageStrategy { constructor(private readonly options: StudioSaveOptions) {} async list(player: RpgPlayer): Promise { const result = await this.post<{ slots: SaveSlotList }>("/saves/list", { projectId: this.options.projectId, playerId: this.playerId(player), }); return result.slots; } async get(player: RpgPlayer, index: number): Promise { const result = await this.post<{ slot: SaveSlot | null }>("/saves/get", { projectId: this.options.projectId, playerId: this.playerId(player), index: this.slotIndex(index), }); return result.slot; } async save( player: RpgPlayer, index: number, snapshot: string, meta: SaveSlotMeta ): Promise { // Server-owned fields overwrite any metadata received from the browser. const trustedMeta: SaveSlotMeta = { ...meta, level: player.level, exp: player.exp, map: player.getCurrentMap()?.id, date: new Date().toISOString(), }; await this.post("/saves/upsert", { projectId: this.options.projectId, playerId: this.playerId(player), index: this.slotIndex(index), snapshot, meta: trustedMeta, }); } async delete(player: RpgPlayer, index: number): Promise { await this.post("/saves/delete", { projectId: this.options.projectId, playerId: this.playerId(player), index: this.slotIndex(index), }); } private playerId(player: RpgPlayer): string { if (!player.id) throw new Error("Authenticated player id is required"); return player.id; } private slotIndex(index: number): number { if (!Number.isSafeInteger(index) || index < 0) { throw new Error("Save slot must be a non-negative integer"); } return index; } private async post(path: string, body: unknown): Promise { const response = await fetch(`${this.options.baseUrl}${path}`, { method: "POST", headers: { authorization: `Bearer ${this.options.apiKey}`, "content-type": "application/json", }, body: JSON.stringify(body), signal: AbortSignal.timeout(10_000), }); if (!response.ok) { throw new Error(`Save API failed with HTTP ${response.status}`); } if (response.status === 204) return undefined as T; return response.json() as Promise; } } ``` Register the adapter only in the server configuration. Read the URL, project id, and API key from server environment variables; never use a `VITE_` variable for the API key because those variables can be included in browser code. ```ts theme={null} provideSaveStorage( new StudioHttpSaveStorage({ baseUrl: process.env.SAVE_API_URL!, projectId: process.env.RPGSTUDIO_PROJECT_ID!, apiKey: process.env.RPGSTUDIO_API_KEY!, }) ); ``` The remote API should uniquely identify a slot by `projectId + playerId + index`. `list` returns metadata without snapshots, while `get` returns one object shaped like `{ snapshot, ...meta }`. The example expects `save` and `delete` to return `204 No Content`; `save` should perform an atomic insert-or-update. For production, also impose a maximum snapshot size and prevent an older concurrent request from replacing a newer save. Metadata received from a gameplay client is untrusted. Do not use client-provided `map`, `level`, `date`, account ids, or authorization fields to make server-side decisions. Validate or rebuild meaningful metadata in trusted server code or in the save API. ### Built-in localStorage strategy (standalone) For standalone mode (server running in the browser), use the built-in localStorage strategy. It stores full slots (meta + snapshot) under a single key and can carry an optional policy. ```ts theme={null} import { createServer, provideSaveStorage, LocalStorageSaveStorageStrategy, } from "@rpgjs/server"; const server = createServer({ providers: [ provideSaveStorage(new LocalStorageSaveStorageStrategy({ key: "save" })), ], }); ``` ## Client: request slots and trigger save/load The client uses `SaveClientService` to talk to the server. It is already included in `provideRpg()` and `provideMmorpg()`. ### Typical flow 1. Call `saveClient.listSlots()` to get the current slot list. 2. Show the Save/Load UI with those slots. 3. On interaction: * `saveClient.saveSlot(index)` for save. * `saveClient.loadSlot(index)` for load. These calls do not send a player/account id. The server obtains the authenticated `player` from the WebSocket action and passes it to the storage strategy. ## When an MMORPG save is loaded RPGJS does not choose a slot automatically. Your game must choose one of these flows after authentication: * list the slots and let the player select a character; * load a known slot, such as slot `0`; * remember the last selected slot in your account service; * initialize a new character when no slot exists. For example, a game with one character per account can load slot `0` from the server-side `onConnected` hook: ```ts theme={null} import type { RpgPlayerHooks } from "@rpgjs/server"; const player: RpgPlayerHooks = { async onConnected(player) { const result = await player.load( 0, { reason: "load", source: "login" }, { changeMap: true } ); if (!result.ok) { player.initializeDefaultStats(); // Continue with your new-character or title-screen flow. } }, }; ``` When loading succeeds, RPGJS applies the snapshot and, by default, moves the player to the map stored in the slot metadata. Do not repeat this load in `onJoinMap`: a map change already transfers the live player state between rooms. ### Refresh, room transfer, and returning later * **Map change:** RPGJS transfers the current session and player state. No save API read is required. * **Browser refresh:** RPGJS can reconnect the private session and room state. This is not a call to `player.load()`. * **New session or later visit:** `auth()` recovers the stable account id, then your login/title-screen flow must explicitly select and load a slot. ## Player API (server-side) * `player.snapshot()` -> returns the raw snapshot object (low-level). * `player.save()` -> returns a JSON snapshot string for v4 compatibility. * `player.save(slot)` -> stores a snapshot using the storage strategy. * Use `"auto"` to ask the policy `getDefaultSlot()` which slot to use. * `player.load(snapshot)` -> loads a JSON string or object snapshot for v4 compatibility. * `player.load(slot)` -> loads a slot using the storage strategy. Use `player.snapshot()` if you need to serialize or inspect state without saving. `RpgPlayerHooks.onLoad(player, snapshot)` runs after any snapshot is applied. `RpgPlayerHooks.onSave(player, snapshot)` runs before a slot snapshot is handed to the storage strategy. Both hooks receive the exported `RpgPlayerSnapshot` type. Examples: ```ts theme={null} const snapshot = await player.save(); // JSON snapshot string await player.load(snapshot); // restore from snapshot await player.save("auto"); // auto slot (policy) await player.save(2); // fixed slot await player.load(2); // fixed slot ``` ## GUI options (auto slot + save disabled) The save/load GUI can display a dedicated "Auto Save" slot at the top. It is read-only in save mode, and selectable in load mode. ### Menu GUI (server-side) `MenuGui.open()` accepts these options: * `saveShowAutoSlot` (boolean) -> show the auto slot in the GUI * `saveAutoSlotIndex` (number) -> which slot index to use for auto save * `saveAutoSlotLabel` (string) -> label displayed for auto slot `canSave` is computed from the AutoSaveStrategy and sent to the client; if false, the "Save" entry is disabled in the menu. ### Save/Load component (client-side) Props supported by the component: * `showAutoSlot` (boolean) * `autoSlotIndex` (number) * `autoSlotLabel` (string) When `showAutoSlot` is enabled: * **save mode**: auto slot is displayed but read-only * **load mode**: auto slot behaves like a normal slot and loads as usual ### Example (menu or title screen) ```ts theme={null} import { inject } from "@rpgjs/client"; import { RpgGui, SaveClientService } from "@rpgjs/client"; const gui = inject(RpgGui); const saveClient = inject(SaveClientService); async function openSaveMenu() { const slots = await saveClient.listSlots(); gui.display("rpg-save", { mode: "save", slots, onInteraction: async (action, { index }) => { if (action === "save") await saveClient.saveSlot(index); if (action === "load") await saveClient.loadSlot(index); }, }); } ``` ## Events sent by the server These are emitted to the client and handled by `SaveClientService`: * `save.list.result` -> `{ requestId, slots }` * `save.save.result` -> `{ requestId, index, slots }` * `save.load.result` -> `{ requestId, index, ok, slot }` * `save.error` -> `{ requestId, message }` ## Notes * Works in standalone and client/server modes. * Slot metadata is a free object, so you can display any custom fields. * The Save/Load GUI displays interactions. `SaveClientService` requests the slots, while the server storage strategy persists them. * Auto-save is disabled by default. Providing storage does not enable it. # Sounds Guide Source: https://v5.rpgjs.dev/guide/sounds Guide for Sounds Guide in RPGJS. # Sounds Guide This guide explains how to work with sounds in RPG-JS, including static sound configuration, playing sounds from the server, and dynamic sound resolution. ## Overview Sounds are audio files that can be played during gameplay to provide audio feedback for actions, events, and ambient effects. RPG-JS supports both pre-loaded static sounds and dynamic sound creation through a resolver system. ## Audio channels and player preferences The existing sound API supports four channels: `master`, `music`, `sfx`, and `ui`. Values range from `0` to `1` and are persisted in the browser for each project. ```ts theme={null} engine.setSoundVolume("master", 0.8); engine.setSoundVolume("music", 0.6); engine.setSoundVolume("sfx", 1); engine.setSoundVolume("ui", 0.5); const musicVolume = engine.getSoundVolume("music"); ``` The native Options menu provides accessible 0–100 sliders for the same channels. Map music, ambient sounds, Action Battle effects, and native UI cues consume these gains automatically. The legacy `RpgSound.global.volume(value)` call is kept as an alias of the Master channel. ## Semantic menu sounds Native menus use the semantic cues `navigate`, `confirm`, `cancel`, `open`, `close`, and `error`. RPGJS includes a small original neutral-fantasy pack generated at runtime, so these cues work without assets or attribution. Override the global Studio interface theme with `audio.ui`: ```ts theme={null} const config = { projectId: "my-game", audio: { ui: { navigate: "wood-cursor", confirm: { id: ["confirm-1", "confirm-2"], volume: 0.8, cooldownMs: 60 }, }, }, }; ``` The project theme applies to every native menu, then falls back to the built-in cue. Per-menu overrides are not used. In Studio, a missing or empty media field uses the built-in cue and a selected media file replaces it globally. `playSound()` remains the single API for ordinary and spatial effects. Passing positions enables distance attenuation and stereo panning; without positions it behaves exactly as before. ```ts theme={null} engine.playSound("hit", { channel: "sfx", position: { x: enemy.x(), y: enemy.y() }, listener: { x: player.x(), y: player.y() }, }); ``` ## Studio and Action Battle Studio stores interface defaults in `audio.ui` and the general Action Battle defaults in `combatAudio`, both in the Project Audio tab. Title-screen presentation belongs to `menus.titleScreen.settings`, with `backgroundMusic` and `backgroundImage`. Maps keep their normal background music and ambience but do not override combat audio. Enemies can override `audio.combat`, and skills keep their dedicated `sound` and `impactSound` fields. Action Battle resolves sounds in this order: * cast: skill `sound`, source enemy, project, built-in default; * impact: skill `impactSound`, source enemy, project, built-in default; * hurt/defeat: target enemy, project, built-in default. Combat effects use 2D spatial audio with full volume within 64 pixels, attenuation to silence at 640 pixels, and a maximum stereo pan strength of `0.75`. Battle music is optional: when no enemy or project battle track is configured, map music is not ducked. Music transitions are sequential: the current track fades out before the next track fades in, preventing two background tracks from playing audibly together. The built-in map renderer registers its BGM with the shared Howler music manager automatically. A custom map renderer can do the same when its source changes: ```ts theme={null} engine.music.setMap(map.params.backgroundMusic); ``` Passing `null` or `undefined` stops the owned map track. The manager applies the Music channel gain and coordinates that track with title-screen and battle overrides. The legacy project `audio.combat` shape and enemy `combatMusic` / `combatMusicPriority` fields remain supported by the runtime. ## Static Sounds ### Basic Configuration Add sounds to your client module configuration: ```typescript theme={null} import { defineModule, RpgClient } from '@rpgjs/client'; export default defineModule({ sounds: [ { id: "typewriter", src: "typewriter.wav", }, { id: "cursor", src: "cursor.wav", }, { id: "item-pickup", src: "sounds/item-pickup.mp3", }, ], }); ``` ### Sound Object Structure A sound object can have the following properties: ```typescript theme={null} interface SoundDefinition { id: string; // Unique identifier for the sound src: string; // Path to the audio file // Optional: If the sound object has a play() method, it will be used directly play?: () => void; // Custom play method (optional) stop?: () => void; // Custom stop method (optional) pause?: () => void; // Custom pause method (optional) } ``` If a sound object has a `play()` method, it will be used directly. Otherwise, the engine will create an `Audio` element from the `src` property. ## Playing Sounds from Server ### Using `player.playSound()` You can play sounds from the server side using the `playSound()` method on a player instance. **This method plays the sound only for the specific player**, making it ideal for personal feedback sounds. ```typescript theme={null} /** * Play a sound on the client side for this player only * @param soundId - Sound identifier, defined on the client side * @param options - Optional sound configuration * @param options.volume - Volume level (0.0 to 1.0, default: 1.0) * @param options.loop - Whether the sound should loop (default: false) */ player.playSound(soundId: string, options?: { volume?: number; loop?: boolean }): void ``` ### Using `player.stopAllSounds()` Stop all currently playing sounds for this specific player. This is useful when changing maps or resetting the audio state for a player. ```typescript theme={null} /** * Stop all currently playing sounds for this player * Useful when changing maps to prevent sound overlap. */ player.stopAllSounds(): void ``` ### Using `map.playSound()` To play a sound for **all players on the map**, use the `playSound()` method on the map instance. This is ideal for environmental sounds, battle music, or map-wide events. ```typescript theme={null} /** * Play a sound for all players on the map * @param soundId - Sound identifier, defined on the client side * @param options - Optional sound configuration * @param options.volume - Volume level (0.0 to 1.0, default: 1.0) * @param options.loop - Whether the sound should loop (default: false) */ map.playSound(soundId: string, options?: { volume?: number; loop?: boolean }): void ``` ### Using `map.stopSound()` Stop a specific sound for all players on the map. ```typescript theme={null} /** * Stop a sound for all players on the map * @param soundId - Sound identifier to stop */ map.stopSound(soundId: string): void ``` ### Examples #### Play Sound for Current Player Only ```typescript theme={null} // Play item pickup sound only for this player player.playSound("item-pickup"); // Play a notification sound with custom volume player.playSound("notification", { volume: 0.5 }); // Play background music for this player with loop player.playSound("background-music", { volume: 0.7, loop: true }); ``` #### Play Sound for All Players on Map ```typescript theme={null} // Play an explosion sound for everyone on the map map.playSound("explosion"); // Play battle music for all players with volume and loop map.playSound("battle-theme", { volume: 0.8, loop: true }); // Play a door opening sound at low volume map.playSound("door-open", { volume: 0.4 }); ``` ### Stopping Sounds You can stop sounds using `stopSound()` on either the player or map: ```typescript theme={null} // Stop a sound for this player player.stopSound("background-music"); // Stop a sound for all players on the map map.stopSound("battle-theme"); ``` #### Stop All Sounds You can also stop all currently playing sounds at once: ```typescript theme={null} // Stop all sounds for this player player.stopAllSounds(); // To stop all sounds for all players on a map, iterate through players const map = player.getCurrentMap(); map?.getPlayers().forEach(p => p.stopAllSounds()); ``` This is particularly useful when changing maps to prevent sound overlap. See the [Map Sounds Configuration](#map-sounds-configuration) section below for automatic sound stopping when joining maps. ## Map Sounds Configuration Maps can automatically play sounds when a player joins. These sounds are configured using the `sounds` property in the map configuration. ### Basic Map Sounds The simplest way to configure map sounds is using a plain object with `provideServerModules`: ```typescript theme={null} import { provideServerModules } from '@rpgjs/server'; provideServerModules([ { maps: [ { id: 'town', sounds: ['town-bgm', 'town-ambience'] // These sounds play when player joins } ] } ]) ``` ### Stop All Sounds Before Joining By default, sounds from the previous map continue playing when a player changes maps. If you want to stop all sounds before playing the new map's sounds, use the `stopAllSoundsBeforeJoin` option: ```typescript theme={null} import { provideServerModules } from '@rpgjs/server'; provideServerModules([ { maps: [ { id: 'battle-map', sounds: ['battle-theme'], stopAllSoundsBeforeJoin: true // Stop all previous sounds before playing battle theme } ] } ]) ``` **When to use `stopAllSoundsBeforeJoin: true`:** * Battle maps (to cut off peaceful background music) * Important cutscenes or story moments * Areas where you want a clean audio transition * Maps with their own distinct audio atmosphere **When to keep it `false` (default):** * Smooth transitions between similar areas * Continuous ambient sounds that should carry over * When you want sounds to layer naturally ### Complete Map Sound Example ```typescript theme={null} import { provideServerModules } from '@rpgjs/server'; provideServerModules([ { maps: [ // Peaceful town - sounds continue from previous map { id: 'town', sounds: ['town-bgm'] }, // Battle arena - stops all sounds for dramatic effect { id: 'battle-arena', sounds: ['battle-theme'], stopAllSoundsBeforeJoin: true }, // Boss room - stops all sounds and plays boss music { id: 'boss-room', sounds: ['boss-theme'], stopAllSoundsBeforeJoin: true } ] } ]) ``` ### Using defineModule You can also use `defineModule` to define your server configuration separately: ```typescript theme={null} import { defineModule, provideServerModules, type RpgServer } from '@rpgjs/server'; // Define your server module const serverModule = defineModule({ maps: [ { id: 'town', sounds: ['town-bgm'], stopAllSoundsBeforeJoin: false } ] }); // Then use it with provideServerModules provideServerModules([serverModule]) ``` ### Alternative: Using @MapData or @RpgModule (Legacy) For backward compatibility, you can still use decorators, though plain objects are now the recommended approach: ```typescript theme={null} import { MapData, RpgMap } from '@rpgjs/server'; @MapData({ id: 'town', sounds: ['town-bgm', 'town-ambience'], stopAllSoundsBeforeJoin: false }) class TownMap extends RpgMap {} ``` Or with `@RpgModule`: ```typescript theme={null} import { RpgServer, RpgModule } from '@rpgjs/server'; @RpgModule({ maps: [ { id: 'town', sounds: ['town-bgm'] } ] }) class MyServerModule {} ``` > **Note:** Using plain objects with `provideServerModules` or `defineModule` is the recommended approach. Decorators are still supported for backward compatibility but not required. ### Use Cases **Using `map.playSound()` (for all players):** * Environmental sounds (explosions, doors opening) * Battle sounds (combat start, victory fanfare) * Map-wide events (boss spawn, treasure chest opening) * Ambient effects that should be synchronized * Background music that everyone should hear **Using `player.playSound()` (for current player only):** * UI feedback sounds (menu navigation, button clicks) * Personal notifications (level up, achievement unlocked) * Item pickup sounds (only the player who picked it up hears it) * Private messages or alerts * Personal background music ### Complete Example ```typescript theme={null} import { provideServerModules, RpgPlayer, RpgPlayerHooks, RpgMap, RpgEvent } from '@rpgjs/server'; provideServerModules([ { player: { async onConnected(player: RpgPlayer) { // Stop all sounds when player first connects (clean state) player.stopAllSounds(); // Play welcome sound for this player only player.playSound("welcome", { volume: 0.6 }); }, async onLevelUp(player: RpgPlayer) { // Play level up sound for this player only player.playSound("level-up", { volume: 0.8 }); // Show level up animation player.showComponentAnimation("level-up", { text: `Level ${player.level}`, color: "gold" }); }, async onTouchEvent(player: RpgPlayer, event: RpgEvent) { if (event.name === "Treasure Chest") { const map = player.getCurrentMap(); // Play chest opening sound for everyone on the map map?.playSound("chest-open", { volume: 0.7 }); // Play item pickup sound only for this player player.playSound("item-pickup", { volume: 0.5 }); } }, async onBattleStart(player: RpgPlayer) { const map = player.getCurrentMap(); // Play battle music for everyone with loop map?.playSound("battle-theme", { volume: 0.8, loop: true }); }, async onBattleEnd(player: RpgPlayer) { const map = player.getCurrentMap(); // Stop battle music for everyone map?.stopSound("battle-theme"); // Play victory sound for this player player.playSound("victory", { volume: 1.0 }); } }, maps: [ { id: 'town', sounds: ['town-bgm'], stopAllSoundsBeforeJoin: false }, { id: 'battle-arena', sounds: ['battle-theme'], stopAllSoundsBeforeJoin: true } ] } ]); ``` ## Dynamic Sound Resolver The sound resolver allows you to create sounds on-the-fly when they are requested but not found in the cache. This is useful for: * Loading sounds from external APIs * Generating sounds programmatically * Creating sounds based on runtime data * Lazy loading sounds to reduce initial load time ### Configuration Add a `soundResolver` function to your client module: ```typescript theme={null} import { defineModule, RpgClient } from '@rpgjs/client'; export default defineModule({ soundResolver: (id: string) => { // Synchronous resolver if (id === 'dynamic-sound') { return { id: 'dynamic-sound', src: 'path/to/sound.mp3' }; } return undefined; // Return undefined if sound cannot be created }, }); ``` ### Asynchronous Resolver The resolver can also be asynchronous, useful for loading sounds from APIs or files: ```typescript theme={null} export default defineModule({ soundResolver: async (id: string) => { // Load from API try { const response = await fetch(`/api/sounds/${id}`); if (!response.ok) { return undefined; } const soundData = await response.json(); return { id: id, src: soundData.url, }; } catch (error) { console.error(`Failed to load sound ${id}:`, error); return undefined; } }, }); ``` ### Programmatic Generation You can also generate sounds programmatically: ```typescript theme={null} export default defineModule({ soundResolver: (id: string) => { // Generate sound path based on ID pattern if (id.startsWith('footstep-')) { const surface = id.split('-')[1]; // e.g., 'footstep-grass' return { id: id, src: `sounds/footsteps/${surface}.mp3`, }; } // Generate sound for weapon types if (id.startsWith('weapon-')) { const weaponType = id.split('-')[1]; return { id: id, src: `sounds/weapons/${weaponType}/swing.mp3`, }; } return undefined; }, }); ``` ### How It Works 1. When a sound is requested (e.g., via `player.playSound()`), the engine first checks the cache 2. If the sound is not found and a resolver is configured, the resolver is called with the sound ID 3. The resolved sound is automatically cached for future use 4. If the resolver returns `undefined` or `null`, the sound is not found and a warning is logged ### Resolver Function Signature ```typescript theme={null} type SoundResolver = (id: string) => SoundDefinition | Promise | undefined | null; ``` **Parameters:** * `id: string` - The sound ID that was requested **Returns:** * `SoundDefinition` - A sound configuration object (synchronous) * `Promise` - A Promise that resolves to a sound (asynchronous) * `undefined | null` - Indicates the sound cannot be created ### Programmatic API You can also set a resolver programmatically using the engine: ```typescript theme={null} import { RpgClientEngine, inject } from '@rpgjs/client'; // In a hook or initialization code const engine = inject(RpgClientEngine); engine.setSoundResolver((id: string) => { // Your resolver logic return soundDefinition; }); ``` ### Example: Loading from CDN ```typescript theme={null} export default defineModule({ soundResolver: async (id: string) => { const cdnUrl = `https://cdn.example.com/sounds/${id}.mp3`; try { // Verify the sound exists const response = await fetch(cdnUrl, { method: 'HEAD' }); if (!response.ok) { return undefined; } return { id: id, src: cdnUrl, }; } catch (error) { console.warn(`Sound ${id} not found on CDN`); return undefined; } }, }); ``` ### Example: Fallback Chain ```typescript theme={null} export default defineModule({ soundResolver: async (id: string) => { // Try local assets first try { const localPath = `/assets/sounds/${id}.mp3`; const response = await fetch(localPath, { method: 'HEAD' }); if (response.ok) { return { id: id, src: localPath, }; } } catch (error) { // Continue to fallback } // Fallback to CDN try { const cdnUrl = `https://cdn.example.com/sounds/${id}.mp3`; const response = await fetch(cdnUrl, { method: 'HEAD' }); if (response.ok) { return { id: id, src: cdnUrl, }; } } catch (error) { // Continue to fallback } // Fallback to default sound if (id.startsWith('footstep-')) { return { id: id, src: '/assets/sounds/default-footstep.mp3', }; } return undefined; }, }); ``` ## Combining Static and Dynamic You can use both static sounds and a resolver together. Static sounds are loaded first, and the resolver is only called for sounds not found in the static list: ```typescript theme={null} export default defineModule({ // Pre-loaded sounds sounds: [ { id: 'menu-click', src: 'sounds/menu-click.mp3' }, { id: 'notification', src: 'sounds/notification.mp3' }, ], // Resolver for dynamic sounds soundResolver: async (id: string) => { // Only called for sounds not in the static list above if (id.startsWith('dynamic-')) { return await loadDynamicSound(id); } return undefined; }, }); ``` ## Client-Side Sound API You can also play sounds directly from the client side using the engine: ```typescript theme={null} import { RpgClientEngine, inject } from '@rpgjs/client'; // In a client hook or component const engine = inject(RpgClientEngine); // Play a sound await engine.playSound('item-pickup'); // Play a sound with volume and loop await engine.playSound('background-music', { volume: 0.6, loop: true }); // Stop a sound engine.stopSound('background-music'); // Get a sound (with resolver support) const sound = await engine.getSound('dynamic-sound'); if (sound && sound.play) { sound.play(); } ``` ## Best Practices ### 1. Sound File Formats * Use compressed formats (MP3, OGG) for better performance * Provide multiple formats for browser compatibility: ```typescript theme={null} { id: 'background-music', src: 'music.ogg', // Fallback to MP3 if OGG not supported } ``` ### 2. Error Handling Always handle errors gracefully in async resolvers: ```typescript theme={null} soundResolver: async (id: string) => { try { // Load sound return sound; } catch (error) { console.error(`Error loading sound ${id}:`, error); return undefined; } } ``` ### 3. Performance * Use static sounds for frequently played sounds (UI feedback, common actions) * Use resolvers for sounds that are rarely used or loaded on-demand * Pre-load critical sounds in the static `sounds` array ### 4. ID Patterns Use consistent ID patterns to make resolver logic easier: ```typescript theme={null} soundResolver: (id: string) => { // Pattern: category-type-variant // Example: weapon-sword-swing, weapon-axe-swing const [category, type, variant] = id.split('-'); if (category === 'weapon') { return { id: id, src: `sounds/${category}/${type}/${variant}.mp3`, }; } return undefined; } ``` ### 5. Volume Management Consider implementing volume controls: ```typescript theme={null} // In your sound resolver or sound object { id: 'background-music', src: 'music.mp3', volume: 0.5, // 50% volume } ``` ### 6. Memory Management Resolved sounds are automatically cached. If you need to invalidate the cache: ```typescript theme={null} engine.sounds.delete('sound-id'); ``` ## Common Use Cases ### Dialog Box Sounds ```typescript theme={null} // In your client module sounds: [ { id: "typewriter", src: "typewriter.wav", }, { id: "cursor", src: "cursor.wav", }, ], // In your global config box: { sounds: { typewriter: "typewriter", cursorMove: "cursor", cursorSelect: "cursor", } } ``` ### Footstep Sounds ```typescript theme={null} // Server-side: Play footstep sound based on terrain const player: RpgPlayerHooks = { async onMove(player: RpgPlayer) { const terrain = player.getCurrentMap().getTerrainAt(player.x(), player.y()); player.playSound(`footstep-${terrain}`, false); // Only for this player } }; // Client-side: Resolver for footstep sounds soundResolver: (id: string) => { if (id.startsWith('footstep-')) { const surface = id.split('-')[1]; return { id: id, src: `sounds/footsteps/${surface}.mp3`, }; } return undefined; } ``` ### Weapon Sounds ```typescript theme={null} // Server-side: Play weapon sound when attacking const player: RpgPlayerHooks = { async onAttack(player: RpgPlayer, target: RpgPlayer) { const weapon = player.getEquippedWeapon(); if (weapon) { player.playSound(`weapon-${weapon.type}-swing`, true); // Everyone hears it player.playSound(`weapon-${weapon.type}-hit`, true); // Everyone hears it } } }; ``` ## See Also * [Spritesheets Guide](/guide/spritesheets) - Learn about dynamic spritesheet resolution (similar pattern) * [Display Animations Guide](/guide/display-animations) - Combine sounds with visual effects * [Dialog Box Guide](/gui/dialog-box) - Using sounds in dialog boxes # Sprite Components Guide Source: https://v5.rpgjs.dev/guide/sprite-components Guide for Sprite Components Guide in RPGJS. # Sprite Components Guide This guide explains how to use sprite components to add visual elements behind or in front of sprites in RPGJS. Use sprite components for client-side visual layers attached to sprites. If the server must decide which component is displayed for a player, use [Authoritative Sprite Components](/guide/component). If the element is an interactive interface, use [GUI](/gui/index). ## Overview RPGJS allows you to attach custom components to sprites that render either behind or in front of the main sprite graphics. This is useful for adding visual effects like shadows, health bars, status indicators, auras, or other UI elements that should be positioned relative to sprites. For persistent components controlled by the server for a specific player, see [Authoritative Sprite Components](/guide/component). That API reuses the same CanvasEngine component model, but the server decides which registered component id to display and with which serializable props. ## Component Properties ### `componentsBehind` Components in this array render **behind** the sprite with a lower z-index. Perfect for: * Shadow effects * Aura or glow effects * Ground-based visual elements * Background decorations ### `componentsInFront` Components in this array render **in front** of the sprite with a higher z-index. Perfect for: * Health bars * Status effect indicators * Damage numbers * Name tags * UI overlays ## Creating Sprite Components Sprite components are Canvas Engine components (`.ce` files) that receive the sprite object as a prop. ### Basic Component Structure ```javascript theme={null} ``` ### Health Bar Component Example ```javascript theme={null} ``` ## Configuration Methods ### Method 1: Module Configuration Configure components globally for all sprites in your module: ```typescript theme={null} // client.ts import { RpgClient, defineModule } from '@rpgjs/client'; import ShadowComponent from './components/shadow.ce'; import HealthBarComponent from './components/healthbar.ce'; export default defineModule({ sprite: { componentsBehind: [ShadowComponent], componentsInFront: [HealthBarComponent] } }) ``` ### Method 2: Engine Methods Add components dynamically using the engine: ```typescript theme={null} // During game initialization import { RpgClientEngine, inject } from '@rpgjs/client'; const engine = inject(RpgClientEngine); // Add components behind sprites engine.addSpriteComponentBehind(ShadowComponent); // Add components in front of sprites engine.addSpriteComponentInFront(HealthBarComponent); ``` ## Advanced Component Configuration Sprite components support advanced configuration options that allow you to: * Pass static or dynamic props to components * Control when components are displayed using dependencies * Wait for data to be ready before rendering ### Component Configuration Object Instead of passing a component directly, you can use a configuration object with three optional properties: ```typescript theme={null} { component: ComponentClass, // Required: The component to render props: Object | Function, // Optional: Props to pass to the component dependencies: Function // Optional: Dependencies that must be resolved } ``` ### Props Configuration The `props` property can be either a static object or a function that receives the sprite object and returns props dynamically. #### Static Props Pass a static object that will be used for all sprites: ```typescript theme={null} import { RpgClient, defineModule } from '@rpgjs/client'; import LightHalo from './components/light-halo.ce'; export default defineModule({ sprite: { componentsInFront: [{ component: LightHalo, props: { radius: 25, color: 0xffffaa } }] } }) ``` #### Dynamic Props (Function) Use a function to compute props based on the sprite's state. The function receives the sprite object as a parameter: ```typescript theme={null} import { RpgClient, defineModule } from '@rpgjs/client'; import { RpgClientObject } from '@rpgjs/client'; import LightHalo from './components/light-halo.ce'; export default defineModule({ sprite: { componentsInFront: [{ component: LightHalo, props: (object: RpgClientObject) => { // Access sprite properties reactively const level = object.param.level(); const baseRadius = 25; // Increase radius based on level return { radius: baseRadius + (level * 2), color: level > 10 ? 0xffaa00 : 0xffffaa }; } }] } }) ``` **Key Points:** * The function is called for each sprite instance * You can access sprite signals reactively (e.g., `object.hp()`, `object.param.level()`) * Props are recalculated when sprite state changes * The returned object is passed directly to the component as props ### Dependencies Configuration The `dependencies` property allows you to control when a component should be displayed. This is useful for waiting until certain data is available before rendering the component. #### How Dependencies Work * The `dependencies` function receives the sprite object and returns an array of signals or values * The component **only renders** when **all** dependencies are resolved (not `undefined`) * This prevents components from displaying with incomplete or missing data * Dependencies are checked reactively, so the component will appear/disappear as values change #### Basic Dependencies Example Wait for specific sprite properties to be available: ```typescript theme={null} import { RpgClient, defineModule } from '@rpgjs/client'; import { RpgClientObject } from '@rpgjs/client'; import { signal } from 'canvasengine'; import HealthBar from './components/healthbar.ce'; export default defineModule({ sprite: { componentsInFront: [{ component: HealthBar, props: (object: RpgClientObject) => ({ hp: object.hp(), maxHp: object.param.maxHp() }), dependencies: (object: RpgClientObject) => { // Component only displays when both hp and maxHp are defined return [object.hp, object.param.maxHp]; } }] } }) ``` #### Custom Dependencies with Signals You can create custom signals to control component visibility: ```typescript theme={null} import { RpgClient, defineModule } from '@rpgjs/client'; import { RpgClientObject } from '@rpgjs/client'; import { signal } from 'canvasengine'; import LightHalo from './components/light-halo.ce'; export default defineModule({ sprite: { componentsInFront: [{ component: LightHalo, props: (object: RpgClientObject) => ({ radius: 25 }), dependencies: (object: RpgClientObject) => { // Create a custom signal that starts as undefined const isReady = signal(undefined); // Simulate async data loading setTimeout(() => { isReady.set(true); }, 1000); // Component won't display until isReady is set return [isReady]; } }] } }) ``` #### Multiple Dependencies You can wait for multiple conditions: ```typescript theme={null} dependencies: (object: RpgClientObject) => { const customSignal = signal(undefined); const anotherValue = someAsyncOperation(); // Component displays only when ALL are defined: // - object.hp is defined // - object.param.maxHp is defined // - customSignal is defined // - anotherValue is defined return [ object.hp, object.param.maxHp, customSignal, anotherValue ]; } ``` ## Prebuilt Components RPGJS includes prebuilt sprite components that you can use directly in your game without creating them from scratch. These components are ready to use and highly customizable. ### Available Prebuilt Components * **[Light Halo](/guide/prebuilt-components/light-halo)** - An animated light effect that pulses and glows around sprites, perfect for atmospheric lighting, magical auras, or ambient glow effects. More prebuilt components will be added in future versions. Check the [Prebuilt Components](/guide/prebuilt-components/) section for the latest additions. # Spritesheets Guide Source: https://v5.rpgjs.dev/guide/spritesheets Guide for Spritesheets Guide in RPGJS. # Spritesheets Guide This guide explains how to work with spritesheets in RPG-JS, including static spritesheets and dynamic spritesheet resolution. ## Overview Spritesheets are image files containing multiple frames or tiles that are used to display characters, objects, and animations in your game. RPG-JS supports both pre-loaded static spritesheets and dynamic spritesheet creation through a resolver system. ## Static Spritesheets ### Basic Configuration Add spritesheets to your client module configuration: ```typescript theme={null} import { defineModule, RpgClient, Presets } from '@rpgjs/client'; export default defineModule({ spritesheets: [ { id: 'hero', image: 'hero.png', width: 192, height: 256, framesWidth: 3, framesHeight: 4, // ... animation configurations }, Presets.LPCSpritesheetPreset({ id: 'monster', imageSource: 'monster.png', width: 1728, height: 5568, ratio: 1.5, }), ], }); ``` ### Using Presets RPG-JS provides several preset helpers to simplify spritesheet configuration: * **LPCSpritesheetPreset**: For Liberated Pixel Cup (LPC) style character spritesheets * **FacesetPreset**: For character face expressions in dialog boxes * **AnimationSpritesheetPreset**: For animation sequences See the [Display Animations Guide](/guide/display-animations) for more details on animation spritesheets. ## Dynamic Spritesheet Resolver The spritesheet resolver allows you to create spritesheets on-the-fly when they are requested but not found in the cache. This is useful for: * Loading spritesheets from external APIs * Generating spritesheets programmatically * Creating spritesheets based on runtime data * Lazy loading spritesheets to reduce initial load time ### Configuration Add a `spritesheetResolver` function to your client module: ```typescript theme={null} import { defineModule, RpgClient } from '@rpgjs/client'; export default defineModule({ spritesheetResolver: (id: string) => { // Synchronous resolver if (id === 'dynamic-sprite') { return { id: 'dynamic-sprite', image: 'path/to/image.png', width: 192, height: 256, framesWidth: 3, framesHeight: 4, // ... other spritesheet properties }; } return undefined; // Return undefined if spritesheet cannot be created }, }); ``` ### Asynchronous Resolver The resolver can also be asynchronous, useful for loading spritesheets from APIs or files: ```typescript theme={null} export default defineModule({ spritesheetResolver: async (id: string) => { // Load from API try { const response = await fetch(`/api/spritesheets/${id}`); if (!response.ok) { return undefined; } const spritesheetData = await response.json(); return spritesheetData; } catch (error) { console.error(`Failed to load spritesheet ${id}:`, error); return undefined; } }, }); ``` ### Programmatic Generation You can also generate spritesheets programmatically: ```typescript theme={null} export default defineModule({ spritesheetResolver: (id: string) => { // Generate spritesheet based on ID pattern if (id.startsWith('generated-')) { const parts = id.split('-'); const type = parts[1]; const variant = parts[2]; return { id: id, image: `generated/${type}/${variant}.png`, width: 64, height: 64, framesWidth: 1, framesHeight: 1, // Generate animations based on type textures: { default: { animations: () => generateAnimationsForType(type), }, }, }; } return undefined; }, }); ``` ### How It Works 1. When a spritesheet is requested (e.g., when displaying a sprite), the engine first checks the cache 2. If the spritesheet is not found and a resolver is configured, the resolver is called with the spritesheet ID 3. The resolved spritesheet is automatically cached for future use 4. If the resolver returns `undefined` or `null`, the spritesheet is not found and will not be displayed ### Resolver Function Signature ```typescript theme={null} type SpritesheetResolver = (id: string) => SpritesheetDefinition | Promise | undefined | null; ``` **Parameters:** * `id: string` - The spritesheet ID that was requested **Returns:** * `SpritesheetDefinition` - A spritesheet configuration object (synchronous) * `Promise` - A Promise that resolves to a spritesheet (asynchronous) * `undefined | null` - Indicates the spritesheet cannot be created ### Spritesheet Definition A spritesheet definition is an object with the following properties: ```typescript theme={null} interface SpritesheetDefinition { id: string; // Unique identifier image: string; // Path to the image file width: number; // Width of the spritesheet image height: number; // Height of the spritesheet image framesWidth?: number; // Number of frames horizontally framesHeight?: number; // Number of frames vertically textures?: { // Animation configurations [key: string]: { animations: () => AnimationFrame[][]; }; }; // ... other spritesheet-specific properties } ``` ### Programmatic API You can also set a resolver programmatically using the engine: ```typescript theme={null} import { RpgClientEngine, inject } from '@rpgjs/client'; // In a hook or initialization code const engine = inject(RpgClientEngine); engine.setSpritesheetResolver((id: string) => { // Your resolver logic return spritesheetDefinition; }); ``` ### Best Practices 1. **Cache Management**: Resolved spritesheets are automatically cached. If you need to invalidate the cache, you can clear it manually: ```typescript theme={null} engine.spritesheets.delete('spritesheet-id'); ``` 2. **Error Handling**: Always handle errors gracefully in async resolvers: ```typescript theme={null} spritesheetResolver: async (id: string) => { try { // Load spritesheet return spritesheet; } catch (error) { console.error(`Error loading spritesheet ${id}:`, error); return undefined; } } ``` 3. **Performance**: Use resolvers for spritesheets that are not needed immediately. Pre-load critical spritesheets in the static `spritesheets` array. 4. **ID Patterns**: Use consistent ID patterns to make resolver logic easier: ```typescript theme={null} spritesheetResolver: (id: string) => { // Pattern: type-variant-color // Example: character-warrior-red const [type, variant, color] = id.split('-'); // ... } ``` ### Example: Loading from CDN ```typescript theme={null} export default defineModule({ spritesheetResolver: async (id: string) => { const cdnUrl = `https://cdn.example.com/spritesheets/${id}.json`; try { const response = await fetch(cdnUrl); if (!response.ok) { return undefined; } const config = await response.json(); // Ensure the image URL is also from CDN return { ...config, image: `https://cdn.example.com/images/${config.image}`, }; } catch (error) { console.warn(`Spritesheet ${id} not found on CDN`); return undefined; } }, }); ``` ### Example: Fallback Chain ```typescript theme={null} export default defineModule({ spritesheetResolver: async (id: string) => { // Try local assets first try { const localResponse = await fetch(`/assets/spritesheets/${id}.json`); if (localResponse.ok) { return await localResponse.json(); } } catch (error) { // Continue to fallback } // Fallback to CDN try { const cdnResponse = await fetch(`https://cdn.example.com/spritesheets/${id}.json`); if (cdnResponse.ok) { return await cdnResponse.json(); } } catch (error) { // Continue to fallback } // Fallback to default if (id.startsWith('character-')) { return { id: id, image: '/assets/default-character.png', width: 192, height: 256, framesWidth: 3, framesHeight: 4, }; } return undefined; }, }); ``` ## Combining Static and Dynamic You can use both static spritesheets and a resolver together. Static spritesheets are loaded first, and the resolver is only called for spritesheets not found in the static list: ```typescript theme={null} export default defineModule({ // Pre-loaded spritesheets spritesheets: [ { id: 'hero', image: 'hero.png', /* ... */ }, { id: 'npc-1', image: 'npc1.png', /* ... */ }, ], // Resolver for dynamic spritesheets spritesheetResolver: async (id: string) => { // Only called for spritesheets not in the static list above if (id.startsWith('dynamic-')) { return await loadDynamicSpritesheet(id); } return undefined; }, }); ``` ## See Also * [Display Animations Guide](/guide/display-animations) - Learn about animation spritesheets * [Sprite Components Guide](/guide/sprite-components) - Add visual components to sprites * [Dialog Box Guide](/gui/dialog-box) - Using facesets in dialogs # Structure Source: https://v5.rpgjs.dev/guide/structure Understand the default RPGJS project structure. # Structure The starter separates client boot, server boot, shared client config, and modules. ## `client.ts` `client.ts` starts the browser client in MMORPG mode: ```ts theme={null} import { startGame, provideMmorpg } from "@rpgjs/client"; import configClient from "./config/config.client"; startGame({ ...configClient, providers: [...configClient.providers, provideMmorpg({})], }); ``` Use this entry when the client connects to a remote RPGJS server. By default, MMORPG mode stores a stable session id in `localStorage` and passes it to the RPGJS room adapter as the connection session id. Refreshes and multiple tabs from the same browser therefore restore the same player session while each WebSocket keeps its own connection id. Use `connectionIdScope: "session"` to keep the session only for one browser tab, or `connectionIdScope: "ephemeral"` to create a new player session on each page load: ```ts theme={null} providers: [provideMmorpg({ connectionIdScope: "session" })] ``` When one host serves several games, give each game its own initial lobby room: ```ts theme={null} providers: [provideMmorpg({ room: `lobby-${projectId}` })] ``` If your server uses `engine.auth()`, send the token with the MMORPG connection query. RPGJS includes this query on the initial lobby connection and on map-room reconnects: ```ts theme={null} providers: [ provideMmorpg({ query: () => ({ token: localStorage.getItem("token") }) }) ] ``` Set `deferConnection: true` when a client GUI such as `@rpgjs/account` must run before credentials are available. The GUI calls `RpgClientEngine.connect()` after it obtains a token; without this option, startup connects automatically as before. ## `server.ts` `server.ts` creates the game server and registers your providers: ```ts theme={null} import { createServer, provideServerModules, LocalStorageSaveStorageStrategy } from "@rpgjs/server"; import mainServerModule from "./modules/server"; import { provideSaveStorage } from "@rpgjs/server"; import { provideTiledMap } from "@rpgjs/tiledmap/server"; export default createServer({ providers: [ provideServerModules([mainServerModule]), provideSaveStorage(new LocalStorageSaveStorageStrategy({ key: "save" })), provideTiledMap() ] }); ``` This is where you plug modules, maps, database content, save strategies, or server adapters. ## `standalone.ts` `standalone.ts` runs the client and server together for a standalone RPG: ```ts theme={null} import { provideRpg, startGame } from "@rpgjs/client"; import startServer from "./server"; import configClient from "./config/config.client"; startGame({ ...configClient, providers: [...configClient.providers, provideRpg(startServer)], }); ``` Use this entry when you want a single-player RPG running entirely from the client app. Because standalone mode runs the server inside the browser, its production bundle includes the code imported by `server.ts`. Do not put secrets or private service credentials in a standalone game bundle. ## `config/config.client.ts` `config.client.ts` contains the common client setup shared by MMORPG and standalone RPG: ```ts theme={null} import { provideClientGlobalConfig, provideClientModules, Presets } from "@rpgjs/client"; import mainClientModule from "../modules/client"; import { provideTiledMap } from "@rpgjs/tiledmap/client"; export default { providers: [ provideTiledMap({ basePath: "map", }), provideClientGlobalConfig(), provideClientModules([ mainClientModule, { spritesheets: [ { id: "hero", image: "spritesheets/hero.png", ...Presets.RMSpritesheet(3, 4) } ] } ]) ], }; ``` Put here everything the client always needs: map loading, global config, modules, spritesheets, sounds, and resolvers. ### `provideClientGlobalConfig()` `provideClientGlobalConfig()` is the client-side place for shared global configuration. It can hold built-in options such as `keyboardControls`, and also any custom object you want to expose everywhere in the client through dependency injection. ```ts theme={null} import { provideClientGlobalConfig } from "@rpgjs/client"; export default { providers: [ provideClientGlobalConfig({ keyboardControls: { up: "z", down: "s", left: "q", right: "d", action: "enter", escape: "escape" }, ui: { locale: "fr", showDamagePreview: true }, api: { baseUrl: "/api" } }) ] }; ``` If you omit `keyboardControls`, RPGJS injects the default bindings automatically: ```ts theme={null} { up: "up", down: "down", left: "left", right: "right", action: "space", dash: "shift", escape: "escape" } ``` Partial `keyboardControls` objects are merged with these defaults, so a game can override one key without redefining every movement/action binding. The `action` binding also accepts an object when the action key should send a custom action input. This keeps the key generic in built-in components while letting the game decide which action and payload to send. The `dash` binding is a movement input. In MMORPG mode the client predicts it locally through the same movement queue as directional inputs, while the server validates and acknowledges the dash position. ```ts theme={null} provideClientGlobalConfig({ keyboardControls: { up: "z", down: "s", left: "q", right: "d", action: { bind: "space", action: "projectile:shoot", data: (client, sprite) => ({ target: client.pointer.world(), source: "keyboard", playerId: sprite.id }) }, escape: "escape" } }) ``` ### Custom action inputs Custom action inputs use the same client-to-server flow whether they come from the configured action key or from code. The client sends a payload shaped like `{ action, data }`, and the server receives that payload in the player's `onInput()` hook. ```ts theme={null} // Client code client.processAction("projectile:shoot", { target: client.pointer.world(), source: "mouse" }) ``` ```ts theme={null} // Server player hook export const player = { onInput(player, input) { if (input.action !== "projectile:shoot") return const target = input.data?.target // Validate all client-provided data before using it. } } ``` Only the default `"action"` input, or `Control.Action`, automatically triggers nearby event `onAction()` hooks. A custom action such as `"projectile:shoot"` goes directly to `player.onInput()` and does not trigger event interactions by itself. Do not confuse this player hook with map movement input processing. Custom actions are sent with `client.processAction()` and handled in `player.onInput()`; movement inputs are queued separately by the map and processed by the movement loop. You can retrieve this global config anywhere on the client with `inject(GlobalConfigToken)`: ```ts theme={null} import { inject, GlobalConfigToken } from "@rpgjs/client"; import type { KeyboardActionConfig } from "@rpgjs/client"; const config = inject(GlobalConfigToken) as { keyboardControls: { up: string; down: string; left: string; right: string; action: string | KeyboardActionConfig; escape: string; }; ui?: { locale?: string; showDamagePreview?: boolean; }; }; console.log(config.keyboardControls.action); console.log(config.ui?.locale); ``` This is useful in client services, GUI components, or custom systems that need access to global input bindings or project-specific client configuration. ## `modules/server.ts` and `modules/client.ts` The main module keeps client and server ownership explicit. Define server hooks in `modules/server.ts`: ```ts theme={null} import { defineModule, type RpgServer } from "@rpgjs/server"; export default defineModule({ player: { onConnected(player) { console.log("Player connected", player.id); } } }); ``` Define visual behavior in `modules/client.ts`, then install each definition with the matching runtime provider as shown above. This keeps server code out of the client bundle and client components out of the server bundle when building an MMORPG. This isolation depends on the import graph: never import the server module from client code. See [Creating Modules](/guide/create-module) for the complete bundle-ownership rules and the standalone exception. # Synchronization between Server and Client Source: https://v5.rpgjs.dev/guide/synchronization Guide for Synchronization between Server and Client in RPGJS. # Synchronization between Server and Client In RPGJS, synchronization between the server and the client is a crucial aspect to ensure a consistent and immersive gameplay experience. This readme will guide you through the process of synchronizing data between the server and the client, utilizing schemas and hooks provided by RPGJS. Synchronized player properties can also drive server-controlled visuals. See [Authoritative Sprite Components](/guide/component) for components that update from placeholders such as `{name}`, `{hp}`, and `{param.maxHp}`. ## Understanding RPGJS Schemas and Data Synchronization RPGJS uses schemas to define the structure of data that needs to be synchronized between the server and the client. These schemas ensure that the properties of entities, such as players and items, are consistent and correctly communicated between the server and the client. Properties are implemented as signals, which provide reactivity - when a property changes using the `.set()` method, it is automatically sent to the client, keeping the game state up-to-date. Schemas work within the context of a "room" in RPGJS, where a room is essentially a map in the game world. It's important to note that when a player changes maps, synchronization can be lost if not handled properly. ## Best Practices for Synchronization ### Avoid Synchronization Issues After Changing Maps\*\* When a player changes maps, it's crucial not to continue executing the code in the `onConnected` method immediately. This is because the properties might not be synchronized with the client yet. Instead, use the `onJoinMap` hook to ensure synchronization before performing any actions. ```typescript theme={null} import { RpgPlayer, RpgPlayerHooks } from "@rpgjs/server"; export const player: RpgPlayerHooks = { async onConnected(player: RpgPlayer) { player.hitbox(32, 16); player.setGraphic("hero"); await player.changeMap("medieval", { x: 100, y: 100, }); }, // Synchronization is ensured in this hook onJoinMap(player: RpgPlayer) { player.hp = 500; }, }; ``` ### Custom Synchronization with Schemas\*\* You can also create custom synchronization by extending existing schemas or defining new ones. Example: ```typescript theme={null} import { RpgPlayerHooks, RpgPlayer, type RpgWritableSignal } from "@rpgjs/server"; declare module "@rpgjs/server" { export interface RpgPlayer { wood: RpgWritableSignal; } } export const player: RpgPlayerHooks = { props: { wood: Number, // Add the custom property }, onConnected(player: RpgPlayer) { player.wood.set(0); // Initialize the custom property using signal }, }; ``` With these steps, you've successfully added a custom property named `wood` to the player entity and ensured its synchronization with the client. The use of signals (`.set()` method) enables reactivity, which is essential for automatic synchronization. Any changes you make to this property will be automatically sent to the client, maintaining a consistent game state for all players. **Save custom props** In addition to synchronization, RPGJS provides a mechanism to save snapshots of player data, including custom properties, to databases. ```ts theme={null} import { RpgPlayerHooks, RpgPlayer, type RpgWritableSignal } from "@rpgjs/server"; declare module "@rpgjs/server" { export interface RpgPlayer { wood: RpgWritableSignal; } } export const player: RpgPlayerHooks = { props: { wood: Number, }, onConnected(player: RpgPlayer) { player.wood.set(0); }, async onDisconnect(player: RpgPlayer) { // Save the player data, including custom properties, to the database const snapshot = player.snapshot(); console.log(snapshot); // --> { ..., "wood": 0 } }, }; ``` **Save custom props but not synchronized with the client** ```ts theme={null} import { RpgPlayerHooks, RpgPlayer, type RpgWritableSignal } from "@rpgjs/server"; declare module "@rpgjs/server" { export interface RpgPlayer { secret: RpgWritableSignal; } } export const player: RpgPlayerHooks = { props: { secret: { $syncWithClient: false, }, }, onConnected(player: RpgPlayer) { player.secret.set("mysecretvalue"); const snapshot = player.snapshot(); console.log(snapshot); // --> { ..., "secret": "mysecretvalue" } }, }; ``` The custom property `secret` could be used for various purposes in your game. It might hold sensitive information that shouldn't be shared with the client, such as a player's authentication token, API keys, or any other private data. By configuring the property with `$syncWithClient: false`, you can control what data is sent to the client and what remains hidden on the server. This enables you to strike a balance between synchronization and data security. **Synchronize with client but do not register** ```ts theme={null} import { RpgPlayerHooks, RpgPlayer, type RpgWritableSignal } from "@rpgjs/server"; declare module "@rpgjs/server" { export interface RpgPlayer { message: RpgWritableSignal; } } export const player: RpgPlayerHooks = { props: { message: { $permanent: false, }, }, async onConnected(player: RpgPlayer) { player.message.set("custom message"); const snapshot = player.snapshot(); console.log(snapshot); // --> { ... } // does not include the message property }, }; ``` The `$permanent` configuration allows you to control whether a custom property should be persisted and stored as part of the player's data or not. This can be useful for temporary properties, session-specific information, or data that doesn't need to be preserved beyond the current session. For instance, you might use the non-permanent property to temporarily store a player's chat message before sending it to other players or logging it for debugging purposes. Since the property is not saved permanently, it won't clutter the saved data with transient information. ## Retrieving Synchronization on Client Side Once you have set up synchronization on the server side, you need to retrieve and use this synchronized data on the client side. ## Custom WebSocket Events Besides synchronized signals, you can also exchange custom websocket events between the server and the client. ### Receive server events on the client Inject the websocket service with `WebSocketToken`, then subscribe with `on()`. ```ts theme={null} import { inject } from "@rpgjs/client"; import { WebSocketToken, type AbstractWebsocket } from "@rpgjs/client"; const socket = inject(WebSocketToken); socket.on("weather:warning", (payload) => { console.log("Weather warning:", payload.level); }); ``` On the server, send the event with `player.emit()` for one player or `map.broadcast()` for the whole map. ```ts theme={null} player.emit("weather:warning", { level: "storm" }); map.broadcast("weather:warning", { level: "storm" }); ``` ### Send client events to the server Emit custom websocket events from the client with the same injected socket. ```ts theme={null} import { inject } from "@rpgjs/client"; import { WebSocketToken, type AbstractWebsocket } from "@rpgjs/client"; const socket = inject(WebSocketToken); socket.emit("chat:message", { text: "Hello server", }); ``` On the server, receive them with `player.on()` for one player or `map.on()` to listen at map level. ```ts theme={null} player.on("chat:message", ({ text }) => { console.log("Player message:", text); }); map.on("chat:message", (player, data) => { console.log(player.id, data.text); }); ``` ### Using the onInit Hook for Sprites The `onInit` hook is called when a sprite instance is created, but before the component is displayed. This is the perfect place to access synchronized properties and set up reactive data. ```typescript theme={null} import { provideClientModules } from "@rpgjs/client"; import { signal, effect } from "canvasengine"; export default { providers: [ provideClientModules([ { sprite: { onInit: (sprite) => { sprite.wood = signal(0); effect(() => { console.log("Player wood:", sprite.wood()); }); }, }, }, ]), ], }; ``` # Testing with @rpgjs/testing Source: https://v5.rpgjs.dev/guide/testing Guide for Testing with @rpgjs/testing in RPGJS. # Testing with @rpgjs/testing The `@rpgjs/testing` package provides utilities to test your RPGJS modules and game logic in a controlled environment. It sets up both server and client instances, allowing you to test player interactions, server hooks, and game mechanics. ## Installation The `@rpgjs/testing` package is already included in RPGJS projects. If you need to install it separately: ```bash theme={null} npm install @rpgjs/testing ``` ## Setup ### Vitest Configuration To use the testing utilities, configure Vitest to use the setup file provided by `@rpgjs/testing`. This setup file automatically mocks WebGL, images, and media elements for testing in a Node.js environment. ```typescript theme={null} // vitest.config.ts import { defineConfig } from 'vitest/config' export default defineConfig({ test: { setupFiles: ['@rpgjs/testing/dist/setup.js'], environment: 'node', globals: true } }) ``` The setup file automatically: * Mocks WebGL canvas using `vitest-webgl-canvas-mock` * Mocks image loading * Mocks HTML media elements (audio/video) * Creates a DOM container for the game ## Basic Usage ### Simple Test The simplest way to test is to use the `testing()` function without any modules: ```typescript theme={null} import { testing } from '@rpgjs/testing' import { beforeEach, test, expect } from 'vitest' import { RpgPlayer } from '@rpgjs/server' let player: RpgPlayer beforeEach(async () => { const fixture = await testing() const client = await fixture.createClient() player = client.player }) test('Player has default HP', () => { expect(player.hp).toBeGreaterThan(0) }) ``` ### Understanding the Testing Fixture The `testing()` function returns a fixture object with a `createClient()` method: ```typescript theme={null} const fixture = await testing() const client = await fixture.createClient() ``` The `client` object provides access to: * **`server`**: The RpgServer instance * **`socket`**: The WebSocket connection * **`client`**: The RpgClientEngine instance * **`playerId`**: The unique identifier of the player * **`player`**: A getter that returns the RpgPlayer instance ```typescript theme={null} const client = await fixture.createClient() // Access the server const server = client.server // Access the client engine const engine = client.client // Access the player const player = client.player // Access the player ID const playerId = client.playerId ``` ## Testing with Custom Modules You can test your custom modules by passing their server and client definitions to the `testing()` function: ```typescript theme={null} import { testing } from '@rpgjs/testing' import { defineModule, type RpgServer } from '@rpgjs/server' import type { RpgClient } from '@rpgjs/client' // Define your server module const serverModule = defineModule({ maps: [ { id: 'test-map', file: '', }, ], database: { // Define items, weapons, armors, etc. here TestPotion: { id: 'TestPotion', name: 'Test Potion', description: 'Restores 100 HP', price: 200, hpValue: 100, consumable: true, _type: 'item' as const, }, }, player: { async onConnected(player) { await player.changeMap('test-map', { x: 100, y: 100 }) player.setVariable('test', 'value') } } }) // Define your client module const clientModule = defineModule({ // Client-side logic }) const myModule = [{ server: serverModule, client: clientModule }] // Use it in tests let player: RpgPlayer let client: any let fixture: any beforeEach(async () => { fixture = await testing(myModule) client = await fixture.createClient() player = await client.waitForMapChange('test-map') }) afterEach(async () => { await fixture.clear() }) test('Module hook was called', () => { expect(player.getVariable('test')).toBe('value') }) ``` ## Advanced Configuration ### Server Configuration You can pass custom server configuration: ```typescript theme={null} const fixture = await testing([], {}, { providers: [ // Custom server providers ], // Other server config options }) ``` ### Client Configuration You can pass custom client configuration: ```typescript theme={null} const fixture = await testing([], { providers: [ // Custom client providers ], // Other client config options }) ``` ### Complete Example ```typescript theme={null} import { testing } from '@rpgjs/testing' import { beforeEach, afterEach, test, expect } from 'vitest' import { RpgPlayer } from '@rpgjs/server' import type { RpgServer, RpgClient } from '@rpgjs/server' import { myModule } from './my-module' let player: RpgPlayer let server: any let client: any let fixture: any beforeEach(async () => { fixture = await testing( myModule, // Module (can be a single module or array) { /* client config */ }, // Client configuration { /* server config */ } // Server configuration ) client = await fixture.createClient() player = await client.waitForMapChange('test-map') // Wait for initial map change server = client.server }) afterEach(async () => { await fixture.clear() // Clean up after each test }) test('Test player parameters', () => { expect(player.hp).toBeGreaterThan(0) expect(player.sp).toBeGreaterThan(0) }) test('Test server-side logic', () => { // Access server methods const players = server.subRoom.players() expect(players).toHaveProperty(player.playerId) }) ``` ## Testing Player Actions You can test player actions and server hooks: ```typescript theme={null} import { testing } from '@rpgjs/testing' import { beforeEach, afterEach, test, expect } from 'vitest' import { RpgPlayer } from '@rpgjs/server' let player: RpgPlayer let fixture: any beforeEach(async () => { fixture = await testing() const client = await fixture.createClient() player = client.player }) afterEach(async () => { await fixture.clear() }) test('Player can move', async () => { const initialX = player.position.x // Simulate movement player.move({ x: 10, y: 0 }) // Wait for movement to complete await new Promise(resolve => setTimeout(resolve, 100)) expect(player.position.x).toBeGreaterThan(initialX) }) test('Player can use items', () => { // Add an item to inventory player.addItem('potion', 1) // Use the item player.useItem('potion') expect(player.getItem('potion')).toBeUndefined() }) ``` ## Testing Events and Hooks Test server-side hooks and events: ```typescript theme={null} import { testing } from '@rpgjs/testing' import { defineModule, type RpgServer } from '@rpgjs/server' import type { RpgClient } from '@rpgjs/client' import { beforeEach, afterEach, test, expect, vi } from 'vitest' import { RpgPlayer } from '@rpgjs/server' const serverModule = defineModule({ maps: [ { id: 'test-map', file: '', }, ], player: { async onConnected(player) { await player.changeMap('test-map', { x: 100, y: 100 }) player.setVariable('connected', true) }, onLevelUp(player) { player.setVariable('leveledUp', true) } } }) const clientModule = defineModule({}) const testModule = [{ server: serverModule, client: clientModule }] let player: RpgPlayer let fixture: any beforeEach(async () => { fixture = await testing(testModule) const client = await fixture.createClient() player = await client.waitForMapChange('test-map') }) afterEach(async () => { await fixture.clear() }) test('onConnected hook was called', () => { expect(player.getVariable('connected')).toBe(true) }) test('onLevelUp hook is called when leveling up', () => { const initialLevel = player.level player.level++ expect(player.getVariable('leveledUp')).toBe(true) }) ``` ## Testing Map Changes You can test map changes and verify that players correctly transition between maps. The testing fixture provides a `waitForMapChange()` helper method to wait for map transitions. ### Basic Map Change Test First, define maps in your server module and set up an initial map change: ```typescript theme={null} import { testing } from '@rpgjs/testing' import { defineModule, RpgPlayer, RpgServer } from '@rpgjs/server' import { RpgClient } from '@rpgjs/client' import { beforeEach, test, expect } from 'vitest' // Define server module with multiple maps const serverModule = defineModule({ maps: [ { id: 'map1', file: '', }, { id: 'map2', file: '', } ], player: { async onConnected(player) { // Start player on map1 await player.changeMap('map1', { x: 100, y: 100 }) }, onJoinMap(player) { console.log('Player joined map:', player.getCurrentMap()?.id) } } }) // Define client module const clientModule = defineModule({ // Client-side logic }) let player: RpgPlayer let client: any let fixture: any beforeEach(async () => { const myModule = [{ server: serverModule, client: clientModule }] fixture = await testing(myModule) client = await fixture.createClient() player = await client.waitForMapChange('map1') }) afterEach(async () => { await fixture.clear() }) test('Player can change map', async () => { // Player is already on map1 from beforeEach (waitForMapChange was called there) const initialMap = player.getCurrentMap() expect(initialMap).toBeDefined() expect(initialMap?.id).toBe('map1') // Change to another map const result = await player.changeMap('map2', { x: 200, y: 200 }) expect(result).toBe(true) // Wait for map change to complete // Always assign the result back to player to get the updated instance player = await client.waitForMapChange('map2') const newMap = player.getCurrentMap() expect(newMap).toBeDefined() expect(newMap?.id).toBe('map2') // Verify player position on new map expect(player.x()).toBe(200) expect(player.y()).toBe(200) }) ``` ### Using `waitForMapChange()` The `waitForMapChange()` method is available on the client object returned by `createClient()`. It: * Polls the player's current map until it matches the expected map ID * Returns a Promise that resolves with the **updated player instance** * Throws an error if the timeout is exceeded (default: 5000ms) **Important:** Always assign the returned value to your player variable, as it returns an updated player instance: ```typescript theme={null} // Wait for player to be on map1 (default timeout: 5000ms) // Note: Assign the result back to player to get the updated instance player = await client.waitForMapChange('map1') // Wait with custom timeout (in milliseconds) player = await client.waitForMapChange('map2', 10000) // Example: Wait for initial map change after onConnected beforeEach(async () => { fixture = await testing(myModule) client = await fixture.createClient() // onConnected changes map, so wait for it player = await client.waitForMapChange('test-map') }) ``` ### Testing Map Hooks You can test hooks that fire when players join maps: ```typescript theme={null} const serverModule = defineModule({ maps: [ { id: 'map1', file: '' }, { id: 'map2', file: '' } ], player: { async onConnected(player) { await player.changeMap('map1') }, onJoinMap(player) { const mapId = player.getCurrentMap()?.id player.setVariable('lastMapJoined', mapId) } } }) test('onJoinMap hook is called when changing maps', async () => { const fixture = await testing(myModule) const client = await fixture.createClient() // Wait for initial map (assign result to get updated player) let player = await client.waitForMapChange('map1') expect(player.getVariable('lastMapJoined')).toBe('map1') // Change map and verify hook was called await player.changeMap('map2') player = await client.waitForMapChange('map2') expect(player.getVariable('lastMapJoined')).toBe('map2') await fixture.clear() }) ``` ## Testing Multiple Clients You can create multiple clients to test multiplayer scenarios: ```typescript theme={null} import { testing } from '@rpgjs/testing' import { beforeEach, afterEach, test, expect } from 'vitest' let fixture: any beforeEach(async () => { fixture = await testing() }) afterEach(async () => { await fixture.clear() }) test('Multiple players can exist', async () => { const client1 = await fixture.createClient() const client2 = await fixture.createClient() expect(client1.playerId).not.toBe(client2.playerId) expect(client1.server).toBe(client2.server) // Same server instance const players = client1.server.subRoom.players() expect(Object.keys(players)).toHaveLength(2) }) ``` ## Helper Functions ### `provideTestingLoadMap()` This function provides a mock map loader for testing. It's automatically included when using `testing()`, but you can use it directly if needed: ```typescript theme={null} import { provideTestingLoadMap } from '@rpgjs/testing' // Returns a provider that mocks map loading const mapLoaderProvider = provideTestingLoadMap() ``` ### `waitForSyncComplete()` Waits for server-client synchronization to complete. Useful when testing client-side state after server-side changes: ```typescript theme={null} import { waitForSyncComplete } from '@rpgjs/testing' // After making server-side changes player.addItem('potion', 5) await waitForSyncComplete(player, client.client) // Now test client-side state const clientPlayer = client.client.sceneMap.players()[player.id] expect(clientPlayer.items()).toBeDefined() ``` ## Cleanup and Test Isolation ### Using `fixture.clear()` It's recommended to call `fixture.clear()` in an `afterEach` hook to ensure proper cleanup between tests. This clears all server and client instances, caches, and resets the DOM: ```typescript theme={null} let fixture: any beforeEach(async () => { fixture = await testing(myModule) const client = await fixture.createClient() player = client.player }) afterEach(async () => { await fixture.clear() // Clean up after each test }) ``` This ensures: * No state leaks between tests * All server and client instances are properly destroyed * DOM is reset to a clean state * Injection contexts are cleared ## Testing Database Items ### Defining Items in Module Database You can define items, weapons, and armors directly in your server module's `database` property: ```typescript theme={null} const serverModule = defineModule({ database: { TestPotion: { id: 'TestPotion', name: 'Test Potion', description: 'Restores 100 HP', price: 200, hpValue: 100, consumable: true, _type: 'item' as const, }, TestSword: { name: 'Test Sword', description: 'A basic sword', price: 500, atk: 50, _type: 'weapon' as const, }, }, player: { async onConnected(player) { await player.changeMap('test-map', { x: 100, y: 100 }) }, }, }) ``` ### Adding Items Dynamically to Maps You can also add items to a map's database dynamically using `addInDatabase()`: ```typescript theme={null} test('should add item dynamically', () => { const customItem = { id: 'custom-item', name: 'Custom Item', price: 100, _type: 'item' as const, } // Add item to current map's database player.getCurrentMap()?.addInDatabase('custom-item', customItem) // Now you can use it const item = player.addItem('custom-item', 1) expect(item).toBeDefined() }) ``` ## Synchronization Utilities ### `waitForSyncComplete()` When you make server-side changes (like adding items, changing player state), you may need to wait for the synchronization to complete before testing client-side state: ```typescript theme={null} import { waitForSyncComplete } from '@rpgjs/testing' test('should sync item to client', async () => { // Make a server-side change player.addItem('TestPotion', 5) // Wait for sync to complete await waitForSyncComplete(player, client.client) // Now you can safely test client-side state const clientPlayer = client.client.sceneMap.players()[player.id] expect(clientPlayer.items()).toBeDefined() }) ``` ## Best Practices 1. **Use `beforeEach`**: Create a fresh fixture and client for each test to ensure isolation 2. **Use `afterEach` with `fixture.clear()`**: Always clean up after each test to prevent state leaks 3. **Test modules separately**: Create focused tests for individual modules 4. **Use async/await**: The `testing()` and `createClient()` functions are async 5. **Wait for map changes**: Use `waitForMapChange()` after `onConnected` or `changeMap()` calls 6. **Assign player from `waitForMapChange()`**: Always assign the returned value to get the updated player instance 7. **Mock external dependencies**: Use Vitest mocks for external services or APIs 8. **Define items in database**: Use the module's `database` property for test items ## Example: Complete Test Suite ```typescript theme={null} import { testing } from '@rpgjs/testing' import { beforeEach, afterEach, describe, test, expect } from 'vitest' import { RpgPlayer, MAXHP_CURVE, MAXSP_CURVE, MAXHP, MAXSP } from '@rpgjs/server' import { defineModule, type RpgServer } from '@rpgjs/server' import type { RpgClient } from '@rpgjs/client' describe('Player Parameters', () => { let player: RpgPlayer let fixture: any beforeEach(async () => { const serverModule = defineModule({ maps: [ { id: 'test-map', file: '', }, ], player: { async onConnected(player) { await player.changeMap('test-map', { x: 100, y: 100 }) }, }, }) const clientModule = defineModule({}) const myModule = [{ server: serverModule, client: clientModule }] fixture = await testing(myModule) const client = await fixture.createClient() player = await client.waitForMapChange('test-map') }) afterEach(async () => { await fixture.clear() }) test('Player has correct initial HP', () => { expect(player.hp).toBe(MAXHP_CURVE.start) }) test('Player has correct initial SP', () => { expect(player.sp).toBe(MAXSP_CURVE.start) }) test('Player has correct MaxHP parameter', () => { expect(player.param[MAXHP]).toBe(MAXHP_CURVE.start) }) test('Player has correct MaxSP parameter', () => { expect(player.param[MAXSP]).toBe(MAXSP_CURVE.start) }) }) ``` ## Customizing Map Configuration in Tests By default, `testing()` uses `provideTestingLoadMap()` which provides maps with default dimensions (1024x768) and a minimal mock component. If you need custom map configuration, you can pass your own `provideLoadMap` in `clientConfig.providers`: ```typescript theme={null} import { provideLoadMap } from '@rpgjs/client' const fixture = await testing( [myModule], { providers: [ provideLoadMap((mapId) => { return { id: mapId, data: { width: 2048, height: 1536, hitboxes: [] }, component: MyCustomComponent, width: 2048, height: 1536 } }) ] } ) ``` If you don't provide `provideLoadMap`, the default `provideTestingLoadMap()` will be used automatically. ## Troubleshooting ### Tests fail with WebGL errors Make sure you've configured Vitest to use the setup file from `@rpgjs/testing`: ```typescript theme={null} // vitest.config.ts export default defineConfig({ test: { setupFiles: ['@rpgjs/testing/dist/setup.js'] } }) ``` ### Player is undefined Ensure you're calling `createClient()` and accessing `client.player` after the client is created: ```typescript theme={null} const client = await fixture.createClient() const player = client.player // Access via getter ``` ### Module hooks not being called Verify that your modules are correctly structured and passed to the `testing()` function: ```typescript theme={null} const fixture = await testing([myModule]) // Don't forget the array ``` ### Map component errors If you see errors about missing map components or hitbox requirements, make sure you're either: * Using the default `provideTestingLoadMap()` (automatically added if no custom `provideLoadMap` is provided) * Or providing a complete `provideLoadMap` with `component`, `width`, `height`, and `data` properties # Time Manager Source: https://v5.rpgjs.dev/guide/time-manager Guide for synchronized game time, calendars, and optional lighting in RPGJS. # Time Manager Use `withTimeManager()` to add synchronized game time without adding methods to `RpgMap`. The server owns the canonical time. The client receives a map snapshot and infers the displayed time locally from `elapsedMinutes`, `scale`, `paused`, and `serverTimestamp`. ## Register the Module You can use the same module object on both sides: ```ts theme={null} // modules/time.ts import { withTimeManager } from '@rpgjs/common' export const TimeManagerModule = withTimeManager({ start: '0001-01-01 08:00', scale: 10, calendar: { months: 12, daysPerMonth: 30, daysPerWeek: 7, seasons: ['spring', 'summer', 'autumn', 'winter'] }, lighting: { enabled: true } }) ``` ```ts theme={null} // server.ts import { provideServerModules } from '@rpgjs/server' import { TimeManagerModule } from './modules/time' provideServerModules([ TimeManagerModule ]) ``` ```ts theme={null} // client.ts import { provideClientModules } from '@rpgjs/client' import { TimeManagerModule } from './modules/time' provideClientModules([ TimeManagerModule ]) ``` For a client-only declaration, this also works: ```ts theme={null} provideClientModules([ withTimeManager() ]) ``` ## Units and limits | Option | Unit | | - | - | | `scale` | Game minutes per **real minute** (`1` is real time, `60` is one game hour per real minute). | | `useGameClock({ speed })` of CanvasEngine | Game minutes per real **second**: divide `scale` by 60. | There is **one clock for the whole server**: every map shares the same time, and `pause()` or `set()` applies to all of them. To keep a map out of the day night cycle (a cave, an interior), see [Per map lighting](#per-map-lighting). ## Clock on screen `hud` draws the clock on the client: a pill with the day and the hour, and a round medallion whose sky follows the hour (a sun that crosses it, a moon and stars at night, colors that change at dawn and dusk). It is off by default. ```ts theme={null} withTimeManager({ scale: 10, hud: { position: "top-right", fastScale: 1000 }, // or `hud: true` }) ``` | Option | Default | | | - | - | - | | `position` | `top-right` | `top-left`, `top-right`, `bottom-left` or `bottom-right`. | | `size` | `default` | `compact` shows the medallion only. | | `fastScale` | none | Shows the fast forward badge from this `scale` on. A paused clock always shows its badge. | The client must register the same module (`provideClientModules([TimeManagerModule])`) and load the styles (`import "@rpgjs/ui-css/index.css"`). "Day N" counts the days since the start. The texts are the `rpg.time.*` keys (English and French are included). The catalogue of the GUI has it in **Compositions / Game clock**. ## Server API Use `TimeManager` from server hooks, events, or services: ```ts theme={null} import { TimeManager, inject } from '@rpgjs/server' const time = inject(TimeManager) time.set({ hour: 22, minute: 30 }) time.advance({ hours: 2 }) time.pause() time.resume() time.setScale(30) ``` Read the derived state: ```ts theme={null} const now = time.state() console.log(now.year, now.month, now.day) console.log(now.hour, now.minute, now.weekday, now.season) ``` ## Client API Use `ClientTimeManager` for display logic: ```ts theme={null} import { ClientTimeManager, inject } from '@rpgjs/client' const time = inject(ClientTimeManager) const now = time.state() if (now) { console.log(`${now.hour}:${String(now.minute).padStart(2, '0')}`) } ``` The client does not write time. It projects the current display value from the last synchronized map snapshot. ## Snapshot The synchronized map field is internal and namespaced: ```ts theme={null} { __rpgjsTime: { elapsedMinutes: 480, scale: 10, paused: false, serverTimestamp: 1783013400000, calendar: { months: 12, daysPerMonth: 30, daysPerWeek: 7, seasons: ['spring', 'summer', 'autumn', 'winter'] } } } ``` Game code should use `TimeManager` and `ClientTimeManager` instead of reading `__rpgjsTime` directly. ## Lighting When `lighting.enabled` is true, the server can apply map lighting from time phases: ```ts theme={null} withTimeManager({ lighting: { enabled: true, transitionMs: 1500, phases: { dawn: { hour: 6, lighting: { ambient: { darkness: 0.2 } } }, day: { hour: 8, lighting: { ambient: { darkness: 0 } } }, dusk: { hour: 18, lighting: { ambient: { darkness: 0.25 } } }, night: { hour: 21, lighting: { ambient: { darkness: 0.55 } } } } } }) ``` A phase is merged into the lighting of the map: the light spots, the shadows and the other keys that the phase does not set are kept. If lighting is omitted or disabled, the time manager does not modify map lighting. ### Day night cycle Use `dayNight` to replace the phases by the continuous color grading of the `DayNightCycle` preset. The scene follows the hour of the time manager and the `spots` of the map are lit at night (see [Lighting](./lighting.md#day-night-cycle)): ```ts theme={null} withTimeManager({ scale: 10, lighting: { enabled: true, dayNight: { lightIntensity: 1.2, vignette: 1 } // or `true` } }) ``` Two sets of phases exist, do not mix them up: | Mode | Keys | Boundaries | | - | - | - | | `phases` | Your own keys | The hours you give (`hour: 21`). | | `dayNight` | `night`, `dawn`, `day`, `dusk` | 0h, 5h, 7h30, 17h30 and 20h30, like the `DayNightCycle` preset. | The hooks `onLightingPhaseChange` and `onPhaseChange` (the same transition, the second one reads better for gameplay rules) receive those keys. The state exposes them too: `state.phase` and `state.hourFloat` (the hour with decimals, `18.5` is 18:30). ### Per map lighting The time manager lights every registered map. Override or disable it per map, like `weather.maps`: ```ts theme={null} withTimeManager({ lighting: { enabled: true, dayNight: true, maps: { cave: false, // untouched tavern: { dayNight: { lightIntensity: 1.4 } } // other options for this map } } }) ``` A map can also opt out on its own side, in its `lighting`: ```ts theme={null} { id: 'house', lighting: { dayNight: { enabled: false }, ambient: { darkness: 0.3 } } } ``` ### Client module The client must register the same module (`provideClientModules([TimeManagerModule])`). Without it the client falls back to a local clock and shows the wrong hour: a warning is logged in the console when the server drives the cycle with the time manager. The client projects the time from the clock of the server, so a client whose clock is off still shows the same hour as the other players. ## Weather Ambiences When `weather.enabled` is true, the server can roll weather ambiences per map. Each ambience has a weight and a game-time duration. When the duration expires, the server rolls the next ambience and applies it with the existing map weather API. ```ts theme={null} withTimeManager({ weather: { enabled: true, default: { ambiences: { clear: { weather: null, weight: { default: 60, months: { 6: 80, 7: 85, 8: 80 } }, duration: { min: { hours: 2 }, max: { hours: 6 } } }, rain: { weather: { effect: 'rain', preset: 'steadyRain', params: { density: 220, speed: 0.7 }, transitionMs: 900 }, weight: { default: 20, months: { 3: 45, 4: 50, 10: 40, 11: 45 } }, duration: { min: { hours: 1 }, max: { hours: 4 } } }, fog: { weather: { effect: 'fog', preset: 'morningFog', params: { density: 0.8, alpha: 0.45 } }, weight: { default: 10, seasons: { autumn: 30, winter: 20 } }, duration: { hours: 2 } } } }, maps: { forest: { ambiences: { rain: { weather: { effect: 'rain', params: { density: 260 } }, weight: 100, duration: { hours: 2 } } } } } } }) ``` `maps[mapId]` overrides the `default` weather table for that map. A `weather: null` ambience clears the map weather. If weather is omitted, disabled, or no table exists for a map, the time manager does not modify that map's weather. Weather remains server-owned. Clients receive the regular `weatherState` map update and should read `engine.sceneMap.weather()` or `engine.sceneMap.getWeather()` for rendering. ## Environment Hooks Use `hooks` on `withTimeManager()` when the environment should drive gameplay rules from the plugin itself: ```ts theme={null} withTimeManager({ hooks: { onDayChange({ map, current }) { console.log(`New day ${current.day}`) }, onLightingPhaseChange({ previousKey, currentKey }) { console.log(`${previousKey} -> ${currentKey}`) }, onBeforeWeatherChange({ candidate, time }) { if (time.season === 'summer' && candidate.key === 'snow') { return false } }, onWeatherChange({ currentKey }) { console.log(`Weather is now ${currentKey}`) } } }) ``` `onBeforeWeatherChange()` can return `false` to cancel the roll, or another `{ key, ambience }` candidate to replace it. Initial map registration is silent: hooks run only after an actual time, lighting, or weather transition. Transition payloads include the current map so plugin hooks can update synchronized map state or trigger map-local systems. Events can react locally by declaring matching methods: ```ts theme={null} import { EventData, RpgEvent } from '@rpgjs/server' import type { TimeWeatherTransitionPayload } from '@rpgjs/common' @EventData({ name: 'Crop' }) export class CropEvent extends RpgEvent { onDayChange() { this.setGraphic('crop-stage-2') } onWeatherChange(payload: TimeWeatherTransitionPayload) { if (payload.currentKey === 'rain') { this.setGraphic('crop-watered') } } } ``` Global event hooks work too: ```ts theme={null} const server = { event: { onWeatherChange(event, payload) { if (payload.currentKey === 'rain') { event.setGraphic('crop-watered') } } } } ``` This keeps the environment logic centralized in the time plugin while still letting map events behave like Stardew Valley objects: crops grow on day changes, lamps react to lighting phases, and NPCs or interactables react to weather changes. # Weather Source: https://v5.rpgjs.dev/guide/weather Guide for Weather in RPGJS. # Weather Manage map weather from the server while keeping a client-side override for local effects. ## Shared Types Weather types are exported by `@rpgjs/common`: ```ts theme={null} import type { WeatherState, WeatherEffect, WeatherParams } from '@rpgjs/common' ``` Every weather effect of CanvasEngine (`@canvasengine/presets`) is supported: | Effect | Presets | | - | - | | `rain` | `lightRain`, `steadyRain`, `stormRain` | | `snow` | `lightSnow`, `winterSnow`, `blizzardSnow` | | `fog` | `rpgMorningMist`, `rpgForestFog`, `rpgSwampFog`, `rpgNightFog`, `rpgHeavyFog` | | `cloud` | `lightClouds`, `overcastClouds`, `stormClouds`, `goldenHourRays`, `sunnySoftRays`, `sunsetTwinkleRays`, `dramaticCrepuscularRays`, `morningHazeRays`, `naturalClouds` | | `rays` | `morningSunRays`, `goldenHourShafts`, `forestLightShafts`, `moonbeams`, `holyRays`, `radiantBeams` | | `embers` | `volcanoEmbers`, `eruptionEmbers` | | `ash` | `ashfall` | | `leaves` | `autumnLeaves`, `autumnGust` | | `petals` | `sakuraPetals`, `sakuraStorm` | | `fireflies` | `summerFireflies`, `swampFireflies` | | `spores` | `forestSpores`, `enchantedSpores` | | `sand` | `dustWind`, `sandstorm` | `effect` can also be the id of a [custom weather](#custom-weather). The catalog is exported by `@rpgjs/common` without importing the renderer, so an editor or a server can list it: ```ts theme={null} import { WEATHER_EFFECTS, WEATHER_PRESET_EFFECTS, getWeatherPresetEffect } from '@rpgjs/common' getWeatherPresetEffect('moonbeams') // 'rays' ``` ## Presets And Params A weather state can name only a `preset`; `effect` is then the one of the preset. `params` override the values of the preset. All `params` are forwarded to CanvasEngine's `` as they are, so a parameter added by a newer CanvasEngine works without a new RPGJS release (`rayFan`, `rayLength`, `rayColor`, `fogOpacity`, `colors`, `particleSize`, `haze`, `topDown`, ...). ```ts theme={null} map.setWeather({ preset: 'moonbeams' }) map.setWeather({ effect: 'embers', params: { colors: ['#ff6a00'], particleSize: 1.4 } }) ``` An unknown preset or effect is reported once in the console and renders nothing. ## Initial Map Weather Set initial weather in `@MapData()` or in a map entry inside your server module: ```ts theme={null} import { MapData, RpgMap } from '@rpgjs/server' @MapData({ id: 'forest', file: require('./tmx/forest.tmx'), weather: { effect: 'fog', preset: 'rpgForestFog', params: { density: 1.2, height: 0.75 }, transitionMs: 1200 } }) export class ForestMap extends RpgMap {} ``` ## Runtime API (Server) `RpgMap` exposes: * `map.getWeather(): WeatherState | null` * `map.setWeather(next: WeatherState | null, options?: { sync?: boolean })` * `map.patchWeather(patch: Partial, options?: { sync?: boolean })` * `map.clearWeather(options?: { sync?: boolean })` Example: ```ts theme={null} map.setWeather({ effect: 'rain', preset: 'steadyRain', params: { density: 220, speed: 0.7, windStrength: 0.25 }, transitionMs: 900, startedAt: Date.now() }) map.patchWeather({ params: { density: 280 } }) ``` `patchWeather()` merges `params`. A patch that names a new `preset` (or a new `effect`) replaces the previous one, so the old effect never overrides the effect of the new preset. When `sync` is not `false`, the weather is broadcast to players in the map. A synchronized `null` is authoritative: `clearWeather()` removes an initial weather effect declared in the map data in both standalone and MMORPG modes. ## Runtime API (Client) `RpgClientMap` exposes: * `map.weatherState` (server-synchronized state) * `map.localWeatherOverride` (client-only override) * `map.weather` (computed: local override first, then server state, then map-data fallback before the first synchronization) * `map.getWeather()` * `map.setLocalWeather(next)` * `map.clearLocalWeather()` Example: ```ts theme={null} const weather = engine.sceneMap.getWeather() engine.sceneMap.setLocalWeather({ effect: 'cloud', params: { density: 0.8, sunIntensity: 1.2 } }) ``` ## Custom Weather Register a weather component by id in a client module, like `componentAnimations`: ```ts theme={null} import { defineModule, RpgClient } from '@rpgjs/client' import AuroraWeather from './aurora.ce' defineModule({ weathers: [ { id: 'aurora', component: AuroraWeather } ] }) ``` ```ts theme={null} // server map.setWeather({ effect: 'aurora', params: { intensity: 0.8 } }, { transitionMs: 2000 }) ``` When `effect` matches a registered id, the scene renders that component instead of ``, in the same layer (`params.zIndex`, 1000 by default). It receives `effect`, `params`, `transitionMs`, `durationMs`, `startedAt` and `seed` as props. Every client renders it, including players who join later. A registered id replaces a built-in effect of the same name, so a game can restyle `rain`. ## Rendering With CanvasEngine Preset The scene already renders the weather state with CanvasEngine. To draw it yourself elsewhere, feed `@canvasengine/presets` with it: ```tsx theme={null} ``` You can derive those values from `engine.sceneMap.weather()` / `engine.sceneMap.getWeather()`. # World Maps API Source: https://v5.rpgjs.dev/guide/world-maps Guide for title in RPGJS. ## Map ↔ World attachment helpers These helpers allow you to attach a map to a world, retrieve the attached world, and remove the map from the world. ### Get attached World * Since: 3.0.0-beta.8 * Method: `map.getInWorldMaps()` * Usage: Recover the world attached to this map (undefined if no world attached) ```ts theme={null} const world = map.getInWorldMaps(); if (world) { // use world manager console.log(world.getAllMaps()); } ``` ### Remove this map from the world * Since: 3.0.0-beta.8 * Method: `map.removeFromWorldMaps()` * Return: `boolean | undefined` * Usage: Remove this map from the world ```ts theme={null} const removed = map.removeFromWorldMaps(); // true if removed, false if not found, undefined if no world attached ``` ### Assign the map to a world * Since: 3.0.0-beta.8 * Method: `map.setInWorldMaps(worldMap)` * Arguments: `{ RpgWorldMaps } worldMap` (Optional: false) * Usage: Assign the map to a world ```ts theme={null} import { WorldMapsManager } from '@rpgjs/common'; const manager = new WorldMapsManager(); manager.configure([ { id: 'town', worldX: 0, worldY: 0, width: 1024, height: 1024 }, ]); map.setInWorldMaps(manager); ``` # Client Engine Hooks Source: https://v5.rpgjs.dev/hooks/client-engine-hooks Guide for Client Engine Hooks in RPGJS. # Client Engine Hooks Client engine hooks allow you to listen to client-side events and customize client behavior. These hooks are defined in the `engine` property of your client module. ## Usage ```ts theme={null} import { RpgClientEngine, RpgClientEngineHooks, defineModule } from '@rpgjs/client' const engine: RpgClientEngineHooks = { onStart(engine: RpgClientEngine) { console.log('Client engine started') return true // Allow connection to server }, onStep(engine: RpgClientEngine, t: number, dt: number) { // Called each frame } } export default defineModule({ engine }) ``` ## Available Hooks ### onStart **Description:** Called when the client engine starts. Return `false` to prevent connection to the server. **Parameters:** * `engine: RpgClientEngine` - The client engine instance **Returns:** * `boolean | void` - Return `false` to prevent server connection **Example:** ```ts theme={null} const engine: RpgClientEngineHooks = { async onStart(engine: RpgClientEngine) { console.log('🎮 Client starting...') // Load global configuration try { const response = await fetch('/api/game-config') const config = await response.json() engine.globalConfig = config console.log('✅ Configuration loaded') return true // Allow connection } catch (error) { console.error('❌ Failed to load configuration:', error) return false // Prevent connection } } } ``` ### onStep **Description:** Called at each frame (typically 60 FPS) **Parameters:** * `engine: RpgClientEngine` - The client engine instance * `t: number` - Current timestamp * `dt: number` - Delta time since last frame **Example:** ```ts theme={null} const engine: RpgClientEngineHooks = { onStep(engine: RpgClientEngine, t: number, dt: number) { // Update custom animations if (engine.customAnimations) { engine.customAnimations.forEach(animation => { animation.update(dt) }) } // Update UI elements if (engine.ui) { engine.ui.updateTimers(dt) } // Performance monitoring if (t % 1000 < dt) { // Every second const fps = Math.round(1000 / dt) if (fps < 30) { console.warn(`Low FPS detected: ${fps}`) } } } } ``` ### onInput **Description:** Called when movement or action input is detected **Parameters:** * `engine: RpgClientEngine` - The client engine instance * `obj: { input: string | number, action?: string | number, data?: any, playerId: number }` - Input data **Example:** ```ts theme={null} const engine: RpgClientEngineHooks = { onInput(engine: RpgClientEngine, { input, data, playerId }) { console.log(`Input received: ${input} from player ${playerId}`) if (input === 'projectile:shoot') { console.log(data?.target) } // Handle custom key bindings switch (input) { case 'F1': engine.gui('help').toggle() break case 'F2': engine.gui('debug').toggle() break case 'F11': engine.toggleFullscreen() break case 'screenshot': engine.takeScreenshot() break case 'chat': engine.gui('chat').focus() break } // Log input for analytics if (engine.analytics) { engine.analytics.logInput(input, playerId) } } } ``` ### onConnected **Description:** Called when the client successfully connects to the server. In MMORPG mode, this hook runs only after the server sends the RPGJS acceptance packet, so a refused `auth()` does not trigger it. **Parameters:** * `engine: RpgClientEngine` - The client engine instance * `socket: any` - The socket connection instance **Example:** ```ts theme={null} const engine: RpgClientEngineHooks = { onConnected(engine: RpgClientEngine, socket: any) { console.log('🔗 Connected to server') // Show connection status engine.gui('notification').show({ message: 'Connected to server', type: 'success', duration: 3000 }) // Initialize client-side systems engine.initializeChat() engine.initializeAudio() engine.initializeParticles() // Send client information socket.emit('client-info', { version: engine.version, platform: navigator.platform, userAgent: navigator.userAgent, screenResolution: { width: screen.width, height: screen.height } }) // Start heartbeat engine.startHeartbeat(socket) } } ``` ### onDisconnect **Description:** Called when the client disconnects from the server **Parameters:** * `engine: RpgClientEngine` - The client engine instance * `reason: any` - Disconnection reason * `socket: any` - The socket connection instance **Example:** ```ts theme={null} const engine: RpgClientEngineHooks = { onDisconnect(engine: RpgClientEngine, reason: any, socket: any) { console.log('🔌 Disconnected from server:', reason) // Show disconnection message engine.gui('notification').show({ message: `Disconnected: ${reason}`, type: 'error', duration: 5000 }) // Stop client-side systems engine.stopHeartbeat() engine.pauseAudio() engine.clearParticles() // Show reconnection dialog engine.gui('reconnect-dialog').open({ reason: reason, onReconnect: () => { engine.reconnect() } }) // Save offline data engine.saveOfflineData() } } ``` ### onConnectError **Description:** Called when there's an error connecting to the server. In MMORPG mode, this is also called when server-side `auth()` refuses the WebSocket connection. **Parameters:** * `engine: RpgClientEngine` - The client engine instance * `err: any` - The connection error * `socket: any` - The socket connection instance **Example:** ```ts theme={null} const engine: RpgClientEngineHooks = { onConnectError(engine: RpgClientEngine, err: any, socket: any) { console.error('❌ Connection error:', err) // Show error message engine.gui('error-dialog').open({ title: 'Connection Failed', message: 'Unable to connect to the game server. Please check your internet connection and try again.', error: err.message, actions: [ { label: 'Retry', action: () => engine.reconnect() }, { label: 'Offline Mode', action: () => engine.startOfflineMode() } ] }) // Log error for debugging if (engine.errorLogger) { engine.errorLogger.log({ type: 'connection_error', error: err, timestamp: Date.now(), userAgent: navigator.userAgent }) } } } ``` `onStart` means the client engine is booting. It does not guarantee that the player has been accepted by the server. Use `onConnected` for accepted connections and `onConnectError` to show a login screen, refresh a token, or redirect the player after an authentication failure. ### onWindowResize **Description:** Called when the browser window is resized **Example:** ```ts theme={null} const engine: RpgClientEngineHooks = { onWindowResize() { console.log('🖥️ Window resized') // Update viewport const newWidth = window.innerWidth const newHeight = window.innerHeight // Adjust game canvas if (engine.renderer) { engine.renderer.resize(newWidth, newHeight) } // Update UI layout if (engine.ui) { engine.ui.updateLayout(newWidth, newHeight) } // Adjust camera if (engine.camera) { engine.camera.updateViewport(newWidth, newHeight) } // Save new dimensions localStorage.setItem('gameWindowSize', JSON.stringify({ width: newWidth, height: newHeight })) } } ``` ## Complete Example ```ts theme={null} import { RpgClientEngine, RpgClientEngineHooks, defineModule } from '@rpgjs/client' const engine: RpgClientEngineHooks = { async onStart(engine: RpgClientEngine) { console.log('🎮 Starting RPG Client...') // Load game configuration try { const slug = 'my-game-project' const response = await fetch(`/api/game/project/${slug}`) const config = await response.json() engine.globalConfig = config engine.gameVersion = config.version // Initialize client systems engine.analytics = new GameAnalytics(config.analyticsKey) engine.errorLogger = new ErrorLogger(config.errorReportingUrl) console.log('✅ Client initialized successfully') return true } catch (error) { console.error('❌ Failed to initialize client:', error) return false } }, onStep(engine: RpgClientEngine, t: number, dt: number) { // Update performance metrics engine.performanceMonitor?.update(dt) // Update custom systems engine.particleSystem?.update(dt) engine.audioManager?.update(dt) // Auto-save progress every 30 seconds if (t % 30000 < dt) { engine.autoSave() } }, onInput(engine: RpgClientEngine, { input, playerId }) { // Handle debug commands if (engine.debugMode) { switch (input) { case 'F3': engine.gui('debug-info').toggle() break case 'F4': engine.toggleWireframe() break } } // Handle screenshot if (input === 'F12') { engine.takeScreenshot() } }, onConnected(engine: RpgClientEngine, socket: any) { console.log('🌐 Connected to game server') // Initialize multiplayer features engine.chat.enable() engine.voiceChat.initialize() // Send client capabilities socket.emit('client-capabilities', { webGL: engine.renderer.isWebGL, audioContext: !!window.AudioContext, gamepadSupport: !!navigator.getGamepads, touchSupport: 'ontouchstart' in window }) engine.gui('loading').hide() engine.gui('hud').show() }, onDisconnect(engine: RpgClientEngine, reason: any, socket: any) { console.log('🔌 Disconnected:', reason) // Disable multiplayer features engine.chat.disable() engine.voiceChat.disconnect() // Show appropriate UI engine.gui('hud').hide() engine.gui('disconnected').show({ reason }) }, onConnectError(engine: RpgClientEngine, err: any, socket: any) { console.error('Connection failed:', err) engine.gui('connection-error').show({ error: err.message, onRetry: () => engine.reconnect(), onOffline: () => engine.startOfflineMode() }) }, onWindowResize() { // Responsive design adjustments const width = window.innerWidth const height = window.innerHeight engine.renderer.resize(width, height) engine.ui.adjustLayout(width, height) // Mobile optimizations if (width < 768) { engine.ui.enableMobileMode() } else { engine.ui.disableMobileMode() } } } export default defineModule({ engine }) ``` # Client Physics Hooks Source: https://v5.rpgjs.dev/hooks/client-physics-hooks Guide for Client Physics Hooks in RPGJS. # Client Physics Hooks Client physics hooks let you extend client-side physics (including client prediction) from `sceneMap`. Use these hooks to keep collision rules aligned with the server. ## Usage ```ts theme={null} import { defineModule, type RpgClient } from "@rpgjs/client"; export default defineModule({ sceneMap: { onPhysicsInit(scene, { mapData }) { // Build physics metadata from mapData }, onPhysicsEntityAdd(scene, { owner, entity, kind }) { // Attach entity-level rules (ex: canEnterTile) }, onPhysicsEntityRemove(scene, { owner }) { // Cleanup entity-level rules }, onPhysicsReset(scene) { // Cleanup all physics extension state }, }, }); ``` ## Available Hooks ### `onPhysicsInit(scene, context)` Called when map physics is initialized. * `scene`: current `RpgClientMap` * `context.mapData`: map data used by `loadPhysic()` Use this to precompute shared collision metadata from map data. ### `onPhysicsEntityAdd(scene, context)` Called when a dynamic physics body is created. * `context.owner`: player/event owning the body * `context.entity`: physics entity instance * `context.kind`: `"hero" | "npc" | "generic"` Use this to attach rules such as `entity.canEnterTile(...)`. ### `onPhysicsEntityRemove(scene, context)` Called before a dynamic physics body is removed. Use this to unregister handlers created in `onPhysicsEntityAdd`. ### `onPhysicsReset(scene)` Called when the physics world is reset before reloading map physics. Use this to clear global state and remaining subscriptions. ## Best Practices * Keep handlers deterministic and synchronous when possible. * Use the same shared rule functions on client and server. * Prefer hook-based setup over `setTimeout()` for physics extensions. # Client Scene Hooks Source: https://v5.rpgjs.dev/hooks/client-scene-hooks Guide for Client Scene Hooks in RPGJS. # Client Scene Hooks Scene hooks allow you to customize the behavior of scenes (maps) on the client side. These hooks are defined in the `sceneMap` property of your client module. ## Usage ```ts theme={null} import { RpgSceneMap, RpgSceneMapHooks, defineModule } from '@rpgjs/client' const sceneMap: RpgSceneMapHooks = { onAfterLoading(scene: RpgSceneMap) { console.log(`Map ${scene.id} loaded`) }, onAddSprite(scene: RpgSceneMap, sprite: RpgSprite) { console.log(`Sprite ${sprite.id} added to scene`) } } export default defineModule({ sceneMap }) ``` ## Composable SceneMap Component Use `sceneMap.component` to wrap the default scene renderer and add scene-level CanvasEngine components without replacing the map component returned by `provideLoadMap()`. ```ts theme={null} import { RpgClient, defineModule } from '@rpgjs/client' import MyScene from './my-scene.ce' export default defineModule({ sceneMap: { component: MyScene } }) ``` ```html theme={null} ``` Children passed to `SceneMap` are rendered in the map scene after the map renderer and before component animations, projectiles, and weather. If the custom element must be sorted with players and events, put it inside `EventLayerComponent` in the map component instead. ## Available Hooks ### onBeforeLoading **Description:** Called before the scene starts loading **Parameters:** * `scene: RpgSceneMap` - The scene instance **Example:** ```ts theme={null} const sceneMap: RpgSceneMapHooks = { onBeforeLoading(scene: RpgSceneMap) { console.log(`🔄 Loading map: ${scene.id}`) // Show loading screen scene.showLoadingScreen({ message: `Loading ${scene.name}...`, showProgress: true }) // Initialize scene data scene.customData = { loadStartTime: Date.now(), weather: 'clear', timeOfDay: 'day', ambientSounds: [] } // Preload custom resources scene.preloadCustomAssets([ 'custom-particles.json', 'ambient-sounds.ogg', 'weather-effects.png' ]) // Set up scene-specific configurations if (scene.id === 'dungeon-1') { scene.enableFogOfWar() scene.setAmbientLight(0.3) // Darker lighting } if (scene.id === 'town-square') { scene.enableDayNightCycle() scene.setWeatherSystem(true) } } } ``` ### onAfterLoading **Description:** Called after the scene has finished loading (images loaded, drawing completed, viewport assigned) **Parameters:** * `scene: RpgSceneMap` - The scene instance **Example:** ```ts theme={null} const sceneMap: RpgSceneMapHooks = { onAfterLoading(scene: RpgSceneMap) { console.log(`✅ Map ${scene.id} fully loaded`) // Hide loading screen scene.hideLoadingScreen() // Calculate loading time const loadTime = Date.now() - scene.customData.loadStartTime console.log(`Map loaded in ${loadTime}ms`) // Initialize scene systems scene.initializeParticleSystem() scene.initializeLightingSystem() scene.initializeAudioSystem() // Set up scene-specific features if (scene.id === 'forest-1') { scene.addAmbientSound('forest-ambience', { volume: 0.3, loop: true }) scene.addParticleEffect('falling-leaves', { density: 0.1 }) scene.setWeather('light-rain', 0.2) } if (scene.id === 'cave-entrance') { scene.addAmbientSound('cave-echo', { volume: 0.5, loop: true }) scene.setAmbientLight(0.6) scene.addParticleEffect('dust-motes', { density: 0.05 }) } // Show welcome message for special areas if (scene.isNewArea) { scene.showAreaTitle(scene.name, 3000) } // Initialize minimap scene.initializeMinimap() // Set up camera bounds scene.camera.setBounds(0, 0, scene.width, scene.height) // Trigger scene-specific events scene.triggerEvent('onSceneReady') } } ``` ### onMapLoading **Description:** Called while the map and resources are being loaded **Parameters:** * `scene: RpgSceneMap` - The scene instance * `loader: PIXI.Loader` - The PIXI loader instance **Example:** ```ts theme={null} const sceneMap: RpgSceneMapHooks = { onMapLoading(scene: RpgSceneMap, loader: any) { console.log(`📦 Loading resources for map: ${scene.id}`) // Add custom resources to loader loader.add('custom-tileset', '/assets/custom-tileset.png') loader.add('scene-music', `/assets/music/${scene.id}.ogg`) loader.add('scene-effects', `/assets/effects/${scene.id}.json`) // Track loading progress loader.onProgress.add((loader: any, resource: any) => { const progress = loader.progress scene.updateLoadingProgress(progress, `Loading ${resource.name}...`) console.log(`Loading progress: ${progress}% - ${resource.name}`) }) // Handle loading errors loader.onError.add((error: any, loader: any, resource: any) => { console.error(`Failed to load resource: ${resource.name}`, error) scene.showLoadingError(`Failed to load ${resource.name}`) }) // Add scene-specific resources based on map type if (scene.mapType === 'dungeon') { loader.add('torch-flame', '/assets/effects/torch-flame.json') loader.add('dungeon-ambience', '/assets/audio/dungeon-ambience.ogg') } if (scene.mapType === 'outdoor') { loader.add('wind-particles', '/assets/effects/wind.json') loader.add('bird-sounds', '/assets/audio/birds.ogg') } // Load weather effects if needed if (scene.hasWeather) { loader.add('rain-effect', '/assets/weather/rain.json') loader.add('snow-effect', '/assets/weather/snow.json') loader.add('fog-effect', '/assets/weather/fog.png') } } } ``` ### onAddSprite **Description:** Called when a sprite is added to the scene **Parameters:** * `scene: RpgSceneMap` - The scene instance * `sprite: RpgSprite` - The sprite being added **Example:** ```ts theme={null} const sceneMap: RpgSceneMapHooks = { onAddSprite(scene: RpgSceneMap, sprite: RpgSprite) { console.log(`➕ Sprite ${sprite.id} added to scene ${scene.id}`) // Apply scene-specific effects to sprites if (scene.id === 'underwater-cave') { sprite.addFilter('underwater-distortion') sprite.addParticleEffect('bubbles', { rate: 0.1 }) } if (scene.id === 'desert-oasis') { sprite.addFilter('heat-shimmer') if (sprite.type === 'player') { sprite.addStatusEffect('heat-exhaustion-risk') } } // Add sprite to minimap if (scene.minimap && sprite.showOnMinimap) { scene.minimap.addSprite(sprite) } // Set up sprite lighting if (scene.lightingSystem) { if (sprite.type === 'player') { sprite.lightSource = scene.lightingSystem.addLight({ x: sprite.x, y: sprite.y, radius: 100, intensity: 0.8, color: '#ffffff' }) } if (sprite.type === 'torch') { sprite.lightSource = scene.lightingSystem.addLight({ x: sprite.x, y: sprite.y, radius: 80, intensity: 1.0, color: '#ff8800', flicker: true }) } } // Play spawn sound if (sprite.spawnSound) { scene.playSound(sprite.spawnSound, { x: sprite.x, y: sprite.y, volume: 0.5 }) } // Add to collision system if (sprite.hasCollision) { scene.collisionSystem.addSprite(sprite) } } } ``` ### onRemoveSprite **Description:** Called when a sprite is removed from the scene **Parameters:** * `scene: RpgSceneMap` - The scene instance * `sprite: RpgSprite` - The sprite being removed **Example:** ```ts theme={null} const sceneMap: RpgSceneMapHooks = { onRemoveSprite(scene: RpgSceneMap, sprite: RpgSprite) { console.log(`➖ Sprite ${sprite.id} removed from scene ${scene.id}`) // Remove from minimap if (scene.minimap) { scene.minimap.removeSprite(sprite) } // Remove lighting if (sprite.lightSource) { scene.lightingSystem.removeLight(sprite.lightSource) } // Remove from collision system if (sprite.hasCollision) { scene.collisionSystem.removeSprite(sprite) } // Play despawn effects if (sprite.type === 'enemy') { scene.addParticleEffect('enemy-death', sprite.x, sprite.y) scene.playSound('enemy-death', { x: sprite.x, y: sprite.y, volume: 0.7 }) } if (sprite.type === 'item') { scene.addParticleEffect('item-pickup', sprite.x, sprite.y) scene.playSound('item-pickup', { x: sprite.x, y: sprite.y, volume: 0.5 }) } // Clean up sprite-specific resources if (sprite.customResources) { sprite.customResources.forEach(resource => { scene.unloadResource(resource) }) } // Update scene statistics scene.updateSpriteCount(sprite.type, -1) } } ``` ### onChanges **Description:** Called when server data changes on the map **Parameters:** * `scene: RpgSceneMap` - The scene instance * `obj: { data: any, partial: any }` - Change data **Example:** ```ts theme={null} const sceneMap: RpgSceneMapHooks = { onChanges(scene: RpgSceneMap, { data, partial }) { console.log(`🔄 Scene ${scene.id} data changed`) // Handle weather changes if (partial.weather) { scene.setWeather(partial.weather.type, partial.weather.intensity) // Adjust ambient sounds based on weather if (partial.weather.type === 'rain') { scene.addAmbientSound('rain', { volume: partial.weather.intensity }) } else if (partial.weather.type === 'storm') { scene.addAmbientSound('thunder', { volume: 0.8 }) scene.addLightningEffect() } } // Handle time of day changes if (partial.timeOfDay) { scene.setTimeOfDay(partial.timeOfDay) // Adjust lighting const lightLevel = scene.getLightLevelForTime(partial.timeOfDay) scene.setAmbientLight(lightLevel) // Change ambient sounds if (partial.timeOfDay === 'night') { scene.addAmbientSound('night-crickets', { volume: 0.4 }) scene.removeAmbientSound('day-birds') } else if (partial.timeOfDay === 'day') { scene.addAmbientSound('day-birds', { volume: 0.3 }) scene.removeAmbientSound('night-crickets') } } // Handle map events if (partial.events) { Object.keys(partial.events).forEach(eventId => { const eventData = partial.events[eventId] const event = scene.getEvent(eventId) if (event) { event.updateFromServer(eventData) } }) } // Handle global map states if (partial.mapState) { if (partial.mapState.isUnderAttack) { scene.enableCombatMode() scene.playMusic('battle-theme') } else { scene.disableCombatMode() scene.playMusic('ambient-theme') } } // Handle player count changes if (partial.playerCount !== undefined) { scene.updatePlayerCount(partial.playerCount) // Adjust server performance based on player count if (partial.playerCount > 10) { scene.enablePerformanceMode() } else { scene.disablePerformanceMode() } } } } ``` ### onDraw **Description:** Called when the scene is being drawn (each frame) **Parameters:** * `scene: RpgSceneMap` - The scene instance * `t: number` - Current timestamp **Example:** ```ts theme={null} const sceneMap: RpgSceneMapHooks = { onDraw(scene: RpgSceneMap, t: number) { // Update custom visual effects if (scene.customEffects) { scene.customEffects.forEach(effect => { effect.update(t) }) } // Update particle systems if (scene.particleSystem) { scene.particleSystem.update(t) } // Update lighting system if (scene.lightingSystem) { scene.lightingSystem.update(t) } // Update weather effects if (scene.weatherSystem) { scene.weatherSystem.update(t) } // Update day/night cycle if (scene.dayNightCycle) { scene.dayNightCycle.update(t) } // Update camera effects if (scene.camera.shake) { scene.camera.updateShake(t) } // Update UI elements if (scene.ui) { scene.ui.update(t) } // Performance monitoring if (t % 1000 < 16) { // Every second (assuming 60 FPS) scene.updatePerformanceMetrics() } } } ``` ## Complete Example ```ts theme={null} import { RpgSceneMap, RpgSceneMapHooks, RpgSprite, defineModule } from '@rpgjs/client' const sceneMap: RpgSceneMapHooks = { onBeforeLoading(scene: RpgSceneMap) { console.log(`🔄 Preparing to load map: ${scene.id}`) // Initialize scene systems scene.customData = { loadStartTime: Date.now(), weather: 'clear', timeOfDay: 'day', playerCount: 0, effects: [] } // Show loading screen scene.showLoadingScreen(`Loading ${scene.name}...`) // Scene-specific preparations if (scene.id.includes('dungeon')) { scene.enableFogOfWar() scene.setAmbientLight(0.4) } }, onMapLoading(scene: RpgSceneMap, loader: any) { // Add custom resources loader.add(`${scene.id}-music`, `/assets/music/${scene.id}.ogg`) loader.add(`${scene.id}-effects`, `/assets/effects/${scene.id}.json`) // Track progress loader.onProgress.add((loader: any, resource: any) => { scene.updateLoadingProgress(loader.progress, resource.name) }) }, onAfterLoading(scene: RpgSceneMap) { console.log(`✅ Map ${scene.id} loaded successfully`) const loadTime = Date.now() - scene.customData.loadStartTime console.log(`Load time: ${loadTime}ms`) // Hide loading screen scene.hideLoadingScreen() // Initialize scene systems scene.initializeParticleSystem() scene.initializeLightingSystem() scene.initializeWeatherSystem() scene.initializeAudioSystem() // Set up scene-specific features this.setupSceneFeatures(scene) // Show area title if (scene.isNewArea) { scene.showAreaTitle(scene.name, 3000) } }, onAddSprite(scene: RpgSceneMap, sprite: RpgSprite) { console.log(`➕ Adding sprite ${sprite.id} to ${scene.id}`) // Apply scene effects this.applySceneEffectsToSprite(scene, sprite) // Add to systems if (scene.minimap) scene.minimap.addSprite(sprite) if (scene.lightingSystem) this.setupSpriteLighting(scene, sprite) if (scene.collisionSystem && sprite.hasCollision) { scene.collisionSystem.addSprite(sprite) } // Update counters scene.customData.playerCount += sprite.type === 'player' ? 1 : 0 }, onRemoveSprite(scene: RpgSceneMap, sprite: RpgSprite) { console.log(`➖ Removing sprite ${sprite.id} from ${scene.id}`) // Remove from systems if (scene.minimap) scene.minimap.removeSprite(sprite) if (sprite.lightSource) scene.lightingSystem.removeLight(sprite.lightSource) if (scene.collisionSystem) scene.collisionSystem.removeSprite(sprite) // Play removal effects this.playRemovalEffects(scene, sprite) // Update counters scene.customData.playerCount -= sprite.type === 'player' ? 1 : 0 }, onChanges(scene: RpgSceneMap, { data, partial }) { // Handle weather changes if (partial.weather) { scene.setWeather(partial.weather.type, partial.weather.intensity) } // Handle time changes if (partial.timeOfDay) { scene.setTimeOfDay(partial.timeOfDay) const lightLevel = this.getLightLevelForTime(partial.timeOfDay) scene.setAmbientLight(lightLevel) } // Handle events if (partial.events) { this.updateSceneEvents(scene, partial.events) } }, onDraw(scene: RpgSceneMap, t: number) { // Update all systems scene.particleSystem?.update(t) scene.lightingSystem?.update(t) scene.weatherSystem?.update(t) scene.dayNightCycle?.update(t) // Update custom effects scene.customData.effects.forEach(effect => effect.update(t)) // Performance monitoring if (t % 1000 < 16) { scene.updatePerformanceMetrics() } }, // Helper methods setupSceneFeatures(scene: RpgSceneMap) { switch (scene.id) { case 'forest-clearing': scene.addAmbientSound('forest-birds', { volume: 0.3 }) scene.addParticleEffect('falling-leaves') break case 'dark-cave': scene.addAmbientSound('cave-drips', { volume: 0.4 }) scene.setAmbientLight(0.2) break case 'town-square': scene.addAmbientSound('town-chatter', { volume: 0.2 }) scene.enableDayNightCycle() break } }, applySceneEffectsToSprite(scene: RpgSceneMap, sprite: RpgSprite) { if (scene.id === 'underwater-level') { sprite.addFilter('underwater-distortion') sprite.addParticleEffect('bubbles') } if (scene.id === 'desert-area') { sprite.addFilter('heat-shimmer') } }, setupSpriteLighting(scene: RpgSceneMap, sprite: RpgSprite) { if (sprite.type === 'player') { sprite.lightSource = scene.lightingSystem.addLight({ x: sprite.x, y: sprite.y, radius: 100, intensity: 0.8, color: '#ffffff' }) } }, playRemovalEffects(scene: RpgSceneMap, sprite: RpgSprite) { if (sprite.type === 'enemy') { scene.addParticleEffect('enemy-death', sprite.x, sprite.y) scene.playSound('enemy-death', { x: sprite.x, y: sprite.y }) } }, getLightLevelForTime(timeOfDay: string): number { switch (timeOfDay) { case 'dawn': return 0.6 case 'day': return 1.0 case 'dusk': return 0.7 case 'night': return 0.3 default: return 1.0 } }, updateSceneEvents(scene: RpgSceneMap, events: any) { Object.keys(events).forEach(eventId => { const event = scene.getEvent(eventId) if (event) { event.updateFromServer(events[eventId]) } }) } } export default defineModule({ sceneMap }) ``` # Client Sprite Hooks Source: https://v5.rpgjs.dev/hooks/client-sprite-hooks Guide for Client Sprite Hooks in RPGJS. # Client Sprite Hooks Sprite hooks allow you to customize the behavior of sprites (players and events) on the client side. These hooks are defined in the `sprite` property of your client module. ## Usage ```ts theme={null} import { RpgSprite, RpgSpriteHooks, defineModule } from '@rpgjs/client' const sprite: RpgSpriteHooks = { onInit(sprite: RpgSprite) { console.log(`Sprite ${sprite.id} initialized`) }, onChanges(sprite: RpgSprite, data: any, old: any) { console.log(`Sprite ${sprite.id} data changed`) } } export default defineModule({ sprite }) ``` ## Available Hooks ### onInit **Description:** Called when a sprite is initialized on the client **Parameters:** * `sprite: RpgSprite` - The sprite instance **Example:** ```ts theme={null} const sprite: RpgSpriteHooks = { onInit(sprite: RpgSprite) { console.log(`🎭 Sprite ${sprite.id} initialized`) // Set default properties sprite.opacity = 1.0 sprite.scale = { x: 1, y: 1 } // Initialize based on sprite type if (sprite.type === 'player') { // Add player-specific visual effects sprite.addGlow('#00ff00', 0.3) sprite.showNameplate(sprite.name) // Add health bar sprite.healthBar = sprite.addChild('health-bar', { width: 32, height: 4, offsetY: -40 }) } if (sprite.type === 'npc') { // Add interaction indicator sprite.interactionIcon = sprite.addChild('interaction-icon', { graphic: 'exclamation', visible: false, offsetY: -50 }) } if (sprite.type === 'enemy') { // Add enemy visual effects sprite.addGlow('#ff0000', 0.2) sprite.showHealthBar() } // Play spawn animation sprite.playAnimation('spawn') } } ``` ### onDestroy **Description:** Called when a sprite is removed from the scene **Parameters:** * `sprite: RpgSprite` - The sprite instance **Example:** ```ts theme={null} const sprite: RpgSpriteHooks = { onDestroy(sprite: RpgSprite) { console.log(`🗑️ Sprite ${sprite.id} destroyed`) // Clean up visual effects if (sprite.glow) { sprite.removeGlow() } if (sprite.nameplate) { sprite.nameplate.destroy() } if (sprite.healthBar) { sprite.healthBar.destroy() } // Play destruction animation if (sprite.type === 'enemy') { sprite.playAnimation('death', () => { // Animation complete callback sprite.showParticleEffect('explosion') }) } // Clean up event listeners sprite.removeAllListeners() // Clear timers if (sprite.timers) { sprite.timers.forEach(timer => clearTimeout(timer)) } } } ``` ### onBeforeRemove **Description:** Called when the server requests sprite removal, before the sprite disappears from the scene. Return a promise to keep the sprite visible while a transition runs. The server does not decide how the transition is rendered. It only sends a removal context with `event.remove()`. The client reads that context in `onBeforeRemove` and can play an animation, sound, particle effect, GUI transition, or any combination of effects. **Parameters:** * `sprite: RpgSprite` - The sprite instance * `context.reason?: string` - Removal reason, such as `"defeated"` * `context.data?: any` - Custom data sent by `event.remove()` * `context.transition?: object` - Optional transition metadata. This is a project-defined payload; RPGJS forwards it to the client. * `context.timeoutMs?: number` - Safety timeout used by the server before the event is removed from the map. **Server example:** ```ts theme={null} event.remove({ reason: 'defeated', transition: { type: 'enemy-death', animation: 'die', graphic: 'slime_die', sound: 'slime-death', duration: 700 }, timeoutMs: 700 }) ``` **Client example:** ```ts theme={null} import { inject, RpgClientEngine, RpgSprite, RpgSpriteHooks } from '@rpgjs/client' const sprite: RpgSpriteHooks = { async onBeforeRemove(sprite: RpgSprite, context) { if (context.reason !== 'defeated') return const engine = inject(RpgClientEngine) const transition = context.transition const tasks = [] if (transition?.animation) { const timeoutMs = context.timeoutMs ?? transition.duration ?? 700 if (transition.graphic) { tasks.push(sprite.setAnimation(transition.animation, transition.graphic, 1, { timeoutMs })) } else { tasks.push(sprite.setAnimation(transition.animation, 1, { timeoutMs })) } } if (transition?.sound) { tasks.push(engine.playSound(transition.sound)) } await Promise.all(tasks) } } ``` `sprite.setAnimation()` returns a promise for finite animations. Awaiting it keeps the sprite visible until the animation finishes, is interrupted, or reaches the configured timeout. `timeoutMs` is the maximum time the server keeps the removed sprite visible on clients. Set it to at least the expected duration of the client transition. The client hook may finish earlier, but the event is removed from the map when the server timeout expires. ### onChanges **Description:** Called when sprite data changes (from server updates) **Parameters:** * `sprite: RpgSprite` - The sprite instance * `data: any` - New data * `old: any` - Previous data **Example:** ```ts theme={null} const sprite: RpgSpriteHooks = { onChanges(sprite: RpgSprite, data: any, old: any) { console.log(`📊 Sprite ${sprite.id} data updated`) // Handle health changes if (data.hp !== old.hp) { const healthPercent = data.hp / data.maxHp // Update health bar if (sprite.healthBar) { sprite.healthBar.setPercent(healthPercent) } // Show damage/healing numbers const difference = data.hp - old.hp if (difference < 0) { // Damage taken sprite.showFloatingText(`-${Math.abs(difference)}`, '#ff0000') sprite.playAnimation('hit') sprite.shake(200, 3) } else if (difference > 0) { // Healing received sprite.showFloatingText(`+${difference}`, '#00ff00') sprite.playAnimation('heal') } // Low health warning if (healthPercent < 0.25) { sprite.addGlow('#ff0000', 0.5) sprite.startBlinking() } else { sprite.removeGlow() sprite.stopBlinking() } } // Handle level changes if (data.level !== old.level) { sprite.showFloatingText('LEVEL UP!', '#ffff00', { size: 24, duration: 3000 }) sprite.playAnimation('level-up') sprite.showParticleEffect('level-up-sparkles') } // Handle equipment changes if (data.equipment !== old.equipment) { sprite.updateEquipmentVisuals(data.equipment) } // Handle state changes if (data.states !== old.states) { sprite.updateStateVisuals(data.states, old.states) } // Handle name changes if (data.name !== old.name) { if (sprite.nameplate) { sprite.nameplate.setText(data.name) } } } } ``` ### onUpdate **Description:** Called at each frame for sprite updates **Parameters:** * `sprite: RpgSprite` - The sprite instance * `obj: any` - Update data **Example:** ```ts theme={null} const sprite: RpgSpriteHooks = { onUpdate(sprite: RpgSprite, obj: any) { // Update custom animations if (sprite.customAnimations) { sprite.customAnimations.forEach(animation => { animation.update(obj.deltaTime) }) } // Update floating elements if (sprite.nameplate) { sprite.nameplate.followSprite(sprite) } if (sprite.healthBar) { sprite.healthBar.followSprite(sprite) } // Update particle effects if (sprite.particleEffects) { sprite.particleEffects.forEach(effect => { effect.update(obj.deltaTime) }) } // Handle breathing animation for idle sprites if (sprite.isIdle && !sprite.isMoving) { const breathingScale = 1 + Math.sin(obj.time * 0.002) * 0.02 sprite.scale.y = breathingScale } // Update glow effects if (sprite.glowEffect) { const glowIntensity = 0.3 + Math.sin(obj.time * 0.005) * 0.2 sprite.glowEffect.intensity = glowIntensity } // Update interaction indicators if (sprite.interactionIcon && sprite.type === 'npc') { const player = sprite.scene.getPlayerSprite() if (player && sprite.distanceTo(player) < 64) { sprite.interactionIcon.visible = true sprite.interactionIcon.y = -50 + Math.sin(obj.time * 0.01) * 5 } else { sprite.interactionIcon.visible = false } } } } ``` ### onMove **Description:** Called when the sprite's position changes **Parameters:** * `sprite: RpgSprite` - The sprite instance **Example:** ```ts theme={null} const sprite: RpgSpriteHooks = { onMove(sprite: RpgSprite) { console.log(`🚶 Sprite ${sprite.id} moved to (${sprite.x}, ${sprite.y})`) // Create footstep particles if (sprite.type === 'player' && sprite.isOnGround) { sprite.createFootstepParticles() } // Update shadow position if (sprite.shadow) { sprite.shadow.x = sprite.x sprite.shadow.y = sprite.y + sprite.height } // Play movement sound if (sprite.isMoving && !sprite.movementSound?.isPlaying) { const terrain = sprite.scene.getTerrainAt(sprite.x, sprite.y) sprite.playMovementSound(terrain) } // Update camera if this is the main player if (sprite.isMainPlayer) { sprite.scene.camera.followSprite(sprite) } // Check for area triggers const areas = sprite.scene.getAreasAt(sprite.x, sprite.y) areas.forEach(area => { if (!sprite.currentAreas.includes(area.id)) { sprite.onEnterArea(area) } }) // Update lighting if sprite has a light source if (sprite.lightSource) { sprite.lightSource.x = sprite.x sprite.lightSource.y = sprite.y } } } ``` ## Complete Example ```ts theme={null} import { RpgSprite, RpgSpriteHooks, defineModule } from '@rpgjs/client' const sprite: RpgSpriteHooks = { onInit(sprite: RpgSprite) { console.log(`🎭 Initializing sprite: ${sprite.id} (${sprite.type})`) // Common initialization sprite.opacity = 1.0 sprite.customData = {} sprite.visualEffects = [] sprite.timers = [] // Type-specific initialization switch (sprite.type) { case 'player': this.initializePlayer(sprite) break case 'npc': this.initializeNPC(sprite) break case 'enemy': this.initializeEnemy(sprite) break case 'item': this.initializeItem(sprite) break } // Play spawn animation sprite.playAnimation('spawn', () => { sprite.isReady = true }) }, onDestroy(sprite: RpgSprite) { console.log(`🗑️ Destroying sprite: ${sprite.id}`) // Clean up visual effects sprite.visualEffects.forEach(effect => effect.destroy()) // Clear timers sprite.timers.forEach(timer => clearTimeout(timer)) // Remove UI elements if (sprite.nameplate) sprite.nameplate.destroy() if (sprite.healthBar) sprite.healthBar.destroy() if (sprite.interactionIcon) sprite.interactionIcon.destroy() // Play destruction effect if (sprite.type === 'enemy') { sprite.scene.addParticleEffect('explosion', sprite.x, sprite.y) } }, onChanges(sprite: RpgSprite, data: any, old: any) { // Health changes if (data.hp !== old.hp) { this.handleHealthChange(sprite, data.hp, old.hp, data.maxHp) } // Level changes if (data.level !== old.level) { this.handleLevelUp(sprite, data.level, old.level) } // Equipment changes if (JSON.stringify(data.equipment) !== JSON.stringify(old.equipment)) { this.updateEquipmentVisuals(sprite, data.equipment) } // State changes if (JSON.stringify(data.states) !== JSON.stringify(old.states)) { this.updateStateEffects(sprite, data.states, old.states) } }, onUpdate(sprite: RpgSprite, { deltaTime, time }) { // Update custom animations sprite.visualEffects.forEach(effect => effect.update(deltaTime)) // Update UI elements if (sprite.nameplate) { sprite.nameplate.x = sprite.x sprite.nameplate.y = sprite.y - sprite.height - 10 } // Breathing animation for idle sprites if (!sprite.isMoving && sprite.isReady) { const breathScale = 1 + Math.sin(time * 0.003) * 0.01 sprite.scale.y = breathScale } // Update interaction indicators if (sprite.interactionIcon) { const player = sprite.scene.getMainPlayer() const distance = sprite.distanceTo(player) sprite.interactionIcon.visible = distance < 64 if (sprite.interactionIcon.visible) { sprite.interactionIcon.y = sprite.y - sprite.height - 20 + Math.sin(time * 0.01) * 3 } } }, onMove(sprite: RpgSprite) { // Create movement particles if (sprite.type === 'player' && sprite.isMoving) { const terrain = sprite.scene.getTerrainAt(sprite.x, sprite.y) this.createMovementParticles(sprite, terrain) } // Update camera for main player if (sprite.isMainPlayer) { sprite.scene.camera.centerOn(sprite.x, sprite.y) } // Update lighting if (sprite.lightSource) { sprite.lightSource.position.set(sprite.x, sprite.y) } }, // Helper methods initializePlayer(sprite: RpgSprite) { sprite.addGlow('#00ff00', 0.2) sprite.nameplate = sprite.scene.addUI('nameplate', { text: sprite.name, x: sprite.x, y: sprite.y - sprite.height - 10 }) sprite.healthBar = sprite.scene.addUI('health-bar', { maxValue: sprite.maxHp, currentValue: sprite.hp, x: sprite.x - 16, y: sprite.y - sprite.height - 25 }) }, initializeNPC(sprite: RpgSprite) { sprite.interactionIcon = sprite.scene.addUI('interaction-icon', { graphic: 'chat-bubble', x: sprite.x, y: sprite.y - sprite.height - 20, visible: false }) }, initializeEnemy(sprite: RpgSprite) { sprite.addGlow('#ff0000', 0.3) sprite.healthBar = sprite.scene.addUI('enemy-health-bar', { maxValue: sprite.maxHp, currentValue: sprite.hp, x: sprite.x - 16, y: sprite.y - sprite.height - 30 }) }, initializeItem(sprite: RpgSprite) { sprite.addGlow('#ffff00', 0.4) sprite.startFloating() }, handleHealthChange(sprite: RpgSprite, newHp: number, oldHp: number, maxHp: number) { const difference = newHp - oldHp if (difference < 0) { // Damage sprite.showFloatingText(`-${Math.abs(difference)}`, '#ff0000') sprite.playAnimation('hit') sprite.shake(150, 2) } else if (difference > 0) { // Healing sprite.showFloatingText(`+${difference}`, '#00ff00') sprite.playAnimation('heal') } // Update health bar if (sprite.healthBar) { sprite.healthBar.setValue(newHp, maxHp) } // Low health effects const healthPercent = newHp / maxHp if (healthPercent < 0.25) { sprite.addStatusEffect('low-health') } else { sprite.removeStatusEffect('low-health') } }, handleLevelUp(sprite: RpgSprite, newLevel: number, oldLevel: number) { sprite.showFloatingText('LEVEL UP!', '#ffff00', { size: 24, duration: 3000 }) sprite.playAnimation('level-up') sprite.scene.addParticleEffect('level-up-sparkles', sprite.x, sprite.y) sprite.scene.playSound('level-up') } } export default defineModule({ sprite }) ``` # Server Engine Hooks Source: https://v5.rpgjs.dev/hooks/server-engine-hooks Guide for Server Engine Hooks in RPGJS. # Server Engine Hooks Server engine hooks allow you to listen to server-level events and customize server behavior. These hooks are defined in the `engine` property of your server module. ## Usage ```ts theme={null} import { RpgServerEngine, RpgServerEngineHooks, defineModule } from '@rpgjs/server' const engine: RpgServerEngineHooks = { onStart(server: RpgServerEngine) { console.log('Server is starting...') }, onStep(server: RpgServerEngine, metrics) { // Called after each queued map tick console.log(metrics.durationMs, metrics.pendingInputs) }, auth(server: RpgServerEngine, socket: any) { // Custom authentication logic const token = socket.handshake.query.token if (!token) { throw 'Authentication failed: No token provided' } return 'user-id-123' } } export default defineModule({ engine }) ``` ## Available Hooks ### onStart **Description:** Called when the server starts **Parameters:** * `server: RpgServerEngine` - The server engine instance **Example:** ```ts theme={null} const engine: RpgServerEngineHooks = { onStart(server: RpgServerEngine) { console.log('Server started successfully') // Initialize global server data server.globalData = { startTime: Date.now(), playerCount: 0 } } } ``` ### onStep **Description:** Called for map rooms, once after each queued map tick has been processed. It is not emitted for lobby rooms. A queued tick is one pass through the server scheduler; depending on the accumulated delta, it can execute zero, one, or several fixed simulation steps. Empty maps stop their automatic loop, so observability does not keep an idle room active. **Parameters:** * `server: RpgServerEngine` - The server engine instance * `metrics: RpgServerStepMetrics` - Metrics for the processed map tick: * `tick` - Current cumulative fixed simulation tick after processing * `durationMs` - Wall-clock processing duration in milliseconds, excluding the hook itself * `scheduledDeltaMs` - Delta in milliseconds passed to the queued tick * `queuedDeltaMs` - Delta in milliseconds that arrived during processing and remains queued * `fixedSteps` - Number of fixed simulation steps executed (which can be zero or greater) * `pendingInputs` - Total unprocessed player inputs after the tick Handlers that only accept `server` remain compatible. Async handlers are awaited in the tick queue. If a handler throws or rejects, RPGJS logs the error for the automatic loop and retries queued simulation work on a later tick. Avoid network I/O here because it directly increases the server backlog. **Example:** ```ts theme={null} import type { RpgServerEngineHooks, RpgServerStepMetrics } from '@rpgjs/server' type MetricsWindow = { startedAt: number fixedSteps: number durationMs: number maxQueuedDeltaMs: number maxPendingInputs: number } function createMetricsWindow(): MetricsWindow { return { startedAt: Date.now(), fixedSteps: 0, durationMs: 0, maxQueuedDeltaMs: 0, maxPendingInputs: 0 } } // Application-owned queue drained by an exporter outside the tick hook. const metricsQueue: Array = [] let window = createMetricsWindow() const engine: RpgServerEngineHooks = { onStep(server, metrics: RpgServerStepMetrics) { window.fixedSteps += metrics.fixedSteps window.durationMs += metrics.durationMs window.maxQueuedDeltaMs = Math.max( window.maxQueuedDeltaMs, metrics.queuedDeltaMs ) window.maxPendingInputs = Math.max( window.maxPendingInputs, metrics.pendingInputs ) if (Date.now() - window.startedAt >= 10_000) { // Enqueue this aggregate for an out-of-band exporter instead of // performing analytics or network I/O on every server tick. metricsQueue.push({ mapId: server.getCurrentRoomId(), ...window }) window = createMetricsWindow() } } } ``` ### auth **Description:** Flexible authentication function for player connections. This function is called during each RPGJS room connection and should handle credential verification before the player is allowed to join the room. **Parameters:** * `server: RpgServerEngine` - The server engine instance * `socket: any` - The socket instance for the connecting player **Returns:** * `Promise | string | undefined` - Player's stable public identifier if authentication succeeds, or undefined to generate an ID automatically **Throws:** * `string` - Error message if authentication fails The returned identifier becomes the player `publicId` used by RPGJS and Signe room user collections. In MMORPG mode, the hook is called when the client connects to the lobby and again when it reconnects to map rooms. Send the same token on each connection and return the same id for the same account. For browser MMORPG clients, pass the token through `provideMmorpg({ query })`; see [Authentication](/advanced/auth). **Example:** ```ts theme={null} const engine: RpgServerEngineHooks = { async auth(server: RpgServerEngine, socket: any) { const token = socket.handshake.query.token if (!token) { throw 'Authentication failed: No token provided' } try { // Verify token with your authentication service const user = await verifyJWTToken(token) return user.id } catch (error) { throw 'Authentication failed: Invalid token' } } } ``` ## Engine Runtime API The `server` argument is an `RpgServerEngine` instance. It exposes stable helpers for reading the current RPGJS room without depending on low-level Signe internals. ```ts theme={null} const engine: RpgServerEngineHooks = { onStart(server: RpgServerEngine) { const room = server.getCurrentRoom() const roomInfo = server.getCurrentRoomInfo() const globalConfig = server.globalConfig console.log(roomInfo?.id, roomInfo?.kind, globalConfig) } } ``` ### getCurrentRoom Returns the current RPGJS room instance, such as `LobbyRoom` or `RpgMap`. ```ts theme={null} const room = server.getCurrentRoom() ``` This is different from `server.room`, which is the low-level Signe/Party room wrapper. ### getCurrentRoomInfo Returns stable metadata for the current room: ```ts theme={null} const info = server.getCurrentRoomInfo() if (info?.kind === 'map') { console.log(`Current map room: ${info.name}`) } ``` The returned object contains: * `id`: full room id, such as `lobby-1` or `map-town` * `kind`: `lobby`, `map`, or `unknown` * `name`: room id without the RPGJS prefix * `className`: runtime room class name * `playersCount`: number of players in the room when available * `autoSync`: whether automatic sync is enabled * `hasDatabase`: whether the room exposes a database signal You can also use `getCurrentRoomId()` and `getCurrentRoomKind()` when you only need one value. ### globalConfig `server.globalConfig` is provided for compatibility with older server-engine usage: ```ts theme={null} const globalConfig = server.globalConfig ``` In map rooms, it returns the current map's `globalConfig`. In lobby rooms or before room initialization, it returns the last assigned value or `{}`. ### app and io `server.app` and `server.io` are optional compatibility handles. RPGJS v5 does not create Express or socket.io automatically, but custom Node entries can assign these properties when migrating older code: ```ts theme={null} server.app = app server.io = wsServer ``` ## Complete Example ```ts theme={null} import { RpgServerEngine, RpgServerEngineHooks, defineModule } from '@rpgjs/server' import { verifyJWTToken } from './auth-service' const engine: RpgServerEngineHooks = { onStart(server: RpgServerEngine) { console.log('🚀 RPG Server started') server.globalData = { startTime: Date.now(), events: [] } }, onStep(server: RpgServerEngine) { // The legacy one-argument handler remains supported. // This runs after each processed map tick, not after each fixed step. if (server.globalData.events.length > 0) { const event = server.globalData.events.shift() console.log('Processing global event:', event) } }, async auth(server: RpgServerEngine, socket: any) { const { token, guestMode } = socket.handshake.query if (guestMode === 'true') { // Allow guest connections return `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` } if (!token) { throw 'Authentication required' } try { const user = await verifyJWTToken(token) console.log(`User ${user.username} authenticated`) return user.id } catch (error) { throw 'Invalid authentication token' } } } export default defineModule({ engine }) ``` # Server Event Hooks Source: https://v5.rpgjs.dev/hooks/server-event-hooks Guide for Server Event Hooks in RPGJS. # Server Event Hooks Event hooks allow you to customize the behavior of events on the server side. These hooks are defined in the `event` property of your server module and apply to all events in your game. ## Usage ```ts theme={null} import { RpgEvent, RpgPlayer, RpgMap, RpgEventHooks, defineModule } from '@rpgjs/server' const event: RpgEventHooks = { onInit(event: RpgEvent) { console.log(`Event ${event.id} initialized`) }, onAction(event: RpgEvent, player: RpgPlayer) { console.log(`Player ${player.name} interacted with ${event.id}`) } } export default defineModule({ event }) ``` ## Available Hooks ### onInit **Description:** Called when an event is initialized on the map. Use `onInit` for base event setup that does not depend on a change-detection cycle yet: initial graphic, movement route, speed, direction, attached shapes, or default metadata. **Parameters:** * `event: RpgEvent` - The event instance **Example:** ```ts theme={null} const event: RpgEventHooks = { onInit(event: RpgEvent) { console.log(`🎯 Event ${event.id} initialized`) } } ``` ### onAction **Description:** Called when a player interacts with an event (typically by pressing the action key) **Parameters:** * `event: RpgEvent` - The event instance * `player: RpgPlayer` - The player who interacted with the event **Example:** ```ts theme={null} const event: RpgEventHooks = { async onAction(event: RpgEvent, player: RpgPlayer) { console.log(`${player.name} interacted with ${event.name}`) switch (event.name) { case 'treasure-chest': if (!event.getVariable('isOpened')) { event.setVariable('isOpened', true) player.addItem('gold', 100) player.showText('You found 100 gold!') event.setGraphic('opened-chest') } else { player.showText('The chest is empty.') } break case 'save-point': player.save() await player.showText('Game saved!') event.showAnimation('save-effect') break case 'teleporter': player.changeMap('other_map', { x: event.x, y: event.y }) break } } } ``` ### onBeforeCreated **Description:** Called before an event is created, allowing you to modify the event object or return a custom event configuration **Parameters:** * `object: any` - The raw event data from the map, typically `{ id?, x?, y?, event }` for positioned events * `map: RpgMap` - The map instance where the event will be created **Returns:** * `any` - Modified event configuration **Example:** ```ts theme={null} const event: RpgEventHooks = { onBeforeCreated(object: any, map: RpgMap) { console.log(`Creating event ${object.name} on map ${map.id}`) } } ``` ### onPlayerTouch **Description:** Called when a player touches an event (collision detection) **Parameters:** * `event: RpgEvent` - The event instance * `player: RpgPlayer` - The player who touched the event **Example:** ```ts theme={null} const event: RpgEventHooks = { onPlayerTouch(event: RpgEvent, player: RpgPlayer) { console.log(`${player.name} touched ${event.name}`) } } ``` ### onTouch / onTouchEnd **Description:** Called when an event starts or stops touching a player or another event. `onTouch` is called once when a collision pair begins. `onTouchEnd` is called once when that pair separates. For player/event collisions, `onTouch` runs before `onPlayerTouch` so existing `onPlayerTouch` code keeps working. For event/event collisions, both events receive the hook. **Parameters:** * `other: RpgPlayer | RpgEvent` - The player or event touching this event * `context: RpgTouchContext` - Collision context with `self`, `other`, `otherType`, `player`, `phase`, `pairId`, and `map` **Example:** ```ts theme={null} import type { EventDefinition, RpgTouchContext } from '@rpgjs/server' import type { RpgEvent, RpgPlayer } from '@rpgjs/server' export const PressurePlate: EventDefinition = { name: 'PressurePlate', onTouch(other: RpgPlayer | RpgEvent, context: RpgTouchContext) { if (other.name === 'Stone') { context.map.setVariable('temple.door.open', true) } }, onTouchEnd(other: RpgPlayer | RpgEvent, context: RpgTouchContext) { if (other.name === 'Stone') { context.map.setVariable('temple.door.open', false) } } } ``` ### onDetectInShape / onDetectOutShape **Description:** Called when a player enters or leaves an event's detection shape **Parameters:** * `event: RpgEvent` - The event instance * `player: RpgPlayer` - The player who entered/left the shape * `shape: RpgShape` - The shape instance **Example:** ```ts theme={null} const event: RpgEventHooks = { onDetectInShape(event: RpgEvent, player: RpgPlayer, shape: RpgShape) { }, onDetectOutShape(event: RpgEvent, player: RpgPlayer, shape: RpgShape) { } } ``` ### onInShape / onOutShape **Description:** Called when the event itself enters or leaves a shape **Parameters:** * `event: RpgEvent` - The event instance * `shape: RpgShape` - The shape instance **Example:** ```ts theme={null} const event: RpgEventHooks = { onInShape(event: RpgEvent, shape: RpgShape) { }, onOutShape(event: RpgEvent, shape: RpgShape) { } } ``` ### onChanges **Description:** Called during the change-detection cycle for a player. This hook is the reactive part of event design. When player state changes, especially player variables, RPGJS can re-run the cycle and call `onChanges(event, player)` so the event can recompute its state for that player. You can also force that cycle with `player.syncChanges()`. **Parameters:** * `event: RpgEvent` - The event instance * `player: RpgPlayer` - The player who caused the change (if applicable) **Example:** ```ts theme={null} const event: RpgEventHooks = { onChanges(event: RpgEvent, player: RpgPlayer) { const isOpened = player.getVariable('chest-opened') event.setGraphic(isOpened ? 'chest-opened' : 'chest-closed') } } ``` ## Designing `onInit` and `onChanges` together In practice, these two hooks often share similar logic: * `onInit` sets a correct initial state when the event appears * `onChanges` keeps that state synchronized with player data later Typical example: a chest. ```ts theme={null} const event: RpgEventHooks = { onInit(event: RpgEvent) { event.setGraphic('chest-closed') }, onChanges(event: RpgEvent, player: RpgPlayer) { const isOpened = player.getVariable('chest-opened') event.setGraphic(isOpened ? 'chest-opened' : 'chest-closed') } } ``` Use player variables for this kind of state, because they persist with the player and travel with the player snapshot across saves and map transfers. Use map variables instead when the state belongs to the shared map room: ```ts theme={null} import type { EventDefinition } from '@rpgjs/server' const Door: EventDefinition = { name: 'Door', onChanges() { const map = this.getCurrentMap() const open = map?.getVariable('temple.door.open') === true this.through = open this.setGraphic(open ? 'door-open' : 'door-closed') } } ``` `map.setVariable()`, `map.removeVariable()`, and `map.clearVariables()` automatically re-run `onChanges` for visible players and events on the map. Player variable writes automatically re-run the same cycle for that player. # Server Map Hooks Source: https://v5.rpgjs.dev/hooks/server-map-hooks Guide for Server Map Hooks in RPGJS. # Server Map Hooks Map hooks are global hooks that apply to all maps. They are defined in the `map` property of your server module. ## Usage ```ts theme={null} import { defineModule, type RpgServer } from "@rpgjs/server"; export default defineModule({ map: { onBeforeUpdate(mapData, map) { // Mutate or validate the incoming data before the update is applied. }, onPhysicsInit(map, { mapData }) { // Build shared physics metadata }, onPhysicsEntityAdd(map, { owner, entity, kind }) { // Attach entity-level collision rules }, onPhysicsEntityRemove(map, { owner }) { // Cleanup entity-level rules }, onPhysicsReset(map) { // Cleanup map-level physics extension state }, }, }); ``` ## Lifecycle Hooks ### `onBeforeUpdate(mapData, map)` Called before a map update is applied. Use this to inspect, normalize, or validate map data before `loadPhysic()`. Its return value is ignored; mutate the provided data when normalization is required. ### `onLoad(map)` Called when a map is loaded and initialized. ### `onJoin(player, map)` Called when a player joins a map. ### `onLeave(player, map)` Called when a player leaves a map. ## Physics Extension Hooks ### `onPhysicsInit(map, context)` Called when map physics is initialized. * `context.mapData`: map data used by `loadPhysic()` ### `onPhysicsEntityAdd(map, context)` Called when a dynamic physics body is created. * `context.owner`: player/event owning the body * `context.entity`: physics entity instance * `context.kind`: `"hero" | "npc" | "generic"` ### `onPhysicsEntityRemove(map, context)` Called before a dynamic physics body is removed. Use this to unregister handlers attached in `onPhysicsEntityAdd`. ### `onPhysicsReset(map)` Called when map physics is reset before a reload. Use this for map-level cleanup. ## Shared Logic Pattern For client prediction parity, put collision rules in shared functions, then call them from: * `server.map.onPhysics*` * `client.sceneMap.onPhysics*` This guarantees the same movement constraints on both sides. # Server Player Hooks Source: https://v5.rpgjs.dev/hooks/server-player-hooks Guide for Server Player Hooks in RPGJS. # Server Player Hooks Player hooks allow you to listen to player-specific events and customize player behavior on the server side. These hooks are defined in the `player` property of your server module. ## Usage ```ts theme={null} import { RpgPlayer, RpgMap, RpgPlayerHooks, defineModule } from '@rpgjs/server' const player: RpgPlayerHooks = { onConnected(player: RpgPlayer) { console.log(`Player ${player.id} connected`) player.changeMap('spawn-map') }, onStart(player: RpgPlayer) { console.log(`Player ${player.id} started the game`) }, onJoinMap(player: RpgPlayer, map: RpgMap) { console.log(`Player ${player.id} joined map ${map.id}`) } } export default defineModule({ player }) ``` ## Movement and disconnection `onMove(player)` runs when authoritative physics changes the player's synchronized x/y position, in both standalone RPG and MMORPG modes. It excludes events and unchanged positions. Async handlers do not block the physics step; errors are logged by the runtime. `onDisconnected(player)` runs when the last active connection for the session closes, whether the player is in the lobby, a map, or a custom gameplay room. The room's leave hooks run first. A successful `changeMap()` or `changeRoom()` transfer closes the source connection without dispatching `onDisconnected`. The destination connection starts a new connection lifecycle. ```ts theme={null} const player: RpgPlayerHooks = { onMove(player) { console.log(player.id, player.x(), player.y()) }, onDisconnected(player) { console.log(player.id, 'disconnected') } } ``` ## Custom Properties You can define custom properties that will be synchronized with the client and optionally saved to the database: ```ts theme={null} // First, extend the RpgPlayer interface declare module '@rpgjs/server' { export interface RpgPlayer { gold: number experience: number secretData: string } } const player: RpgPlayerHooks = { props: { gold: { $default: 100, $syncWithClient: true, $permanent: true }, experience: { $default: 0, $syncWithClient: true, $permanent: true }, secretData: { $default: '', $syncWithClient: false, // Not sent to client $permanent: false // Not saved to database } } } ``` ## Available Hooks ## Initializing Default Stats RPGJS can provide built-in default parameters for a player such as `maxHp`, `maxSp`, `str`, `int`, `dex`, and `agi`. Use `player.initializeDefaultStats()` when you want to: * apply the built-in default parameter curves * initialize HP/SP from those max values * ensure the client receives visible HP/SP and parameter values on first game load Typical usage: * call it in `onConnected()` if your game starts immediately * call it in `onStart()` if your game begins after a title screen or another GUI flow If your player data comes from your own database, a save slot, or a snapshot, do not call `player.initializeDefaultStats()` after hydration unless you intentionally want to overwrite those values. In that case, restore your data first and let that state be synchronized to the client. If you only want the built-in parameter curves without restoring HP/SP, use `player.applyDefaultParameters()`. ### onConnected **Description:** Called when a player connects to the server **Parameters:** * `player: RpgPlayer` - The player instance **Example:** ```ts theme={null} const player: RpgPlayerHooks = { onConnected(player: RpgPlayer) { player.initializeDefaultStats() console.log(`Welcome ${player.name}!`) player.gold = 1000 player.changeMap('tutorial-map') // Send welcome message player.showText('Welcome to the game!') } } ``` ### onLoad **Description:** Called after a JSON or slot snapshot has been applied to the player. **Parameters:** * `player: RpgPlayer` - The hydrated player instance * `snapshot: RpgPlayerSnapshot` - The resolved snapshot applied to the player ```ts theme={null} const player: RpgPlayerHooks = { onLoad(player, snapshot) { console.log(`Loaded ${player.id}`, snapshot) } } ``` ### onSave **Description:** Called after the snapshot is created and before a slot is persisted. Calling the legacy `player.save()` overload without a slot only serializes the snapshot and does not emit this hook. ```ts theme={null} const player: RpgPlayerHooks = { async onSave(player, snapshot) { await auditSave(player.id, snapshot) } } ``` ### onJoinMap **Description:** Called when a player joins a map **Parameters:** * `player: RpgPlayer` - The player instance * `map: RpgMap` - The map instance the player joined **Example:** ```ts theme={null} const player: RpgPlayerHooks = { onJoinMap(player: RpgPlayer, map: RpgMap) { console.log(`${player.name} entered ${map.name}`) // Set player position based on map spawn point if (map.spawnPoint) { player.teleport(map.spawnPoint.x, map.spawnPoint.y) } // Apply map-specific effects if (map.id === 'dark-forest') { player.addState('darkness') } } } ``` ### onStart **Description:** Called when the player starts the game from a GUI interaction This hook is executed after a GUI interaction when the GUI returns a `data.id` equal to `'start'`. This is typically used after a title screen or another start menu. **Parameters:** * `player: RpgPlayer` - The player instance **Example:** ```ts theme={null} const player: RpgPlayerHooks = { async onConnected(player: RpgPlayer) { await player.gui('rpg-title-screen').open() }, onStart(player: RpgPlayer) { player.initializeDefaultStats() player.changeMap('starting-map') player.showText('The adventure begins!') } } ``` ### onLeaveMap **Description:** Called when a player leaves a map **Parameters:** * `player: RpgPlayer` - The player instance * `map: RpgMap` - The map instance the player left **Example:** ```ts theme={null} const player: RpgPlayerHooks = { onLeaveMap(player: RpgPlayer, map: RpgMap) { console.log(`${player.name} left ${map.name}`) // Remove map-specific effects if (map.id === 'dark-forest') { player.removeState('darkness') } // Save player progress player.save() } } ``` ### onInput **Description:** Called when the server receives an action from the client. **Parameters:** * `player: RpgPlayer` - The player instance * `input: RpgActionInput` - Action name and optional custom data **Example:** ```ts theme={null} const player: RpgPlayerHooks = { onInput(player, { action, data }) { if (action === 'escape') { // Open menu player.gui('main-menu').open() } console.log('Custom action data:', data) } } ``` ### onLevelUp **Description:** Called when a player increases one level **Parameters:** * `player: RpgPlayer` - The player instance * `nbLevel: number` - Number of levels gained **Example:** ```ts theme={null} const player: RpgPlayerHooks = { onLevelUp(player: RpgPlayer, nbLevel: number) { console.log(`${player.name} gained ${nbLevel} level(s)!`) // Restore health and mana player.hp = player.param.maxHp player.sp = player.param.maxSp // Show level up effect player.showAnimation('level-up-effect') player.showText(`Level Up! You are now level ${player.level}`) // Grant skill points player.skillPoints += nbLevel * 2 } } ``` ### onSkillChange **Description:** Called after a player learns or forgets a skill. **Parameters:** * `player: RpgPlayer` - The player instance * `payload.action: 'learn' | 'forget'` - Skill change type * `payload.skill` - Skill data * `payload.skillId: string` - Skill identifier * `payload.source?: string` - Change source, such as `manual`, `level`, or `studio` * `payload.level?: number` - Level that triggered the change, when available **Example:** ```ts theme={null} const player: RpgPlayerHooks = { onSkillChange(player, payload) { if (payload.action === 'learn') { player.showNotification(`Learned ${payload.skillId}`) } } } ``` ### onDead **Description:** Called when a player's HP drops to 0 **Parameters:** * `player: RpgPlayer` - The player instance **Example:** ```ts theme={null} const player: RpgPlayerHooks = { async onDead(player: RpgPlayer) { const selection = await player.callGameover({ title: 'Game Over', subtitle: 'Choose your fate', entries: [ { id: 'title', label: 'Title Screen' }, { id: 'load', label: 'Load Game' } ] }) if (selection?.id === 'title') { await player.gui('rpg-title-screen').open() } if (selection?.id === 'load') { await player.showLoad() } } } ``` ### onDisconnected **Description:** Called when a player leaves the server **Parameters:** * `player: RpgPlayer` - The player instance **Example:** ```ts theme={null} const player: RpgPlayerHooks = { onDisconnected(player: RpgPlayer) { console.log(`${player.name} disconnected`) // Save player data player.save() // Notify other players const map = player.getCurrentMap() if (map) { map.broadcastToPlayers('showText', [`${player.name} has left the game`]) } } } ``` ### onMove **Description:** Called when the player's x, y positions change **Parameters:** * `player: RpgPlayer` - The player instance **Example:** ```ts theme={null} const player: RpgPlayerHooks = { onMove(player: RpgPlayer) { // Check for hidden treasures const treasures = player.getCurrentMap().getEventsOfType('treasure') treasures.forEach(treasure => { if (treasure.isHidden && player.distanceTo(treasure) < 16) { treasure.reveal() player.showText('You found a hidden treasure!') } }) // Update step counter player.stepCount = (player.stepCount || 0) + 1 // Random encounters if (player.stepCount % 100 === 0) { if (Math.random() < 0.1) { // 10% chance player.callBattle('random-encounter') } } } } ``` ### onInShape / onOutShape **Description:** Called when a player enters or leaves a shape **Parameters:** * `player: RpgPlayer` - The player instance * `shape: RpgShape` - The shape instance **Example:** ```ts theme={null} const player: RpgPlayerHooks = { onInShape(player: RpgPlayer, shape: RpgShape) { if (shape.name === 'healing-zone') { player.addState('regeneration') player.showText('You feel rejuvenated...') } if (shape.name === 'danger-zone') { player.showText('⚠️ Danger Zone - Proceed with caution!') } }, onOutShape(player: RpgPlayer, shape: RpgShape) { if (shape.name === 'healing-zone') { player.removeState('regeneration') player.showText('The healing effect fades away.') } } } ``` ### canChangeMap **Description:** Determines if a player can change to a specific map **Parameters:** * `player: RpgPlayer` - The player instance * `nextMap: RpgMapChangeTarget` - A descriptor containing the destination map ID **Returns:** * `boolean | Promise` - Whether the player can change maps **Example:** ```ts theme={null} const player: RpgPlayerHooks = { async canChangeMap(player, nextMap) { // Check with external service const hasPermission = await checkMapPermission(player.id, nextMap.id) if (!hasPermission) { player.showText('Access denied.') return false } return true } } ``` ## Complete Example ```ts theme={null} import { RpgPlayer, RpgMap, RpgPlayerHooks, defineModule } from '@rpgjs/server' // Extend player interface declare module '@rpgjs/server' { export interface RpgPlayer { gold: number experience: number stepCount: number lastSaveTime: number } } const player: RpgPlayerHooks = { props: { gold: { $default: 100 }, experience: { $default: 0 }, stepCount: { $default: 0 }, lastSaveTime: { $default: 0 } }, onConnected(player: RpgPlayer) { console.log(`🎮 ${player.name} joined the game`) player.changeMap('town-square') player.showText(`Welcome back, ${player.name}!`) }, onJoinMap(player: RpgPlayer, map: RpgMap) { console.log(`📍 ${player.name} entered ${map.name}`) // Auto-save when entering important maps if (map.isImportant) { player.save('auto') player.lastSaveTime = Date.now() } }, onInput(player: RpgPlayer, { action }) { if (action === 'menu') { player.gui('inventory').open() } }, onLevelUp(player: RpgPlayer, nbLevel: number) { player.hp = player.param.maxHp player.sp = player.param.maxSp player.showAnimation('level-up') player.showText(`🎉 Level Up! You are now level ${player.level}`) }, onMove(player: RpgPlayer) { player.stepCount++ // Auto-save every 5 minutes const now = Date.now() if (now - player.lastSaveTime > 300000) { // 5 minutes player.save() player.lastSaveTime = now } }, onDisconnected(player: RpgPlayer) { console.log(`👋 ${player.name} left the game`) player.save() } } export default defineModule({ player }) ``` # Quick Start Source: https://v5.rpgjs.dev/index Create and run your first RPGJS project in a few minutes. # Quick Start Use the starter, install dependencies, and launch the dev server: ```bash theme={null} npx degit rpgjs/starter#v5 my-rpg-game cd my-rpg-game npm install npm run dev ``` Open `http://localhost:5173`. ## Choose MMORPG or standalone RPG By default, the starter runs as a standalone RPG. This mode runs the client and server together in the browser and does not create a public multiplayer server. To run the client and authoritative server separately for an MMORPG, set `RPG_TYPE=mmorpg`: ```bash theme={null} RPG_TYPE=mmorpg npm run dev ``` On Windows, use `cross-env`: ```bash theme={null} npm install --save-dev cross-env npx cross-env RPG_TYPE=mmorpg npm run dev ``` ## Next steps * [Getting Started](/guide/get-started) * [Structure](/guide/structure) * [Create your first map](/guide/create-your-first-map) * [Create hero in map](/guide/create-hero-in-map) * [Put an MMORPG online](/guide/deploy-mmorpg) * [API overview](/api) * [Studio overview](/studio/index) * [GitHub repository](https://github.com/RSamaium/RPG-JS) # Migrate from v4 to v5 Source: https://v5.rpgjs.dev/migration/v4-to-v5 Compatibility policy and migration plan for RPGJS v4 projects moving to v5. # Migrate from v4 to v5 This page is a stabilization draft. The compatibility matrix will be completed and verified against real v4 projects before the v5 stable release. RPGJS v5 keeps the core v4 product model: the same gameplay can run as a standalone RPG or an MMORPG, the server owns multiplayer gameplay state, maps act as rooms, and players, events, hooks, database content, and GUI commands remain the primary framework concepts. The main migration is architectural. V5 favors explicit entries, plain module definitions, providers, replaceable runtime adapters, and CanvasEngine game components over compiler-controlled autoload and decorators. ## Migration Paths ### Run the v4 project structure on the v5 runtime Use `compatibilityV4Plugin()` when the project should keep its v4 directory layout, `rpg.toml`, flagged imports, and Tiled asset conventions. See [V4 Compatibility](/guide/v4-compatibility) for the current setup and known limitations. This is the preferred first step for an existing game because it separates the runtime upgrade from the source migration. ### Migrate incrementally to native v5 modules After the game runs on the v5 runtime, migrate one module at a time: 1. keep server gameplay hooks and commands 2. replace decorated module configuration with `defineModule()` 3. register runtime integrations through `provideX()` providers 4. move visual components to the chosen v5 GUI or CanvasEngine integration 5. validate the module in standalone and MMORPG modes The compatibility layer and native v5 modules may coexist during this process. ### Replace Signe types leaked by early v5 betas Early v5 betas re-exported reactive and DI implementation types from RPGJS packages. Replace those imports before v5 stable: | Early beta import | Stable v5 replacement | | - | - | | `WritableSignal` or `Signal` from `@rpgjs/server` | `RpgWritableSignal` or `RpgReadableSignal` for gameplay property declarations | | `signal`, `computed`, or `effect` from `@rpgjs/server` | Framework-managed gameplay properties, or a direct `@signe/reactive` dependency in an advanced plugin | | `Context` from `@rpgjs/client` | `RpgContext` | | `NodeRoomStorage*` from `@rpgjs/server/node` | the corresponding `RpgRoomStorage*` contract | | Signe Cloudflare option types | `CreateRpgServerWorkerOptions` and `RpgServerWorkerEnv` | Plugins that deliberately use Signe internals should import and declare their `@signe/*` dependencies directly. See [Advanced Signe Extension Points](/advanced/signe-extension-points). ### Register providers through RPGJS contracts Import provider types from `@rpgjs/common`, including on the client and server. Factories may initialize asynchronously; the runtime waits for their result before exposing the provider: ```ts theme={null} import type { RpgContext, RpgFactoryProvider } from "@rpgjs/common" interface InventoryService { capacity: number } const inventoryProvider = { provide: "inventory", deps: ["inventory-config"], async useFactory(context: RpgContext): Promise { const config = context.get<{ capacity: number }>("inventory-config") await loadInventoryCatalog() return { capacity: config.capacity } }, } satisfies RpgFactoryProvider ``` Each object provider must choose exactly one creation strategy: `useValue`, `useClass`, `useFactory`, or `useExisting`. These contracts catch conflicting strategies during type checking instead of silently ignoring one. For synchronized gameplay property declarations, import `RpgReadableSignal` or `RpgWritableSignal` from `@rpgjs/common`. See [Synchronization](/guide/synchronization) for the gameplay-level API. Import `@signe/reactive` directly only when an advanced plugin intentionally needs Signe behavior that RPGJS does not expose. ### Use the stable Node storage names Node storage factories and their RPGJS-owned contracts come from the same entry: ```ts theme={null} import { createSqliteNodeRoomStorage, type RpgRoomStorageProvider, } from "@rpgjs/server/node" const storage: RpgRoomStorageProvider = createSqliteNodeRoomStorage({ databasePath: "./data/rooms.sqlite", }) ``` SQLite configuration requires exactly one source: `databasePath` for an RPGJS-owned connection or `database` for an existing compatible connection. See [Node server in production](/advanced/node-server-production) for the memory lifecycle and production persistence guidance. ## Compatibility Matrix | V4 capability | V5 target | Stable-release validation | | - | - | - | | `rpg.toml` and v4 module layout | Supported by `compatibilityV4Plugin()` | Build a real v4 starter | | `client!`, `server!`, `rpg!`, and `mmorpg!` imports | Translated by the Vite compatibility plugin | Inspect final client and server bundles | | Tiled maps, worlds, tilesets, and assets | Supported by `@rpgjs/tiledmap` | Run development and production builds | | Player, map, and event hooks | Preserved or mapped | Contract tests for arguments and ordering | | Decorated classes | Supported as a v4 compatibility path | Run representative v4 gameplay modules | | Shared and scenario events | Preserved | Multiplayer behavior tests | | Default and mobile GUI modules | Mapped to v5 behavior | Visual and interaction tests | | Vue GUI | Supported through `@rpgjs/vue` | Migrate a real Vue GUI | | Community plugins | Assessed individually | Publish supported/adaptable/unsupported status | | Save data | Migration contract to be defined | Load representative v4 saves in v5 | | Production MMORPG deployment | Runtime-adapter specific | Node and Cloudflare staging tests | ## Compatibility Categories Every v4 feature must be assigned one category before v5 stable: * **Compatible**: works without source changes * **Translated**: the compatibility layer adapts it automatically * **Migration required**: a documented, mechanical source change is required * **Deprecated**: still works in v5 but native v5 code should use another API * **Unsupported**: intentionally removed with a documented reason and alternative Unclassified behavior is a release blocker. ## Stable v5 Promise Once v5.0 is stable, documented v5 APIs will not be removed or behaviorally redefined in a minor release. V4 compatibility APIs that ship in v5.0 will remain available throughout the v5 major line unless they are explicitly identified as experimental before that release. The final migration guide will include tested before/after examples, plugin status, save-data guidance, and a troubleshooting section based on migrations of real projects. # Framework Philosophy Source: https://v5.rpgjs.dev/philosophy The durable product and architecture principles that guide RPGJS v5. # Framework Philosophy RPGJS is an RPG framework first. Its public model should be understood through players, maps, events, worlds, gameplay data, GUI, save/load, and multiplayer rooms rather than through its dependency injection, rendering, or transport implementation. This document defines the principles that RPGJS v5 intends to keep stable. It is a product contract for framework contributors and users, not a description of every current implementation detail. ## One Game, Two Runtime Modes The same gameplay model must work in both modes: * standalone RPG, where the gameplay server runs with the client * MMORPG, where the client connects to an authoritative remote server A feature is not complete until its behavior and ownership are defined in both modes. Standalone mode may remove the network boundary, but it must not require a different gameplay architecture. ## Server-Authoritative Gameplay The server owns gameplay state whenever multiplayer correctness matters. This includes player progression, inventory, combat results, map state, shared events, save data, and validation of client actions. The client renders, predicts, gathers input, and reacts to synchronized state. Client prediction must remain reconcilable with the authoritative result. A visual component must not silently become the owner of gameplay state. ## Stable RPG Vocabulary The most durable RPGJS APIs are its gameplay concepts: * players and their commands * maps and worlds * shared and scenario events * database entries such as items, skills, classes, and states * hooks for gameplay lifecycles * GUI commands such as dialogs and choices * save/load and synchronization contracts Infrastructure may evolve behind these concepts. A user should not need to rewrite gameplay because the renderer, transport, reactive engine, or hosting adapter changes. ## Maps Are Coordination Boundaries A map is the natural unit of world loading, event ownership, and multiplayer coordination. In MMORPG mode it normally corresponds to a synchronized room. Runtime adapters may distribute or host rooms differently, but must preserve documented map and player lifecycle behavior. ## Modules Are the Extension Unit Features that can stand on their own should be modules. A module should be installable, configurable, testable, and removable without changing the application shell or replacing an entire core subsystem. RPGJS should expose small extension points through hooks, providers, registries, resolvers, components, or named slots. The normal module authoring path must remain simpler than the dependency injection system that implements it. ## Official Defaults, Replaceable Infrastructure RPGJS provides official defaults without coupling gameplay to one deployment: * CanvasEngine is the default rendering and game-component environment. * Tiled is the default map authoring workflow. * Signe provides the current reactive synchronization and room foundations. * Node.js is the baseline server runtime. Storage, transport, hosting, map loading, and player-visible GUI remain replaceable through documented contracts. Platform-specific integrations such as Cloudflare belong in dedicated adapters rather than shared engine code. ## A Simple Default Path Explicit configuration is preferable to compiler magic, but explicit must not mean repetitive. A new user should have one recommended way to: * start an RPG or MMORPG * create and install a module * declare a map and event * register database content * display a GUI * add synchronized player state Advanced providers and runtime adapters are escape hatches. They must not be required knowledge for the basic RPG workflow. ## Compatibility and Deprecation RPGJS v5 follows these compatibility rules: * documented stable APIs remain compatible throughout the v5 major line * a removal requires a documented deprecation before the next major version * compatibility aliases must delegate to the same behavior as the preferred API * experimental APIs must be explicitly identified * changes to public third-party types are treated as RPGJS compatibility changes * save formats and multiplayer lifecycle behavior receive the same care as TypeScript signatures The v4 compatibility layer preserves the v4 project model where practical and must document what is supported, translated, deprecated, or intentionally not supported. Compatibility is verified with real projects, not only isolated compiler fixtures. ## Stability Standard Stable does not mean that RPGJS stops evolving. It means that users can depend on the concepts and public contracts learned in v5.0 while the implementation continues to improve. A new feature belongs in v5 only when it strengthens these principles without introducing a competing architecture. # Roadmap Source: https://v5.rpgjs.dev/roadmap Future RPGJS improvements under consideration. # Roadmap This page lists future improvements that are being considered. Items here are not stable APIs until they are implemented and documented in the relevant guide or API reference. ## Input Context For Actions `RpgClientEngine.processAction()` already supports custom action data: ```ts theme={null} client.processAction('projectile:shoot', { target: { x, y }, source: 'map-click' }) ``` RPGJS now exposes `client.pointer`, and keyboard actions from `character.ce` can send custom action data through `keyboardControls.action`. A remaining future improvement is to make other built-in input sources, such as map clicks and event clicks, provide a consistent action context without each feature wiring its own pointer state. The intended shape is: ```ts theme={null} client.processAction('projectile:shoot', { target: client.pointer.world(), source: 'keyboard' }) ``` Or through richer keyboard control configuration: ```ts theme={null} provideClientGlobalConfig({ keyboardControls: { action: { bind: 'space', action: 'projectile:shoot', data: (client, sprite) => ({ target: client.pointer.world(), source: 'keyboard', playerId: sprite.id }) } } }) ``` On the server, the player hook would continue to receive a normal action input: ```ts theme={null} const player = { onInput(player, input) { if (input.action === 'projectile:shoot') { const target = input.data?.target } } } ``` The main design constraint is that `character.ce` should stay generic. It can send custom action data only if RPGJS exposes a reliable client-side input context, such as a pointer world position helper, selected target helper, or action context resolver. Implemented: * Add an official `client.pointer` helper. * Let `keyboardControls.action` accept an object form with `bind`, `action`, and optional static or functional `data`. * Keep simple string controls compatible with the current behavior. Potential follow-up work: * Let map clicks emit action inputs with world coordinates. * Let event clicks emit action inputs with `eventId` and world coordinates. # Use a game with Studio Source: https://v5.rpgjs.dev/studio/game-integration Connect an RPGJS game to RPGJS Studio data with provideStudioGame. # Use a game with Studio Use `provideStudioGame` when the current RPGJS game should load its maps, database, media references, and player start configuration from RPGJS Studio data. ## Install the package ```bash theme={null} npm install @rpgjs/studio ``` ## Studio mode In Studio mode, the game reads data from RPGJS Studio. Add `provideStudioGame` to both the client and server configurations and pass the Studio project identifier. Client configuration: ```ts theme={null} // src/config/config.client.ts import { provideStudioGame } from "@rpgjs/studio/client"; export const configClient = { providers: [ provideStudioGame({ projectId: "your-project-id", }), ], }; ``` Server configuration: ```ts theme={null} // src/config/config.server.ts import { provideStudioGame } from "@rpgjs/studio/server"; export const configServer = { providers: [ provideStudioGame({ projectId: "your-project-id", }), ], }; ``` When `projectId` is set, the runtime uses online Studio data by default. ## Generated characters A spritesheet media record with `metadata.generationMode: "idle"` uses its `metadata.columns` and `metadata.rows` (normally 2×2) and `metadata.idleDirections` (normally down, left, right, up) for stationary poses. The runtime reads media linked through `metadata.groupId` and uses an animation named `walk` while the character moves. Other named animations are available as actions. The idle cell is fitted into a 128-pixel game frame before applying its saved `metadata.scale`. Each animation uses its own frame grid; the idle pose remains fixed when the character stops. Online games load these links from `GET /api/game/media/:mediaId/animations`; offline bundles use their media index. Existing spritesheets without `generationMode: "idle"` retain their previous frame layout. The client awaits the idle and linked animation images before making the spritesheet available, including when the actor appearance contains a complete media object. Switching between movement and idle restores each animation's scale explicitly, so the movement correction does not carry over into idle. After preloading, the client calibrates each animation direction against the corresponding idle pose. Its first frame determines the visible height, horizontal center and ground position (alpha greater than 16). The transform stays fixed for the entire timeline: later effects and pose changes do not rescale the character. Transparent padding is removed from the ground anchor without changing the hitbox. The base character's scale then controls the whole group. Calibration uses visible artwork, not semantic body detection: effects already present in the reference frame and differently drawn poses can still affect the result. Saved animation scales are used as a fallback when browser pixel access is unavailable. This also applies when combat plays a linked media as a separate graphic: its `metadata.groupId` identifies the idle parent whose reference size and global scale must be used. ## Studio hotbar settings `createStudioActionBattlePreset()` connects the Action Battle hotbar to the Studio project and map menu settings: ```ts theme={null} import { provideActionBattle } from "@rpgjs/action-battle/server" import { createStudioActionBattlePreset } from "@rpgjs/studio/server" provideActionBattle({ ...createStudioActionBattlePreset(), }) ``` Studio persists the project default under `menus.hotbar`: ```json theme={null} { "menus": { "hotbar": { "enabled": true, "guiId": null, "settings": { "content": "mixed", "slotCount": 8 } } } } ``` `guiId: null` selects the native RPGJS GUI. Inside `settings`, `content` is `skills`, `items`, or `mixed`, and `slotCount` is between 1 and 10. A map can persist the same complete binding to override the project. A map without `menus.hotbar` inherits the project configuration. The preset opens or closes the hotbar on map changes, filters its allowed entries, and preserves temporarily hidden player assignments. ## MMORPG mode Studio MMORPG maps use an authoritative, chunked data path: * a trusted publisher loads and normalizes the complete Studio map, project, database, events, and collisions; * the Node server or Cloudflare Durable Object stores that authoritative payload, runs physics and events, and decides which chunks surround each player; * the browser receives only the nearby render descriptors and collision barriers required for display and client prediction. Terrain transition control masks follow the same rule: the publisher splits them into overlapping regions and the server sends only the regions belonging to disclosed chunks. The client rebuilds the visible transition masks without receiving the complete control texture or the complete map. The complete map is never sent to the browser. Studio events, trigger logic, database records, project configuration, raw terrain structure, and collisions outside the streamed area remain server-side. Public image and audio assets are still downloaded by the browser because it needs them for rendering and playback. Authoritative streaming requires Studio map format v2. A v1 Studio map continues to work in standalone mode, but publication to an MMORPG map room fails explicitly instead of silently exposing or approximating its data. ### Share one server between Studio projects Use a connection-scoped startup resolver when several Studio projects share the same MMORPG Worker. The browser forwards the requested project and optional map, while the server makes the authoritative startup decision: ```ts theme={null} // src/config/config.client.ts const params = new URLSearchParams(window.location.search) const projectId = params.get("game") ?? "" const mapId = params.get("map") provideMmorpg({ room: `lobby-${projectId}`, query: { game: projectId, ...(mapId ? { map: mapId } : {}), }, }) provideStudioGame() ``` ```ts theme={null} // src/config/config.server.ts provideStudioGame({ resolveStartup: ({ query, connection }) => { // connection.state may contain trusted context attached by auth(). const projectId = query.game return query.map ? { projectId, flow: "direct", mapId: query.map } : { projectId, flow: "title" } }, }) ``` `flow: "title"` keeps the Studio-owned title screen, character selection, actor application, default stats, and start-map transfer. `flow: "direct"` skips the interactive screens and transfers to `mapId` after verifying that the map belongs to `projectId`. The resolver receives `{ player, query, headers, connection }`, runs after the RPGJS `connected` packet, and is evaluated only once for that lobby connection. Its result is reused when the player presses Start. Missing projects and maps from another project fail explicitly; they never fall back to global Studio configuration. The `game` URL parameter also lets the Studio client load the corresponding title-screen configuration. When `map` is present, the client does not render the title screen while the server validates and performs the direct transfer. Configure the disclosure window on the shared Studio module: ```ts theme={null} provideStudioGame({ projectId: "your-project-id", streaming: { chunkSize: 16, loadRadius: 2, retainRadius: 3, }, }); ``` `chunkSize` is expressed in Studio cells. `loadRadius` controls the chunks sent around the authoritative player position, while `retainRadius` keeps a slightly larger client cache to avoid loading churn at chunk boundaries. NPCs, events, players, and projectiles continue to use the generic RPGJS spatial synchronization path and are disclosed according to server interest management. Client prediction remains enabled with Studio. Movement is predicted against the collision barriers already disclosed for the active chunks, blocked at the edge of the streamed window, then reconciled with authoritative Node or Durable Object snapshots. Event behavior, NPC decisions, projectile impacts, and every collision outside that window remain server-authoritative. ### Live map updates from Studio Players must never publish map definitions. Studio, Vite, CI, an editor backend, or another trusted process sends the full payload to the map room: ```http theme={null} POST /parties/main/map-/map/update Content-Type: application/json X-RPGJS-Map-Update-Token: ``` `Authorization: Bearer ` is also accepted. Configure `RPGJS_MAP_UPDATE_TOKEN` only on the Node server or Worker. Never put it in browser code or in a `VITE_` environment variable. Map content and world topology are separate authoritative updates. After publishing a prepared map, the trusted publisher sends the current topology to every map room in that world: ```http theme={null} POST /parties/main/map-/world//update Content-Type: application/json X-RPGJS-Map-Update-Token: { "id": "", "maps": [/* complete runtime topology */] } ``` This fan-out matters because each map is a separate Node room or Durable Object with its own world manager. Updating only the room for `marsh`, for example, does not update a player who is still connected to `port`. The easiest development publisher is the RPGJS Vite plugin: ```ts theme={null} import { createStudioMapUpdatePayload } from "@rpgjs/studio/server"; rpgjs({ server: ServerModule, devServer: { target: "http://127.0.0.1:8787", mapIds: ["your-map-id"], mapUpdateToken: process.env.RPGJS_MAP_UPDATE_TOKEN, resolveMapPayload: ({ mapId }) => createStudioMapUpdatePayload(mapId, { projectId: "your-project-id", startMapId: "your-map-id", }), }, }); ``` Vite republishes the resolved map after relevant development changes. The same callback works with a local Node room provider or a Wrangler Durable Object. `createStudioMapUpdatePayload()` supplies `worldUpdates`, and the RPGJS remote publisher automatically sends them to every referenced map room. To test the HTTP contract directly, send a previously prepared Studio v2 payload: ```bash theme={null} curl --fail-with-body \ -X POST \ -H 'content-type: application/json' \ -H 'x-rpgjs-map-update-token: local-map-update-token' \ --data-binary @prepared-studio-map.json \ http://127.0.0.1:8787/parties/main/map-/map/update ``` The payload must be complete because `/map/update` replaces the authoritative map revision. Use `createStudioMapUpdatePayload()` rather than assembling production payloads by hand. Its result includes the normalized v2 render data, dimensions, server collisions, events, project configuration, and database records needed by the room. This direct `curl` updates only one map revision; it does not perform the world fan-out. Use the RPGJS publisher or the Studio seed command for a full map-and-world publication. ### Trusted publisher data provider A trusted backend that already owns the Studio project data can inject a `GameDataProvider` and avoid calling the public Studio API while preparing an MMORPG map update: ```ts theme={null} import { createStudioMapUpdatePayload, type GameDataProvider, } from "@rpgjs/studio/server"; const databaseBackedStudioProvider: GameDataProvider = { kind: "online", getProject: async ({ projectId, mapId }) => loadProject({ projectId, mapId }), getMap: async (mapId) => loadMap(mapId), getMedia: async (mediaId) => loadMedia(mediaId), getDatabase: async (projectId) => loadDatabase(projectId), }; await createStudioMapUpdatePayload("your-map-id", { projectId: "your-project-id", startMapId: "your-map-id", dataProvider: databaseBackedStudioProvider, }); ``` The injected provider handles every project, map, event-media, and database read for that payload. It is server-owned in both standalone and MMORPG deployments: never expose database adapters, private storage handles, or credentials to browser code. When `dataProvider` is omitted, the existing online, offline, and auto runtime modes keep selecting the built-in provider. For a runnable local Worker, deterministic fixture, seed script, and real Studio API seed command, see the [Studio playground](https://github.com/RSamaium/RPG-JS/tree/master/playground/games/studio). ## Offline mode Offline mode lets the game run from exported Studio data without calling the Studio API. Export the project data from Studio into the game public directory, using the default bundle path: ```text theme={null} public/ game-data/ project.json database.json events.json maps/ .json media/ media-index.json ``` Then configure `provideStudioGame` without `projectId`, or force `runtimeMode` to `"offline"`: ```ts theme={null} provideStudioGame({ runtimeMode: "offline", }); ``` By default, offline data is loaded from `/game-data`. Use `bundleBasePath` only if the exported folder is served from another path: ```ts theme={null} provideStudioGame({ runtimeMode: "offline", bundleBasePath: "/my-game-data", }); ``` Offline database records are normalized before they are registered in RPGJS. Studio skill records are available to RPGJS skills automatically with their `spCost`, `hitRate`, `power`, and `coefficient` fields, and enemies can learn skills referenced by their `skills` array. The project can define hero skill progression with `skills` or `skillsToLearn`. At runtime, `provideStudioGame()` creates a default RPGJS class containing those entries, then RPGJS learns each skill when the configured level is reached: ```json theme={null} { "skills": [ { "level": 1, "skillId": "slash-id" }, { "level": 5, "skillId": "fire-id" } ] } ``` Studio listens to the RPGJS `player.onSkillChange` hook and displays a notification when the hero learns or forgets a skill. Enemy records can also drive action-battle AI. Use `behavior` on the enemy to set fields such as `enemyType`, `behaviorKey`, `visionRange`, `attackRange`, `attackCooldown`, `dodgeChance`, `dodgeCooldown`, `fleeThreshold`, `attackPatterns`, `patrolWaypoints`, `groupBehavior`, or the nested behavior gauge options. The older `aiBehavior` field is still accepted as a compatibility alias. Every learned enemy skill is evaluated from its action mode, targeting range, area mask, SP cost, and cooldown. `attackSkillId` gives one skill priority without disabling normal attacks or the other learned skills. ### Skill workflow triggers Studio skills may declare `workflowTriggers` that reference Studio block collections: ```json theme={null} { "workflowTriggers": [ { "phase": "cast", "blockCollectionId": "cast-workflow" }, { "phase": "impact", "blockCollectionId": "impact-workflow" }, { "phase": "defeat", "blockCollectionId": "defeat-workflow" } ] } ``` This is a Studio orchestration feature built on RPGJS's native skill `onUse` hook. It does not add a second engine hook. Studio preserves the default skill effect, waits for projectile impact when applicable, then executes the blocks from the referenced collection. The block context exposes the caster as the player and the affected map event as the current event when one exists. A skill workflow can call or spawn Common Events through the corresponding blocks. ### Item workflow triggers Studio item records expose fields and workflow phases according to their item type: * regular items expose `hpValue`, `mpValue`, `hitRate`, `consumable`, `onAdd`, `onUse`, `onUseFailed`, and `onRemove`; * weapons and armors expose their equipment statistics, parameter modifiers, `onAdd`, `onRemove`, and `onEquip`; * weapons and armors do not expose `consumable`, `onUse`, or `onUseFailed`. `hitRate` is edited as a percentage from 0 to 100 and normalized to the native RPGJS `hitRate` value from 0 to 1. An equipment `onEquip` workflow can test `variables.equip`: it is `true` after equipping and `false` after unequipping. ```json theme={null} { "itemType": "weapon", "atk": 12, "workflowTriggers": [ { "phase": "onAdd", "blockCollectionId": "sword-found" }, { "phase": "onEquip", "blockCollectionId": "sword-equipped" } ] } ``` The runtime maps these workflows to the native item hooks. Workflow blocks run in order for each player, can call Common Events, and keep the normal RPGJS item or equipment behavior. The same Studio enemy definition can be placed on a map more than once. The runtime keeps the first placement's legacy id and assigns deterministic ids such as `enemy-id::2` to later placements, while preserving `sourceEventId` for database lookups. Each placement therefore gets its own sprite, hitbox, HP, Battle AI state, and defeat lifecycle. ## Built-in GUI settings Studio projects can bind the native Title Screen, Hotbar, HUD, and Main Menu roles. A `null` `guiId` selects the built-in RPGJS component and leaves room for a future Studio GUI definition: ```json theme={null} { "menus": { "titleScreen": { "enabled": true, "guiId": null }, "hotbar": { "enabled": true, "guiId": null, "settings": { "content": "mixed", "slotCount": 8 } }, "hud": { "enabled": true, "guiId": null }, "mainMenu": { "enabled": true, "guiId": null } }, "keyboardControls": { "back": "escape" } } ``` The client applies Title Screen and HUD visibility. The server starts directly when the project disables the Title Screen, controls Main Menu availability, and remains authoritative for Hotbar state. The configured Back key and the mobile Back touch button produce the same logical action. Map settings do not override the Hotbar. Studio workflows and events use the `set_hotbar` block to display it with `skills`, `items`, or `mixed` content and 1 to 10 slots, or to hide it without clearing persistent assignments. ## Auto mode Use `"auto"` when the game should try the exported bundle first, then fall back to Studio if local data is missing: ```ts theme={null} provideStudioGame({ projectId: "your-project-id", runtimeMode: "auto", }); ``` ## Start without a title screen Set `displayTitleScreen: false` to enter the starting map as soon as the server accepts the connection, in both standalone RPG and MMORPG modes: ```ts theme={null} provideStudioGame({ projectId: "your-project-id", startMapId: "requested-map", displayTitleScreen: false, }); ``` The server initializes the built-in default player stats, then transfers the player to `startMapId` or to the starting map defined by the Studio project. An explicit `displayTitleScreen: false` enables startup even if the project metadata cannot be loaded or its title screen is enabled. Provide `startMapId` when startup must work without project metadata. Character selection is still honored when project data is available, unless `skipCharacterSelect` is true. When `displayTitleScreen` is omitted, the project setting controls the title flow. `autoStart: true` remains an explicit immediate-start override. ## Options * `projectId`: Studio project identifier. When provided, the default runtime mode is `"online"`. * `runtimeMode`: data loading strategy. Use `"online"`, `"offline"`, or `"auto"`. * `bundleBasePath`: public path for exported Studio data. Defaults to `/game-data`. * `displayTitleScreen`: `false` skips the title screen and starts immediately; `true` retains the title flow unless `autoStart: true` overrides it. * `autoStart`: initialize the player and enter the starting map immediately on connection. Defaults to `false`. Studio projects can instead persist `menus.titleScreen.enabled: false`; the Studio runtime then enables immediate startup automatically. Explicit `autoStart` remains useful for non-Studio configuration and overrides. * `startMapId`: force the map used to start the player. * `skipCharacterSelect`: skip actor selection during an automatic direct-map startup. * `resolveStartup`: resolve a player-specific `title` or `direct` startup after MMORPG authentication. See [Share one server between Studio projects](#share-one-server-between-studio-projects). * `streaming`: authoritative Studio v2 chunk settings for MMORPG mode. Set it to `false` only when another server map provider replaces the built-in streaming adapter. Standalone mode always uses the direct loader. Its options are `chunkSize`, `loadRadius`, and `retainRadius`. * `debugCollisions`: display Studio collision debug overlays. This is a shortcut for the built-in Studio debug plugin. * `studioPlugins`: attach Studio client-side map renderer plugins. See [Create a Studio plugin](/studio/plugins). ### Terrain collision contours In standalone RPG and MMORPG games, Studio hole and water borders are generated from the final painted surface. Paint strokes are combined and erase operations are applied in their recorded order. Overlapping strokes do not create internal walls; erased paths and islands retain their own boundaries. Always-low elements such as bridges clear only the portion of a border they cover. Contours are sampled at two-pixel resolution for ordinary maps and simplified within three pixels. Very large features use a coarser grid to bound temporary memory. Physics uses small convex pieces along these contours, rather than a single filled polygon that would block islands or other walkable interiors. The same geometry generator is used by server-authoritative collisions, client prediction, and collision debugging; enabling debug collisions does not alter movement rules. ### Liquid contacts and submerged scenery Liquid borders derive their dominant, light and dark colors from the opaque pixels of the selected texture's atlas region. This works with water, lava, mud and custom materials without classifying their names. Existing `border` and `foam` settings remain supported: disabling foam keeps a subtle dark contact; disabling the border removes contact accents. If pixels cannot be read, `fillColor` provides the palette; without either, no colored accent is added. Static scenery placements can opt into visual immersion in map data: ```json theme={null} { "id": 3, "tilesetId": "rocks", "x": 384, "y": 240, "submersion": { "depth": 0.2 }, "extractGroundShadow": true } ``` `depth` is the proportion of the element's rendered height that may be immersed, from `0` to `1`. Omit it or use zero to disable the effect. Only pixels that also intersect liquid are tinted and attenuated. Filled holes use their projected surface at the current fill level, including erased areas. Terrain layers marked with the existing `water` render mode also support immersion. Contact follows sprite alpha, after `extractGroundShadow` separates an embedded shadow when enabled; transparent corners never acquire rectangular foam. Scaled elements and draw-rule segments retain their position and display order. The placement option survives direct map loading and authoritative chunk streaming. It is a client rendering effect in both standalone RPG and MMORPG games: it changes neither collisions nor position nor server authority. There is no new external Studio editor control in this contribution; set the property in map data. Palettes and liquid masks are cached, and an element's composed pixels are reused until its artwork, placement, depth or terrain revision changes. Stream updates invalidate affected rendered state; discarded elements release their cached pixels. No sprite-pixel reads or contour reconstruction run in the animation tick. ### Enemy attack timing overrides An enemy's `behavior.attackProfiles` can override `melee`, `combo`, `charged`, `zone`, and `dashAttack`. Each profile accepts millisecond values: ```json theme={null} { "behavior": { "attackCooldown": 900, "attackProfiles": { "melee": { "startupMs": 250, "activeMs": 120, "recoveryMs": 350, "cooldownMs": 1100 } } } } ``` Leave fields absent to inherit runtime defaults. Preparation, recovery and cooldown accept zero; active duration must be at least 1 ms. Invalid values are ignored. The global attack cooldown also applies. The legacy `aiBehavior` object supports the same fields. Studio exposes these overrides in the enemy AI settings; the runtime applies them to both standalone RPG and MMORPG enemies. # Studio overview Source: https://v5.rpgjs.dev/studio/index Use RPGJS Studio with AI assistants such as Claude Code, Codex, or similar tools. # Studio overview RPGJS Studio can be used directly from an AI coding assistant such as Claude Code, Codex, or any similar tool that supports skills. ## In this section * What the Studio skill is for * How to install the RPGJS Studio skill * How to create and configure the Studio API key * How to connect an RPGJS game to Studio data * How to configure Studio event page options * How to extend the Studio map renderer with plugins * How to render Studio terrain through a framework-independent RGBA API ## Recommended workflow 1. Install the shared skill: ```bash theme={null} npx skills add https://github.com/RSamaium/RPG-JS#v5 ``` 2. When the tool asks which skill to install, choose `RPGJS Studio`. 3. Create an API key from [RPGJS Studio API keys](https://rpgjs.studio/api-keys). 4. Add the key to your environment with `RPGSTUDIO_API_KEY`. Example: ```bash theme={null} export RPGSTUDIO_API_KEY="your-api-key" ``` Or in a `.env` file: ```dotenv theme={null} RPGSTUDIO_API_KEY=your-api-key ``` ## Next step * [Use a game with Studio](/studio/game-integration) * [Studio event page options](/studio/event-page-options) * [Render embedded ground shadows](/studio/ground-shadows) * [Create a Studio plugin](/studio/plugins) * [Low-level 2D terrain rendering](/studio/render-map2d) # Using TiledMap with RPG-JS Source: https://v5.rpgjs.dev/tiled/index Guide for Using TiledMap with RPG-JS in RPGJS. # Using TiledMap with RPG-JS TiledMap integration allows you to use maps created with the [Tiled Map Editor](https://www.mapeditor.org/) in your RPG-JS games. This provides a visual way to design your game maps with layers, tilesets, collision detection, and interactive objects. Use `@rpgjs/tiledmap` when you want to build maps with Tiled Map Editor. If you want your own renderer or your own map format, use [Custom map rendering with provideLoadMap](/advanced/provide-load-map). ## Installation First, install the TiledMap package: ```bash theme={null} npm install @rpgjs/tiledmap ``` ## Runtime ownership Tiled behaves differently according to the game mode: * in standalone RPG mode, the browser loads the complete TMX and TSX files * in MMORPG mode, the server loads the complete files and owns collisions, objects, events and properties; the browser receives only nearby render chunks and their static hitboxes for prediction The MMORPG client never needs the raw TMX/TSX source. Keep secrets and gameplay configuration in server modules rather than Tiled properties that must be rendered. ## Vite Configuration Configure your `vite.config.ts` to handle Tiled map files: ```ts theme={null} import { defineConfig } from 'vite'; import { tiledMapFolderPlugin } from '@rpgjs/vite'; export default defineConfig({ plugins: [ tiledMapFolderPlugin({ sourceFolder: './src/tiled', // Folder containing your TMX files publicPath: '/map', // Public URL path for maps buildOutputPath: 'assets/data', // Build output directory // MMORPG: publish images only. The server/editor reads TMX and TSX privately. allowedExtensions: ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'] }) ] }); ``` ### Plugin Options * **`sourceFolder`**: Directory containing your TMX files, TSX tilesets, and images * **`publicPath`**: URL path prefix for accessing map files (default: `/data`) * **`buildOutputPath`**: Target folder in build output (default: `assets/data`) * **`allowedExtensions`**: File extensions to include (default: `['.tmx', '.tsx', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg']`) Keep the default extensions for a standalone browser build. For an MMORPG, use the image-only list above so neither development middleware nor `dist/client` exposes TMX/TSX files. ## Client-Side Setup Configure the client to use TiledMap: ```ts theme={null} import { provideClientGlobalConfig, provideRpg, startGame } from "@rpgjs/client"; import { provideTiledMap } from "@rpgjs/tiledmap/client"; import startServer from "./server"; startGame({ providers: [ provideTiledMap({ basePath: "map" // Must match publicPath in vite.config.ts }), provideClientGlobalConfig(), provideRpg(startServer), // ... other client providers ], }); ``` ## Server-Side Setup Configure the server to use TiledMap: ```ts theme={null} import { createServer, provideServerModules } from "@rpgjs/server"; import { provideTiledMap } from "@rpgjs/tiledmap/server"; export default createServer({ providers: [ ...provideTiledMap({ basePath: "/map", // Same image URL prefix as the client streaming: { chunkSize: 16, // Tiled cells per chunk loadRadius: 2, // Chunks sent around the authoritative position retainRadius: 3 // Chunks kept to avoid boundary churn } }), provideServerModules([ { maps: [ { id: "mymap", // Map ID (should match TMX filename) // ... other map configuration } ] } ]) ] }); ``` The server module is transport-neutral. The same configuration works inside the Node.js room transport and inside a Cloudflare Durable Object. One map room owns one authoritative map instance; Wrangler local development exercises that same Durable Object path. ### Custom, Studio, and other map formats Chunk streaming is not tied to Tiled. A map package can pair `provideServerMapStreaming()` with `provideClientMapStreaming()`: * the server compiler converts private source data into a public manifest and chunks * every chunk supplies renderer data plus static collision geometry * the client adapter incrementally applies and removes renderer data Studio or a custom map system can therefore keep its complete document private and choose its own chunk representation. Tiled currently supplies the built-in adapter; other formats install their own pair of adapters. ## File Structure Organize your Tiled files in the configured source folder: ``` src/ ├── tiled/ │ ├── mymap.tmx # Your Tiled map file │ ├── tileset.tsx # Tileset definition │ ├── tiles.png # Tileset image │ └── objects.png # Object sprites └── ... ``` ## Features ### Automatic Collision Detection TiledMap automatically detects collision tiles and applies tile rules to physics: * Set the `collision` property to `true` on tiles in Tiled Map Editor * Collision rules are attached to entities through physics extension hooks * The server always uses the complete collision map * In MMORPG mode, the client predicts only with hitboxes from disclosed chunks * No additional code required for basic tile blocking A thin temporary boundary is added around the disclosed area, preventing prediction from moving into a chunk whose physics has not arrived yet. Server reconciliation remains authoritative. ### Collision Levels (`z`) Tile collisions depend on height, as in RPGJS v4. The level of a tile is the sum of its layer `z` property and its tile `z` property (both default to `0`). A colliding tile on level `n` only blocks characters whose `z` is in `[n * zTileHeight, (n + 1) * zTileHeight)`. `map.zTileHeight` is the tile height by default. `player.z()` is measured in pixels, not in levels. With 32px tiles, the player must be raised to at least `32` to walk over collision tiles on level `0`: ```ts theme={null} // Server side: walk over level 0 collisions (e.g. water) player.z.set(map.zTileHeight) // Back on the ground player.z.set(0) ``` The server uses this rule for authoritative collisions, and the client applies the same rule for prediction in both standalone RPG and MMORPG modes. Custom static hitboxes accept the same optional range through `z` and `zHeight` (in pixels). A hitbox without `z` blocks characters at every height. This is the case for map borders and for Studio hitboxes. ### Event Integration Place events in Tiled using point objects: 1. Create an Object Layer in Tiled 2. Add Point objects with names matching your event names 3. Events will be automatically positioned based on object coordinates ```ts theme={null} // In your server configuration { maps: [ { id: "mymap", events: [MyEvent()] // Event will be positioned from Tiled object } ] } ``` ### Map API Compatibility When the server loads a Tiled map, `@rpgjs/tiledmap` attaches the CanvasEngine Tiled instance to `map.tiled` and exposes v4-compatible helpers on the map: ```ts theme={null} const layer = map.getLayerByName("Ground"); const index = map.getTileIndex(4, 2); const origin = map.getTileOriginPosition(4, 2); const tile = map.getTileByPosition(128, 64); map.setTile(4, 2, "Ground", { gid: 12 }); map.updateTileset({ firstgid: 1, name: "base" }); ``` Available properties and methods: * `map.layers` * `map.zTileHeight` * `map.getLayerByName(name)` * `map.getTileIndex(x, y)` * `map.getTileOriginPosition(x, y)` * `map.getTileByPosition(x, y, z?)` * `map.getTileByIndex(tileIndex)` * `map.setTile(x, y, layer, tileInfo)` * `map.updateTileset(tileset)` These helpers are available only for maps prepared by `@rpgjs/tiledmap`. For new code, prefer using `map.tiled` directly when you need CanvasEngine-specific behavior. ## Physics Hooks Integration The tiled module now uses shared physics hooks: * Server: `map.onPhysicsInit`, `map.onPhysicsEntityAdd`, `map.onPhysicsEntityRemove`, `map.onPhysicsReset` * Client: `sceneMap.onPhysicsInit`, `sceneMap.onPhysicsEntityAdd`, `sceneMap.onPhysicsEntityRemove`, `sceneMap.onPhysicsReset` This gives consistent tile collision behavior for both client prediction and server validation. NPCs, players, events, and projectiles are synchronized by the same interest window: a complete visual snapshot is sent on entry and removed on exit, while collisions and impacts are still decided by the server.