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

@umicat/three-sdk

v0.17.3

Published

Three.js runtime for Umicat games: the scene3d design format, its loader with physics, a kinematic character controller, and the Umicat platform via @umicat/platform-sdk.

Readme

@umicat/three-sdk

The three.js runtime for Umicat games, per ADR-033. Platform services come from @umicat/platform-sdk untouched; what lives here is the engine layer we own.

Status: published and in use. @umicat/three-sdk is on npm; Balaboo (a tower defence that ships from woodland/) is built on it, and umicat-template-3d is what new 3D projects start from.

What it has: the scene3d format and loader, physics wiring, a character controller and animator, an input layer that mounts on-screen controls on touch devices and binds keys everywhere, bone sockets, hit tints, audio, and the editor's screenshot/video-capture protocol. What it still does not have: an editor.

This paragraph said "seed, not a product — no input system, no character controller, no audio, and no published package" for ten minor versions after each of those arrived. A status line is the first thing anyone reads and the last thing anyone updates.

@umicat/platform-sdk        identity · saves · gameData · rooms · ai · voice · dialogue
        ▲                              (shared with @umicat/phaser-sdk)
        │
@umicat/three-sdk       ThreeUmicat · scene3d · loadScene3D · physics · Input3D
                        CharacterController3D · CharacterAnimator · GameAudio
                        sockets · tints · setupScreenshotListener/setupRecordingListener
        ▲
   your game            gameplay

What is proven, and by what

npm test runs a real three.js game in a real browser against a host speaking the real wire protocol. Only the host page and the backend behind it are mocked — every line of SDK is the shipped code.

| claim | test | |---|---| | a 3D game does the real handshake and gets identity | host is umicat-home-ui, not standalone | | cloud saves go over RPC, not to localStorage | round-trip, plus the host's RPC log | | runtime AI uses the same channel | ai.complete returns {ok, text} per the protocol | | scene3d builds the authored scene | entity count, parenting, the right clip playing | | physics runs | a crate dropped from y=6 settles on the ground and does not fall through | | design mode is render-only | same entities, zero mixers, zero bodies | | authoring mistakes fail loudly | duplicate ids and missing clips throw at load, naming the clips that exist | | music and effects are two volumes | the clip's gain hangs off sfxGain, not the master — audio-volume.test.mjs |

Eight suites, not one: the table above is the platform seam. The others cover the character controller, the animator, sockets, tints, input icons, the courtyard sample and the audio graph.

slice.png is the scene those assertions describe, rendered.

The scene3d format

src/scene3d.ts. It inherits ADR-021's answer to what does the editor edit — design data on disk, no save loaded — because that answer is engine-neutral. It does not inherit the 2D schema, which does not survive three dimensions.

Each rule is a decision:

  • Rotation is a quaternion, not Euler angles — those are order-dependent and interpolate badly, so an editor round-tripping them drifts.
  • Ids are authored and stable. The editor, the runtime and saves all refer to entities by id; regenerating them on load breaks every reference on first edit.
  • Transforms are local to parent. Storing world transforms makes reparenting a lie.
  • Assets are referenced by id, never by path — paths change on re-import.
  • Colliders are explicit. A render mesh used as a dynamic collider is the classic way to ship a game that is correct and unplayably slow.
  • Animation clips are mapped semantically per asset ({ walk: 'Walk' }). Guessing that every model calls its walk cycle Walk fails silently; an independent review's cross-rig retarget returned zero matched bones and zero tracks, which is the same class of failure, quieter.

validateScene refuses duplicate ids, dangling parents, entities that would render nothing, trimesh colliders on dynamic bodies, and malformed quaternions — at load, because every one of them otherwise appears as a blank screen later.

Solved: the character that disappeared once the camera moved

Worth keeping, because the symptom pointed everywhere except at the cause.

In the Courtyard sample the fox rendered at boot and was gone after walking, leaving flat ground. Every measurement said it should be visible: 18 draw calls and 5,042 triangles that frame, hero.visible === true, bones at sensible world positions, and three's own Vector3.project(camera) putting the character at NDC (0, 0) — dead centre — while the centre of the frame was a single flat colour. A canvas read-back agreed with the screenshot, so it was the render and not the capture.

It was the ground. makePrimitive laid the plane down with mesh.rotation.x = -Math.PI / 2, and applyTransform then set the object's quaternion from the entity's authored rotation — identity — discarding it. So every "ground" stood upright as a 40x40 wall. At boot the camera and the character were on the same side of it and the picture looked right, with the wall reading as ground. Walk past it and the camera is behind a wall, still pointing correctly at a character it can no longer see. A raycast through the frame centre said it in one line: ground at 9.79m, fox at 13.89m.

Fixed by rotating the geometry (PlaneGeometry(...).rotateX(-Math.PI/2)) rather than the object, so an entity's authored rotation is never overwritten. After the fix the same raycast returns fox at 13.89m then ground at 18.05m — which matches the hand-computed ground crossing exactly.

Two lessons kept deliberately. A correct-looking measurement can be measuring the right thing about the wrong scene: NDC (0,0) was true the whole time, the character was centred, behind a wall. And the first render looked fine, which is how a construction bug this total survived a screenshot.

One real bug was fixed while chasing it and is unrelated but worth having: a SkinnedMesh's bounding sphere comes from the bind pose and does not follow the bones, so three.js culls a character against a stale volume once it moves. loadScene3D sets frustumCulled = false on skinned meshes.

0.3.0 — actions, not just locomotion

CharacterAnimator owns the two things every game was writing by hand, and both are character behaviour rather than game logic (ADR-034).

Locomotion follows character.state, not the keys held — a clip chosen from input leaves a character walking in mid-air.

An action is a one-shot that interrupts and returns. Attacking is not a state you enter, it is a thing that happens: play('attack') runs the clip once, holds its last frame rather than snapping to the bind pose in the gap, and hands control back. busy is there so a game can refuse a second swing from one press — firing twice is the usual reason an attack feels broken.

Changing locomotion mid-action does not blend a walk cycle into the middle of a sword swing: the new locomotion clip is swapped in underneath at zero weight and faded up only when the action releases.

Names resolve through the manifest's map first (attack -> attack-melee-right) and then by raw clip name, so a game can reach a clip nobody mapped.

0.2.0 — the character's jump and controls belong to the platform

ADR-034 makes the platform the owner of the character, so two things that used to be each game's problem moved in here.

Jump (jumpSpeed, coyoteTime, jumpBuffer, jumpCut). 0.1.0 left jump to the game on the grounds that how it feels is a design decision. That is true right up until every game shares one character — then a badly-tuned jump is badly tuned everywhere. update(dt, dir, { jump }) takes the button's CURRENT state and the controller owns the timing: coyote time so a press a few frames late still counts, buffering so a press slightly early is not lost, and a cut on release so a tap hops and a hold arcs.

The cut applies once, on release. Applying it every airborne frame compounds (×0.45, ×0.2, ×0.09…) and swallows the jump entirely — caught by the release-before-landing test, not by reading the code.

state (idle | walk | jump | fall) so animation follows what the character is DOING rather than what was pressed; deriving it from input leaves a character walking in mid-air. teleport() is the respawn primitive, and it clears vertical velocity — without that, a character rescued from a long drop arrives still travelling at the speed it fell.

Touch (Input3D({ touch }), on by default for coarse-pointer devices). An on-screen thumbstick and jump button, merged into the same direction() and jump a keyboard feeds, so no game branches on input source. Before this, 3D was simply unplayable on a phone.

The controls mount to <body>, not the game's #hud: a game that writes hud.textContent = '...' wipes every child, and the controls vanish with no error at all. That happened. The HUD belongs to the game; this layer belongs to the platform.

What is deliberately missing

No character controller (Rapier's KinematicCharacterController is the intended basis), no input, no audio, no editor, no HUD, no asset pipeline integration, no published build. Those are the next slice's scope, and pretending otherwise would be a worse lie than the gap.

ThreeUmicat.dialogue has no default renderer: a game passes opts.renderer or it throws. A 3D dialogue box is a design question nobody has answered, and shipping a broken default would be worse than requiring an explicit one.