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

@tikron/rpg

v0.7.0

Published

Isomorphic RPG combat core for Tikron — data-driven skills, buffs, effects, stats, and combat math as pure deterministic reducers shared by the authoritative room and the client HUD. Zero timers, zero globals.

Downloads

33

Readme

@tikron/rpg

Isomorphic, deterministic RPG combat core for Tikron — data-driven skills, buffs, effects, stats, and combat math as pure reducers behind one timer-driven engine. A port of the AAEmu skill system's semantics (stat summation, two-stage hit resolution, cast pipeline, buff stacking/tolerance, aggro/AI) with hardcoded combat constants reproduced faithfully. Only dependency: @tikron/sim.

npm i @tikron/rpg

No timers, no Date.now, no Math.random, no DOM/Workers globals. Time is the absolute now (ms) you pass into every entry point; randomness is a single seeded stream. Same seed + same call sequence ⇒ identical event stream — on the server, in a test, or after a Durable Object eviction restores from a snapshot.

The engine loop contract

tick(now) is the ONLY driver. A room feeds player intents (useSkill, moveUnit, …) whenever they arrive, then calls tick once per room tick to advance time and drain the combat event feed. Everything else — cast/channel timers, projectile travel, buff ticks, auto-attack swings, NPC AI, regen, combat-state timeouts, death — happens inside tick and comes back as CombatEvent[] to broadcast.

import { CasualRealtimeRoom } from "@tikron/server";
import { RpgEngine, sampleContent, type CombatEvent } from "@tikron/rpg";

class DungeonRoom extends CasualRealtimeRoom {
  private rpg = new RpgEngine(sampleContent, { seed: 0xC0FFEE });

  onReady() {
    this.rpg.spawnNpc("boss", { x: 0, y: 0 });
    this.setSimulationInterval((tick) => this.step(tick), 100);
  }

  onJoin(client) {
    this.rpg.spawnPlayer({ id: client.id, pos: { x: 5, y: 0 }, weapon: "sword" });
    this.onMessage("cast", (from, m: { skillId: string; target?: string }) =>
      this.rpg.useSkill(from.id, m.skillId, m.target ? { unitId: m.target } : undefined, this.now()),
    );
  }

  private step(tick: number) {
    for (const ev of this.rpg.tick(this.now())) this.broadcastCombat(ev);
  }

  private broadcastCombat(ev: CombatEvent) {
    /* relay to clients; drive authoritative hp/mp state from `unitPoints`/`damaged`/… */
  }
  private now() { return this.currentTick * 100; }
}

Content is a plain JSON-able ContentPack (skills, buffs, effects, npcs, weapons); validateContent throws on any dangling id before you ever spawn a unit. The custom effect kind and registerCustomEffect are the extension points for game-specific logic.

getUnit(id) returns a readonly UnitView for HUDs and input gating. It resolves active crowd-control for you — stunned, rooted, silenced, sleeping, and canMove (alive && !stunned && !rooted && !sleeping) — so a room can reject a move client-side without knowing any buff ids: if (!engine.getUnit(id)?.canMove) return;. The same predicates are exported as functions (isRooted(engine, unit), isStunned, …) for use inside custom effect handlers.

Key API

new RpgEngine(content, opts?) · spawnPlayer · spawnNpc · useSkill(caster, skillId, target, now) · startAutoAttack · moveUnit · stopCast · resurrect · grantXp · tick(now) → CombatEvent[] · getUnit / units() (readonly UnitView) · registerCustomEffect · serialize() / RpgEngine.restore(content, snap, opts?).

Pure helpers are exported too — computeDamage, computeHeal, rollHitType, computeStat, killExp, TimerHeap, makeRng — so combat math and scheduling unit-test without an engine.

Determinism & persistence

Seed the engine (opts.seed), pass a monotonic now, and two engines agree tick-for-tick. serialize() returns a JSON-safe snapshot (units, buffs, cooldowns, aggro, AI, cast state, the timer heap, and the RNG position); RpgEngine.restore(content, snap) resumes with byte-identical subsequent behavior — the eviction-survival path for a Durable Object room.

The snapshot also captures the engine options it ran under (pvpEnabled, combatTimeoutMs, aiIntervalMs, regenIntervalMs, maxUnits), so a restored fight keeps the same rules rather than silently reverting to defaults. Pass an explicit opts to restore(content, snap, opts?) only to override specific fields (the seed always comes from the snapshot). One thing is not serialized: custom effect handlers. Re-call registerCustomEffect after every restore, or any custom effect is a no-op until you do.

Clock base after restore. The snapshot is stamped with RpgSnapshot.nowMs (a public field: the last absolute ms the engine advanced to). A room's currentTick restarts at 0 on a DO cold start, so currentTick * tickMs would jump behind nowMs and freeze every timer. Rebase your clock onto the snapshot so now stays monotonic across the restore:

const snap = load();                       // your persisted RpgSnapshot
const engine = RpgEngine.restore(content, snap);
const clockBase = snap.nowMs;              // engine time at eviction
// each tick: feed absolute ms that only ever increase
engine.tick(clockBase + this.currentTick * this.tickMs);

As a backstop, tick(now) clamps now = max(now, internalNow) and never rewinds — a too-small now stalls rather than corrupts — but the clamp only prevents damage; you still need the rebase above for time to actually keep flowing.

Gotchas

  • setEquipmentModifiers does not top up pools. A gear/buff source that raises maxHp or maxMp (flat) lifts the ceiling but leaves current hp/mp where they are (it only clamps them down if the max drops). Heal/refill explicitly if you want the new headroom filled on equip.

Links & license

tikron.dev · AGENTS.md. Licensed under the Tikron License 1.0 (adapted from FSL-1.1) — converts to Apache-2.0 one year after each release. See LICENSE.