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

@medieval-kit/registry

v0.2.1

Published

A lowpoly medieval procedural model library for Vibe3D and Three.js.

Readme

Medieval Kit

A lowpoly medieval model library for Vibe3D and Three.js. Installing a prop puts its source in your own tree, as readable TypeScript you can edit.

The whole kit in one scene

Every model

Install

bunx vibe3d init
bun add three
bun add -d @types/three

init writes models.json and puts the shared Vibe3D runtime under src/lib/vibe3d/, which the installed models import from. @types/three is a dev dependency and not optional: three ships no type declarations of its own, so without it the source you just installed does not typecheck.

init seeds models.json with @scifi-kit only, so add this registry to the registries object it created:

{
  "registries": {
    "@medieval-kit": {
      "source": "npm:@medieval-kit/registry",
      "version": "latest"
    }
  }
}

That map is how the namespace resolves. Without it vibe3d add stops with Registry @medieval-kit is not configured in models.json.

Then take one model, or the lot:

bunx vibe3d add @medieval-kit/wooden-barrel
bunx vibe3d add @medieval-kit

Project setup

The installed source imports through the @/ alias that models.json declares, and it imports .ts files by their extension. Both have to be true on your side as well. In tsconfig.json:

{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "paths": { "@/*": ["./src/*"] }
  }
}

No baseUrl. It was removed in TypeScript 7, and paths resolves relative to this file on its own. Verified against a clean project on 7.0.2.

The alias has to be set in your bundler too, because TypeScript's paths only teaches the type checker. For Vite:

resolve: { alias: [{ find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) }] }

Models

37 models and one shared lib. Addresses are @medieval-kit/<id>. This table is generated from the models themselves with bun scripts/catalog-table.ts. A hand-written list goes stale on the first model you add, and this one already did once.

| Model | Category | Triangles | Parts | Size (m) | Material slots | Animated | | --- | --- | ---: | ---: | --- | --- | :-: | | wooden-chest | Furniture | 552 | 4 | 0.86×0.51×0.48 | oak, iron | ✔ | | wooden-barrel | Props | 1690 | 3 | 0.83×1.05×0.83 | oak, iron | | | wooden-crate | Props | 1320 | 3 | 0.69×0.52×0.55 | oak, iron | | | wooden-bucket | Props | 439 | 4 | 0.31×0.46×0.30 | oak, iron | | | trestle-table | Furniture | 660 | 3 | 1.91×0.74×0.78 | oak | | | wooden-bench | Furniture | 216 | 3 | 1.62×0.45×0.30 | oak | | | wooden-stool | Furniture | 180 | 2 | 0.38×0.43×0.36 | oak | | | pitch-torch | Lighting | 239 | 3 | 0.13×0.70×0.13 | oak, char, ember | ✔ | | iron-lantern | Lighting | 480 | 4 | 0.15×0.30×0.17 | iron, glass, char, ember | ✔ | | iron-anvil | Smithy | 256 | 6 | 0.54×0.75×0.41 | iron, steel, oak | | | cart-wheel | Structure | 1136 | 4 | 1.04×1.06×0.19 | oak, iron | | | log-pile | Props | 924 | 2 | 0.99×0.58×0.70 | oak | | | hay-bale | Props | 910 | 3 | 1.04×0.42×0.43 | straw, cloth | | | linen-sack | Props | 342 | 3 | 0.34×0.53×0.33 | cloth | | | oak-tankard | Props | 390 | 4 | 0.11×0.16×0.14 | oak, iron | | | straw-broom | Tools | 1048 | 3 | 0.30×1.22×0.30 | oak, straw, cloth | | | bronze-bell | Props | 1048 | 4 | 0.51×0.68×0.36 | brass, iron, oak | ✔ | | tavern-sign | Props | 876 | 3 | 0.38×2.29×0.93 | oak, iron | ✔ | | wicker-basket | Props | 2326 | 3 | 0.34×0.16×0.35 | oak, produce | | | leather-book | Props | 440 | 3 | 0.20×0.08×0.27 | leather, cloth, brass | | | glass-phial | Props | 339 | 3 | 0.06×0.14×0.06 | glass, ember, oak, char | | | coin-pouch | Props | 602 | 3 | 0.18×0.11×0.16 | leather, cloth, brass | | | wooden-ladder | Structure | 440 | 2 | 0.49×2.20×0.06 | oak | | | wooden-fence | Structure | 860 | 2 | 5.18×1.31×0.31 | oak | | | wooden-hoe | Tools | 394 | 3 | 0.23×1.23×0.33 | oak, iron, steel | | | wooden-shovel | Tools | 496 | 3 | 0.27×1.20×0.08 | oak, iron | | | wooden-pitchfork | Tools | 392 | 3 | 0.27×1.58×0.15 | oak, iron | | | iron-cauldron | Lighting | 1134 | 3 | 1.09×1.37×1.10 | stone, iron, char, ember | ✔ | | hand-cart | Structure | 2044 | 4 | 0.87×0.68×2.49 | oak, iron | ✔ | | vegetables | Props | 1718 | 2 | 0.60×0.12×0.59 | produce | | | round-shield | Arms | 729 | 3 | 0.73×0.73×0.17 | oak, leather, iron | | | forge-hearth | Smithy | 1426 | 4 | 2.06×1.78×0.87 | stone, char, ember, oak, leather, iron | ✔ | | stone-well | Structure | 1376 | 4 | 1.88×2.00×1.28 | stone, oak, iron, cloth | ✔ | | stone-trough | Structure | 344 | 2 | 1.52×0.45×0.60 | stone, water | | | grindstone | Smithy | 568 | 4 | 1.24×0.95×0.70 | stone, oak, iron, water | ✔ | | market-stall | Structure | 384 | 4 | 1.83×2.03×0.97 | oak, cloth | | | post-mill | Structure | 1672 | 4 | 6.60×7.23×6.98 | oak, iron | ✔ |

39 518 triangles in total. The whole kit in one scene costs less than a single mid-complexity character model.

Every model depends on @medieval-kit/core, a shared support item holding the palette, deterministic randomness, and the geometry vocabulary. Pulling a single prop brings it along, so an individually installed crate still looks like it came from the same catalogue as the barrel beside it.

Plain WebGL

This kit does not require WebGPU. It declares capabilities: [] and builds on MeshStandardMaterial and MeshBasicMaterial, so it renders on WebGLRenderer as well as WebGPURenderer.

Configuration

Models are procedural, not baked. Every prop exposes typed fields:

import { createModel } from '@/models/medieval-kit/wooden-barrel/model.ts'

const barrel = createModel({ staveCount: 17, taper: 0.22, seed: 42 })
scene.add(barrel.root)

barrel.configure({ hoopCount: 6 })   // rebuilds geometry inside a stable root
barrel.dispose()

seed is a real field, not a debug knob. Variation is deterministic: the same seed always produces the same prop, so previews, tests, and art direction stay reproducible. Change the seed to get a different barrel from the same model.

Runtime anatomy

Each model follows the Vibe3D protocol:

  • root keeps its object identity for the model's whole lifetime.
  • parts expose stable semantic anchors. Attach your own lights, labels, or gameplay objects to part.anchor. They survive configure(), because only part.content is rebuilt.
  • materials resolve per slot and can be overridden. Materials you supply are borrowed: the model never disposes them.
  • actions and update(delta) carry interactive state. Eleven models have them, and they are the ✔ column in the table above: wooden-chest, pitch-torch, iron-lantern, bronze-bell, tavern-sign, iron-cauldron, hand-cart, forge-hearth, stone-well, grindstone and post-mill. Crank the grindstone and it turns and slows; set the mill turning and its sails go round; lower the well's bucket down the shaft:
const chest = createModel()
chest.actions.setOpen(true)
chest.update(deltaSeconds)   // the lid eases open, frame-rate independent

const torch = createModel()
torch.actions.setLit(false)
torch.update(deltaSeconds)   // flame flicker and light modulation

A model that never receives update() simply stands still; nothing animates behind your back. Reach for configure() when a user changes a setting; it rebuilds topology and is not cheap. Anything that changes per frame belongs in update().

Material slots

Thirteen slots, and each one exists because a single material could not carry the difference.

| Slot | What it covers | Why it is separate | | --- | --- | --- | | oak | timber | the baseline everything else is measured against | | iron | forged iron, oxidised and matte | separate from steel: the difference is not in colour but in ROUGHNESS, and vertex colour cannot carry roughness | | steel | steel polished by use | the anvil face, the shovel blade, the pitchfork tines | | brass | bronze and copper | the bell, the coins | | straw | straw, wicker, broom bristle | a bale declaring itself "oak" would be a lie told to the consumer | | cloth | linen, sackcloth, rope | woven, so it takes light softly and never glints | | leather | worked leather | darker and glossier than cloth at the same colour | | glass | blown glass | transparent, depthWrite OFF, DoubleSide | | produce | fruit and vegetable skin | given straw's roughness, an apple would look like dry grass | | ember | flame | MeshBasicMaterial: it does not receive light, it emits | | char | charcoal and pitch | the darkest thing in the kit, and it has to stay readable against shadow | | water | standing water | a thin transparent film over whatever holds it: in the reference trough the basin reads hue 39.5, which is the hue of the STONE underneath, so it works through opacity rather than colour | | stone | dressed and rubble masonry | its colour is close to weathered oak; what separates them at a glance is that stone scatters light completely flat, and roughness is not something vertex colour can carry |

ember also acts as a rule in two places: bodies in that slot are skipped entirely while ambient occlusion and mottle are baked. The reason is simple: in an unlit material the vertex colour is the final colour, so darkening it puts the flame out. The rule is attached to the slot itself rather than to a per-model flag, so it cannot be forgotten.

Colour variation lives in the geometry, not in the materials: each of the barrel's 17 staves carries its own tone as vertex colours, and all of them share one material and therefore one draw call.

Building from source

bun build.ts                                  # emit dist/registry.json
bunx vibe3d registry validate dist/registry.json

The build walks models/. A folder containing model.ts becomes a vibe3d:model; any other folder becomes a vibe3d:lib. Registry dependencies are derived from the source. An import from ../core/ makes @medieval-kit/core a dependency automatically, so there is no hand-maintained list to go stale.

drafts/ is deliberately outside that walk: the code is kept in the tree but never reaches the published package, including its own drafts/README.md, which is why this sentence does not link to it. A relative link on an npm package page resolves against the repository, and there is nothing at the other end of it for a reader of this page.

Released under the MIT License.