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

v5.0.0-beta.29.solo.2

Published

Transport-free, deterministic single-player RPG runtime built from RPGJS v5

Readme

rpgjs-solo

The transport-free, single-process runtime from RPGJS Solo. It is intentionally prerelease while RPGJS v5 and the Solo compatibility surface stabilize.

import { SoloRuntime } from 'rpgjs-solo'

const runtime = new SoloRuntime()
runtime.registerMap({ id: 'village', width: 1280, height: 720 })
runtime.spawnEntity({
  id: 'hero',
  kind: 'player',
  mapId: 'village',
  x: 64,
  y: 64
})

runtime.dispatch({
  type: 'move',
  entityId: 'hero',
  vector: { x: 1, y: 0 },
  source: 'human'
})
runtime.stepTicks(1)

Legacy action handlers and command interceptors are intentionally excluded from candidate evaluation unless their registration explicitly attests that the closure is candidate-safe:

runtime.registerAction('accept-duty', ({ entity }) => {
  entity.data.duty = 'accepted'
}, { candidateSafe: true })

The same option is available on registerCommandInterceptor(). Without it, a candidate action is unknown and a legacy interceptor does not run. This fails closed instead of letting a closure write live state, persistence, telemetry, or another external system while an abortable candidate is being evaluated.

Authored props, shrines, signs, and other fixed world objects opt into an immovable physics body. They still support system teleports and map transfers, but movement commands and collisions cannot silently push them out of place.

runtime.spawnEntity({
  id: 'village-waystone',
  kind: 'event',
  mapId: 'village',
  x: 320,
  y: 224,
  immovable: true
})

Human controls, Yuka governors, and replay runners use the same dispatch() contract. Ordinary dispatch and stepping mutate the same entity objects observed by renderers and UI; candidate commits use the explicit rebind seam described below. There is no client copy, room, socket, or synchronization layer.

Atomic candidate ticks

Systems that must validate a complete fixed tick before publishing it can use beginCandidateTick(). The runtime forks one isolated next tick, buffers its command and tick events, and reduces game-owned JSON state beside Rules. An abort discards that candidate without touching live state or emitting events; commit atomically replaces the authoritative Rules object graph and publishes the Rules view, reduced state, runtime events, and domain events as one immutable receipt.

const candidate = runtime.beginCandidateTick({
  id: 'rules-tick:42',
  expectedBaseTick: runtime.tick,
  state: { narrative: [], agency: { decision: 41 } }
})

const result = candidate.dispatch({
  type: 'action',
  entityId: 'guard',
  action: 'accept-duty',
  source: 'ai'
})

if (result.accepted) {
  candidate.reduce(
    {
      id: 'duty:guard:42',
      type: 'DUTY_ACCEPTED',
      tick: candidate.tick,
      payload: { actorId: 'guard' }
    },
    (state) => ({
      ...state,
      narrative: [...state.narrative, 'duty:guard:42'],
      agency: { decision: candidate.tick }
    })
  )
}

const publication = candidate.commit()
// publication contains the committed view and all buffered egress.

Candidate action handlers must be deterministic and candidate-local: mutate only the entity passed in their action context and return a rejection or stage game-owned consequences through reduce(). Logging, analytics, persistence, audio, UI, and other external effects consume the committed publication; they must not run from an action handler while a candidate is being evaluated.

subscribeCandidateTickErrors() observes exceptions raised by publication or legacy runtime-event listeners. Notification failures are post-commit diagnostics: every peer listener still runs, commit() still returns its receipt, and the candidate remains committed.

Candidate commits intentionally replace entity object identity so a frozen or otherwise hostile old entity reference cannot make a transaction half-apply. Renderers and other identity-caching consumers must use subscribeCandidateTicks() as their post-commit rebind seam, reading the immutable publication.view or reacquiring live entities by id. Ordinary non-candidate dispatch() and step() retain their existing in-place identity behavior.

Commands are strictly cloned and deeply frozen before candidate-safe actions or interceptors receive them. Executable authority registrations carry a revision, so changing a handler while a candidate is open makes that candidate stale. Beginning or committing another candidate during publication is rejected; all events from the committed tick are materialized before any observer runs.

Only one fixed tick is admitted per candidate. A stale candidate cannot commit after live state changes, and a new candidate after abort targets the same next tick. Existing dispatch(), step(), stepTicks(), save, and subscription APIs remain unchanged for integrations that do not need an atomic authoring gate.

Projectile entities use a sensor-like collision layer: authored map obstacles still stop them, but players, NPCs, neutral actors, and defeated bodies do not apply a physics impulse. Combat packages remain responsible for deciding which overlapped entities are valid targets and for resolving their hits.

Map dimensions are authoritative physics bounds. Spawns, teleports, transfers, save restores, and fixed-step movement keep the full entity hitbox inside those bounds, so a dash or collision cannot strand gameplay outside the authored map.

Teleports also sweep the entity hitbox against authored rectangular obstacles, so dashes and knockbacks stop at walls instead of tunneling through them. Story-driven gates, bridges, and collapses update the authoritative physics table without rebuilding the map or losing entity identity:

const opened = runtime.getMap('village')!.obstacles!.filter(({ id }) => id !== 'north-gate')
runtime.replaceMapObstacles('village', opened)

Scripted transitions that truly intend to bypass collision can still use collision: 'ignore'; map bounds remain authoritative in either mode.

The package version records its exact RPGJS v5 baseline. This release is based on RPGJS 5.0.0-beta.29 and bundles the fork's audited @rpgjs/[email protected] source. That source is newer than the public registry's 5.0.1; consumers do not inherit an unavailable or floating runtime dependency.

The bundled physics source includes authoritative point-ray and swept-radius projectile intersections. Solo's production boundary therefore allows at most 240,000 bytes while still requiring zero runtime imports, runtime dependencies, rooms, sockets, or synchronization code.

The .solo.N suffix identifies the coordinated private release. Runtime and build inputs are pinned exactly and refreshed to current compatible releases before that coordinated version is published.