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

@rifted/sdk

v1.1.1

Published

Typed authoring surface for Rifted game content (GCF)

Downloads

2,091

Readme

@rifted/sdk

Typed authoring surface for Rifted game content.

A mod is TypeScript. Card bodies read like ordinary code — const, when, statement calls — and the SDK compiles them at build time into a GCF document (s-expressions as JSON) the engine loads directly, plus fluent locale files for every string. Nothing from this package runs inside the game: the build product is data.

If you are starting a new mod, use @rifted/cli to scaffold a project — it handles build, validation, localization and .rmod packaging.

Installation

bun add @rifted/sdk

Works with any Node.js 20+ runtime; Bun is recommended.

Quick example

import { addStack, dmg, Pkg, rand, selfDmg, when } from '@rifted/sdk'

const pkg = Pkg('ex', { version: 1, name: 'Examples' })

const coins = pkg.playerState('coins')

pkg.card('gambit', {
	name: 'Gambit',
	description: 'Roll a die. High: spend 2 coins to strike hard.',
	cooldown: 2,
	scale: 'hyp',
	tags: ['attack'],
	params: { base: 5 },
	onPlay({ params }) {
		const roll = rand(1, 6).as('roll') // one call = one dice roll
		when(roll.gt(4).and(coins.spend(2)), () => {
			dmg('weakest_enemy', params.base.scaled().mul(roll))
			addStack(1)
		}).otherwise(() => {
			selfDmg(roll.div(2).ceil())
		})
	},
})

export default pkg

rifted build turns this into gcf.json:

{ "id": "gambit", "cooldown": 2, "scale": "hyp", "tags": ["attack"],
	"params": { "base": 5 },
	"on_play": ["let", { "roll": ["rand_int", 1, 6] },
		["if", ["and", ["gt", "let.roll", 4], ["spend_player_state", "ex:coins", 2]],
			[["damage", "weakest_enemy", ["mul", ["scale", "card.params.base"], "let.roll"]],
			 ["add_card_stack", 1]],
			["self_damage", ["ceil", ["div", "let.roll", 2]]]]] }

…and name/description into dist/locales/en.ftl — never into the document (the engine carries no presentation data).

The mental model

Three kinds of slots, three writing modes:

| slot | meaning | you write | |---------------------------------------------------|----------------------|----------------------------------------| | onPlay, hook bodies, do, onEnter, execute | do something | statements: dmg(...), when(...) | | when, until, amount, render | compute something | an expression: self.hpPercent.lt(50) | | encounters, maps, affinities | describe something | a plain object |

Inside effect bodies an ambient collector records each statement (the same trick as describe/it in test runners). The callback runs once at build time; runtime branching is when(cond, ...), while a plain JS if over ordinary values is build-time branching and stays legal. A Cond accidentally dropped into a JS if is caught by leak-tracking and fails the build.

Values and snapshots

Expressions are Expr values with fluent ops (add/mul/.../gt/scaled) or the f template for formula-shaped math:

addBaseDamage(f`floor((100 - ${self.hpPercent}) * 0.1) * ${mod.stack}`)

rand(lo, hi) registers a let-binding — one call, one roll, references share the result. .pin() snapshots any changing value the same way.

Splitting a mod across files

Files declare into local content() composers — the same builder surface as the package, detached from it (if you know grammY's Composer, this is that pattern). The entry mounts them with use(); references are plain exports:

// src/content/cards/attack.ts
export const attack = content()
export const strike = attack.card('strike', { ... })   // a ref, import anywhere

// src/content/cards/index.ts — the folder aggregator is itself a composer
export const cards = content()
cards.use(attack, rituals)

// src/content/world.ts
import { strike } from './cards/attack'
export const world = content()
world.encounter('fight', { enemies: [goblin], loot: { pool: [strike], offer: 1, picks: 1 } })

// src/index.ts
const pkg = Pkg('mymod')
pkg.use(cards, world)   // mount order = document order
export default pkg

Registration is module-local, so import order never matters. Mounting is live (grammY semantics): definitions added to a composer after use() still land in the document. A composer belongs to exactly one parent — mounting it twice fails the build, and a reference whose composer was never mounted is caught by the build-time integrity check (reference to unknown card "slash" — defined in a content() that was never mounted via use()?).

Pure handles (pkg.playerState, pkg.event) need the namespace and live on the package — declare them in a shared module and import them from content files. For content that needs the package itself there are also function-style modules: defineContent(pkg => ...), run via pkg.use(fn).

The canonical layout:

src/
  pkg.ts            the package identity — nothing else, safe to import anywhere
  state.ts          handles: player/team state, custom events (pure values)
  content/
    core.ts         shared definitions (affinities, seals, watchers) + their refs
    cards/
      attack.ts     a composer per file, refs as plain exports
      rituals.ts
      index.ts      the folder composer: cards.use(attack, rituals)
    world.ts        enemies, encounters, the map
  index.ts          composition root: pkg.use(core, cards, world); export default pkg

Localization

The engine never sees strings: GCF has a strict field whitelist, and the client derives fluent keys from definition ids (ex:strikecard-ex-strike with .name/.description attributes). The SDK compiles strings into .ftl files; render bindings become fluent variables.

pkg.card('strike', {
	name: 'Strike',                                  // default locale
	description: 'Deal { $dmg } damage.',            // { $dmg } = render binding
	render: ({ params }) => ({ dmg: params.base.scaled() }),
})

pkg.card('gamble', { name: { en: 'Gamble', ru: 'Авантюра' } })   // per-locale
pkg.modifier('venom', { name: { key: 'shared-venom.name' } })    // fluent alias

Generated messages carry translator comments (# card ex:strike — params: base=6 — variables: { $dmg }). Hand-written locales/*.ftl files always win over generated strings; rifted locales:scaffold --lang ru appends stubs for whatever is still untranslated.

Quests out of card bodies

deferCard sends the card away and raises a watcher; finish() inside its body returns it to the deck:

onPlay({ params, deferCard }) {
	dmg('selected', params.base)
	deferCard({
		id: 'oath',
		name: 'Oath of Blood',
		visible: true,
		on: 'damage_dealt',
		do({ card, event }) {
			card.state.progress.inc(event.amount)
			when(card.state.progress.gte(card.params.oath), () => {
				addStack(1)
				card.state.progress.set(0)
				finish()
			})
		},
	})
}

Everything is typed end to end: params/state from their declarations, event payloads from the builtin dictionary or your pkg.event shape, and context capabilities as phantom types — mod.stack does not typecheck in a card render slot, where the engine would silently resolve it to 0.

Entry points

| import | contents | |-----------------------|-------------------------------------------------------------------------------------------------| | @rifted/sdk | the authoring surface: Pkg, content(), statements, on, intent, phase, f, rand | | @rifted/sdk/raw | escape hatch: raw get/value/cond/effect s-expressions | | @rifted/sdk/schema | zod schema + op tables of the GCF document, validateDocument, JSON Schema 2020-12 emitter | | @rifted/sdk/pack | .rmod packing: zip with manifest, sha256-hashed assets and locale files | | @rifted/sdk/locales | fluent generation: buildLocales, LocText, scaffold stubs |

Guarantees

  • Engine equivalence — the test suite builds the engine's own examples.gcf.json and vanilla.gcf.json from SDK sources and asserts deep equality. The emitted format is the loader's format.
  • Determinism — same input, byte-identical gcf.json and .rmod.
  • Early errors — unknown ops/targets/events (with did-you-mean), wrong arity, leaked conditions, unused bindings, broken fluent syntax and schema violations all fail the build pointing at the definition.

License

MIT