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

@diegesis/sheet

v0.1.3

Published

Effect-oriented character sheet engine for Diegesis — the document is minimal, the effect pipeline is the heart: collect, filter, sort by UUID v7, apply, derive, emit.

Readme

@diegesis/sheet

Effect-oriented character sheet engine for Diegesis — the document is minimal, the effect pipeline is the heart: collect, filter, sort by UUID v7, apply, derive, emit.

A CharacterDocument stores only base values and effect instances; everything else — final attributes, derived values, flags, roll bonuses — is recomputed by a deterministic, auditable pipeline whenever anything changes. Lifecycle events flow through an @diegesis/events bus.

Installation

npm install @diegesis/sheet

Quick start

import { createDocument, SheetEngine, defineSystemPack } from '@diegesis/sheet';

const pack = defineSystemPack({
  id: 'mini-dnd',
  version: '1.0.0',
  derived: { 'hp.max': '10 + con * 2' },
  definitions: [
    {
      id: 'blessed',
      label: 'Blessed',
      changes: [{ kind: 'value', path: 'str', op: 'add', value: '2' }],
    },
  ],
});

const doc = createDocument(pack, { base: { str: 3, con: 2 } });
const engine = new SheetEngine(doc, { pack });

engine.applyEffect('blessed');
const sheet = engine.compute();

sheet.values.str;       // 5
sheet.values['hp.max']; // 14
sheet.audit;            // one entry per applied change

compute() is memoized (same reference until the next mutation) and returns a deeply frozen ComputedSheet — do not mutate it (strict mode throws TypeError).

API

SheetEngine

class SheetEngine {
  constructor(document: CharacterDocument, options: SheetEngineOptions);

  compute(): ComputedSheet;
  applyEffect(refOrDef: string | EffectDefinition, options?: ApplyEffectOptions): EffectInstance;
  removeEffect(id: string): EffectInstance | undefined;
  removeBySource(source: EffectSource): EffectInstance[];
  setEnabled(id: string, enabled: boolean): boolean;
  buildRoll(templateId: string): RollExpr; // roll template with active transforms applied

  attach(): () => void;                    // react to bus events (triggers, durations)
  notifyEvent(name: string, payload?: unknown): void; // feed events without a bus
  tickSeconds(seconds: number): void;      // decay second-based durations manually
  updateBase(mutator: (base) => base): void;
  refresh(): void;                         // force recompute + emit

  get document(): CharacterDocument;       // structuredClone of the live document
  loadDocument(json: unknown): CharacterDocument; // validate + hydrate
  destroy(): void;
}

SheetEngineOptions includes pack (SystemPack), bus, id (custom ID generator — must produce UUIDs if documents are serialized), validate (set false to skip pack validation), and durationEvents / clockEvent overrides.

createDocument(pack, { identity?, base?, effects? }) builds an empty document bound to the pack's id/version.

SystemPack

Describes the game system's rules: ordinal ladders (ordinals), derived values (derived, path → formula), roll templates (rollTemplates), and effect definitions (definitions).

interface SystemPack {
  readonly id: string;
  readonly version: string;
  readonly ordinals?: Record<string, readonly string[]>; // e.g. dice: ['d4','d6','d8',...]
  readonly derived?: Record<string, string>;             // path → formula
  readonly rollTemplates?: Record<string, RollTemplate>;
  readonly definitions?: readonly EffectDefinition[];
}

defineSystemPack(pack) is an identity authoring helper (typed pass-through, no validation). Explicit validation is validatePack(pack), which throws PackValidationError; the engine runs it automatically in the constructor unless validate: false.

Effect changes

An effect declares changes applied to the sheet while active:

| Change | Shape | Effect | | --- | --- | --- | | value | { kind: 'value', path, op, value } | set/add/multiply with formulas; upgrade/downgrade along an ordinal ladder (steps); append/remove on arrays | | roll | { kind: 'roll', target, transform } | Transforms roll templates matched by id, tag or glob (addDice, addModifiers, extraDice, bonus) | | flag | { kind: 'flag', path, value } | Writes into the flags.* namespace, visible to formulas and conditions |

Additional fields: condition (formula that toggles the effect), grants (child effects applied/removed in cascade), priority, icon.

Deterministic ordering

Effects apply in passes (flag → set → add → multiply → ordinal → derived), sorted within the pipeline by priority and then by instance id. Instance ids are UUID v7 (time-ordered, sortable), so ties resolve in application order and recomputation is fully deterministic.

Triggers

Triggers react to bus events: { on, condition?, changes?, effect?, roll?, rollInto? }. roll is a formula string or RollExpr; rollInto: { path, op?: 'add' | 'subtract' | 'set' } writes the result into a base path (default subtract). Each firing emits trigger:fired (and trigger:roll when a roll happens).

Durations

duration: { unit, value?, event? } with validated invariants: value required for seconds/rounds/turns, event required for until-event. Rounds/turns decay on the events configured via durationEvents (default round:end/turn:end); seconds decay on clockEvent (default clock:tick, payload a number or { elapsed }) or via engine.tickSeconds(n). Expiration emits effect:expired and cascades to grants.

Stacking

stacking: { group?, mode? } — stack (default: everything applies), newest (only the most recent instance of the group) or highest-priority. Suppressed effects appear in computed.suppressed with the reason (disabled / stacking / condition).

Events and bus

createSheetBus() builds a bus pre-configured with the sheet contract (effect:applied, effect:removed, effect:expired, effect:enabled, effect:disabled, computed, trigger:fired, trigger:roll). Gameplay events (round:end, turn:start, clock:tick, ...) are declared via the events option, which merges them into the contract with full typing — compatible with unknownEvents: 'reject':

import * as v from 'valibot';
import { createSheetBus } from '@diegesis/sheet';

const bus = createSheetBus({
  events: {
    'round:end': v.object({}),
    'turn:end': v.object({}),
    'clock:tick': v.object({ elapsed: v.number() }),
  },
});

const engine = new SheetEngine(doc, { pack, bus });
engine.attach(); // the engine reacts to any bus event

bus.on('computed', ({ patches }) => ui.applyPatches(patches));
bus.emit('round:end', {}); // fires triggers and decays durations

With attach(), the engine processes triggers and durations on every event and re-emits computed only when something actually changed (patches { path, previous, next }).

Serialization

CharacterDocument is plain JSON ({ systemId, systemVersion, identity, base, effects }). To hydrate, use engine.loadDocument(json) — validated against characterDocumentSchema, which requires a UUID in effect.id. The engine's default ID generator is UUID v7; custom generators via SheetEngineOptions.id must produce UUIDs if the document will be serialized and rehydrated. engine.document returns a structuredClone of the current state (there is no snapshot() method).

const json = JSON.stringify(engine.document);
engine.loadDocument(JSON.parse(json));

Errors

SheetError, EffectCycleError, UnknownEffectError, UnknownOrdinalError, UnknownTemplateError, PackValidationError — all carry a typed code (SheetErrorCode).

Documentation

Related packages


Part of diegesis-sdk. MIT.