npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rpgjs-solo-renderer

v5.0.0-beta.29.solo.2

Published

Native CanvasEngine renderer and Tiled authoring surface for RPGJS Solo

Readme

rpgjs-solo-renderer

The renderer accepts a game-owned CanvasEngine compatibility installer and runs it before scene creation. Applications use the public rpgjs-patches package to centralize lifecycle workarounds. Each active map also owns an isolated, keyed viewport and TiledMap element so stale async tileset loads or retiring camera directives cannot overwrite a newer destination.

The native rendering and authoring layer for RPGJS Solo. It keeps the RPGJS Tiled workflow and composes CanvasEngine's scene graph, camera, spritesheets, and fog-of-war directly around SoloRuntime state.

loadSoloTiledMap derives center-based Solo physics obstacles from collision tiles on every authored layer. It supports both the collision: true tile property and RPGJS/Tiled tileset object groups, preserving rectangular or rotated polygon bounds and thin polylines instead of silently treating the decorative ground layer as the only collision authority. Contiguous full-tile collision is coalesced into larger rectangles before physics registration, while partial and polygonal shapes remain independent.

There is no socket, room, sync, prediction, reconciliation, or game-facing Pixi adapter. Pixi remains an implementation detail of CanvasEngine.

CanvasEngine, its Tiled/preset packages, Pixi, Vite, TypeScript, Vitest, and declaration tooling are pinned to versions checked as current for each release; renderer feature-completeness includes that alignment gate. Release this workspace package with pnpm publish, which rewrites its workspace: core dependency to the exact published Solo version. The shared Solo publish guard rejects npm publish, and the workspace-wide packed-manifest check prevents registry consumers from receiving an unresolved workspace protocol.

renderer.uiRoot is a framework-neutral DOM overlay. Use the inherited, current @rpgjs/ui-css primitives there directly or mount the game's chosen UI framework into it; the renderer does not invent a second widget system. Keyboard input respects UI ownership: default-prevented events and keys whose composed path is inside a button, form control, dialog, editable region, or data-solo-input-owner surface do not leak into movement, interaction, or pause.

import { SoloRuntime } from 'rpgjs-solo'
import { installCanvasEnginePatches } from 'rpgjs-patches'
import {
  SoloRenderer,
  createSoloTileObstacles,
  createRpgMakerSpritesheet,
  loadSoloTiledMap,
  replaceSoloTiledLayers
} from 'rpgjs-solo-renderer'

const runtime = new SoloRuntime()
const field = await loadSoloTiledMap({ id: 'field', basePath: '/maps' })
runtime.registerMap(field.runtime)
runtime.spawnEntity({ id: 'hero', kind: 'player', mapId: 'field', x: 96, y: 96 })

const renderer = new SoloRenderer({
  runtime,
  target: document.querySelector('#rpg')!,
  playerId: 'hero',
  maps: [field],
  installCanvasEnginePatches,
  appearances: {
    hero: {
      spritesheet: createRpgMakerSpritesheet('/sprites/hero.png', 3, 4),
      animation: (entity) => typeof entity.data.animation === 'string'
        ? entity.data.animation
        : entity.moving ? 'walk' : 'stand'
    }
  },
  camera: {
    zoom: ({ width }) => width >= 1200 ? 2.5 : width >= 720 ? 2 : 1.5
  },
  fog: { radius: 160 },
  audio: { autoMuteInTests: true }
})

await renderer.start()

The public installer is supplied by the application through the typed injection interface. Install rpgjs-patches@^0.4.0 from npmjs and inject it as above. Applications may omit the hook when no compatibility layer is required.

The validated patch matrix is exported as rpgjsSoloRendererCompatibility. This source cohort requires [email protected], Vite 8.3.3, and consumer-injected rpgjs-patches@^0.4.0. The installer must run before scene creation; SoloRenderer guarantees that order when it receives installCanvasEnginePatches in its options.

import { rpgjsSoloRendererCompatibility } from 'rpgjs-solo-renderer'

console.log(
  `${rpgjsSoloRendererCompatibility.patches.package}@${rpgjsSoloRendererCompatibility.patches.version}`
)

Story-driven terrain remains a game-owned decision while the renderer owns its safe scene lifecycle. Replace complete GID layers immutably, then register the revision; the active viewport is rebuilt under a new revision key without changing the authoritative Solo map or its entities:

const openedField = replaceSoloTiledLayers(field, [
  { name: 'ground', data: openedGroundGids },
  { name: 'terrain', data: openedTerrainGids }
])
renderer.registerMap(openedField)

The helper validates unique layer names, dimensions, and unsigned Tiled GIDs. It never infers physics from visuals. If the story change also affects collision, update the runtime obstacle table from the same game-authored state as shown below.

Games with gates, bridges, or other story-driven traversal changes can derive the replacement obstacle table from the same coalescing path as the TMX loader:

runtime.replaceMapObstacles('field', createSoloTileObstacles({
  id: 'field',
  width: authoredMap.width,
  height: authoredMap.height,
  tileWidth: authoredMap.tileSize,
  tileHeight: authoredMap.tileSize,
  cells: authoredMap.collision
}))

camera.zoom may be a fixed positive scale or a resolver over the live canvas size. Responsive resolvers run again after resizes and orientation changes; the renderer delegates to pixi-viewport's centered setZoom operation and keeps the keyed map viewport and player-follow lifecycle intact.

appearance.animation may also be a fixed animation name. Resolver functions read the authoritative entity state on each runtime tick, so game packages can map their own attack, hurt, down, interaction, or other states onto a spritesheet without adding those rules to Solo's generic renderer.

For occlusion-aware and persistable exploration, supply a game-owned visibility snapshot. The renderer samples it in tile coordinates and owns only the display surface; line-of-sight rules and save migrations remain in the game:

fog: {
  smooth: false,
  visibility: () => ({
    mapId: activeMap.id,
    width: activeMap.width,
    height: activeMap.height,
    tileSize: 16,
    revision: visibility.revision,
    stateAt: (x, y) => visibility.visible.has(y * activeMap.width + x)
      ? 'visible'
      : visibility.discovered.has(y * activeMap.width + x)
        ? 'explored'
        : 'unknown'
  })
}