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

sandstone-npcs

v0.0.2

Published

Mannequin-based NPCs with dialogue trees for Sandstone datapacks

Readme

sandstone-npcs

Mannequin-based NPCs for Sandstone datapacks: skinned, posable mannequins with typewriter-style dialogue trees. Fully self-contained, click detection uses a vanilla advancement trigger, no external dependencies required.

Install

bun add sandstone-npcs

Quick start

import { CreateNPC, ProfileProperties } from 'sandstone-npcs'
import { give, MCFunction } from 'sandstone'

const shopkeeper = CreateNPC('shopkeeper', {
  name: 'Shopkeeper',
  skin: { properties: ProfileProperties('<skin texture hash>') },
  lookAt: 'interactor',
  dialogue: (d) => d
    .node('main', {
      advance: 'click',
      lines: [
        { text: 'Welcome to the shop!' },
        { text: 'Take a look around.' },
      ],
      // no `next` needed chains straight into 'gift' below
    })
    .node('gift', {
      advance: 'auto',
      autoDelay: 30,
      lines: [{
        text: 'Here, have this on the house.',
        onComplete: () => give('@s', 'minecraft:paper', 1),
      }],
    }),
})

// CreateNPC only defines the NPC so call spawn()
MCFunction('myPack:load', () => {
  shopkeeper.spawn([0, 64, 0], [180, 0])
}, { runOnLoad: true })

NPC options

interface NPCOptions {
  name: string
  skin: /* a minecraft:profile component - see Skins below */
  lookAt?: 'nearest' | 'interactor' | 'none'  // defaults to 'nearest'
  lookDistance?: number                  // blocks - "nearest" search radius, defaults to 5
  dismissDistance?: number               // blocks - interactor can wander this far before dialogue auto-ends, defaults to 5
  pose?: 'standing' | 'crouching' | 'sleeping' | 'sitting'  // defaults to 'standing'
  mainHand?: string | ItemWithComponents
  offHand?: string | ItemWithComponents
  dialogue?: DialogueTree | ((d: DialogueBuilder) => void)  // takes precedence over onInteract
  onInteract?: () => void                // runs as the NPC - see asInteractor() below
}

Omit both dialogue and onInteract for a purely decorative NPC, no click detection gets set up for it at all.

The NPC instance

CreateNPC returns a handle with methods for controlling that one NPC:

interface NPCInstance {
  readonly id: string

  // Selector matching this NPC's mannequin
  readonly Selector: SelectorClass

  //Summons the mannequin (+ hitbox/seat) at the given position
  spawn(position: [number, number, number], rotation?: [number, number]): void

  // Removes this NPC's entities.
  kill(): void

  // Simulates a player interacting with this NPC (same effect as a click)
  trigger(): void

  // Makes the NPC face a player/entity selector or a fixed position, once.
  lookAt(target: /* a single-entity selector */ | [number, number, number]): void

  // Teleports the NPC. Omit `position` to teleport to wherever the caller's current execution context is
  teleport(position?: [number, number, number], rotation?: [number, number]): void

  // Changes pose. Switching to/from 'sitting' summons/removes its seat as needed.
  pose(pose: 'standing' | 'crouching' | 'sleeping' | 'sitting'): void

  // Re-equips the NPC's main/off hand. Omit a side to leave it as-is.
  equip(item: { mainHand?: string | ItemWithComponents, offHand?: string | ItemWithComponents }): void

  // Changes the NPC's skin.
  skin(skin: /* a minecraft:profile component */): void

  // Runs a callback as this NPC's mannequin.
  asMe(callback: () => void): void

  // Runs a callback as whichever player is currently tagged as this NPC's interactor
  asInteractor(callback: () => void): void

  // Plays the NPC's punch animation
  swing(hand?: 'mainhand' | 'offhand'): void
}

There's also an exported despawnNpcs() function that kills every NPC ever created with CreateNPC, if you want a single bulk cleanup call.

Dialogue trees

Build a dialogue inline via CreateNPC's dialogue option.

CreateNPC('id', {
  // ...
  dialogue: (d) => d
    // optional: fallback speed/revealMode for every line below that doesn't set its own.
    .options({ revealMode: 'word', speed: 4 })
    .node('main', {
      advance: 'click',          // or 'auto' + autoDelay (ticks to hold before continuing)
      lines: [
        { text: 'Static line.' },
        { variants: ['Picks one of these at random.', 'Or this one.'] },
        {
          text: 'Skipped unless the condition holds.',
          condition: someCondition,
          revealMode: 'instant',  // 'character' (default) | 'word' | 'instant'
          speed: 2,               // ticks per revealed unit (character, or word in 'word' mode)
          onShow: () => { /* runs as the player right when this line starts */ },
          onComplete: () => { /* runs as the player once fully revealed */ },
        },
      ],
      // next defaults to whichever node is added right after this one, only
      // needed to branch or jump elsewhere:
      // next: { condition: someCondition, then: 'nodeA', else: 'nodeB' },
    })
    .node('nodeA', { lines: [...] }),
})

Text reveals one of three ways, set per-line via revealMode (or tree-wide via .options()):

  • 'character' (default) - classic typewriter, one character at a time
  • 'word' - reveals a whole word at a time (speed then means ticks per word, not per character)
  • 'instant' - the line appears all at once; advance/autoDelay still govern what happens after it's up

start(id) picks a non-default starting node (defaults to the first one added).

Need one dialogue tree shared across several NPCs? Build it separately with the same builder callback via the exported DialogueTree(id, ...) factory, then pass the resulting object as dialogue to each CreateNPC call instead of a callback.

Developing this library

bun run setup      # install + link the library into test/
bun dev:build      # build the library (outputs to lib/)
bun dev:watch      # watch mode - rebuilds library and test project on changes
bun dev:test-build # dry-build the test datapack, verbose output
bun test           # run snapshot tests