Stack and tools
This page answers the question "what is this made with?" — and it answers with the declared
version, not the remembered one. The table below is generated by node tools/gen-docs.mjs
from package.json, docs/package.json and the vendored Three.js itself.
| Layer | Tool | Version |
|---|---|---|
| 3D engine (WebGL) | Three.js, vendored | r160 |
| Game | vanilla ES modules, zero build | 44 files |
| Site | Astro with SSR | ^7.1.1 |
| Hosting | Vercel adapter | ^11.0.6 |
| Database | managed Postgres (RLS; private schema) | ^2.110.7 |
| Browser checks | Playwright | ^1.62.1 |
| GLB pipeline | gltf-transform | ^4.4.1 |
| Mesh compression | meshoptimizer | ^1.2.0 |
| Images | sharp · resvg | ^0.35.3 · ^2.6.2 |
| This documentation | Docusaurus | 3.6.3 |
| CI runtime | Node | 22 |
Three.js comes from public/vendor/three.module.js. Of the scripts in tools/, 109 import Playwright, 37 import gltf-transform, and 4 import meshoptimizer.
Block generated by
node tools/gen-docs.mjs. Source:dependencies/devDependencies do package.json · REVISION de public/vendor/three.module.js
The two zones, and why the boundary is hard
The repository has two applications with opposite rules, and almost every misunderstanding from newcomers is born from treating them as one.
public/ — the GAME: Three.js, WebGL, zero build
The game is vanilla JavaScript with ES modules served raw. There is no bundler, no
transpiler, no build step. The browser downloads public/js/game.js exactly as it is in the
repository.
This is a design decision, not laziness, and it pays off in three places:
- The game runs by dragging the folder onto any static host. It does not depend on Astro, does not depend on Vercel, does not depend on npm at runtime. It is what makes it viable to ship on a portal (CrazyGames, itch) without rewriting anything.
- The harness can boot the
Gameclass in pure node.tools/eval/harness.mjsimports the production code with DOM and canvas stubbed, and measures the real game in seconds. A bundler in the middle would break that — and without that there is no gate. node --checkon each file is a complete syntax test (npm run syntax), because the file node parses is byte for byte what the browser executes.
The price, which is also real: cache. Without a build there is no hash in the filename, so
invalidation is manual — the import map's ?v=. The rule and what it has already cost are in
Getting started, in one place only.
Three.js is vendored at public/vendor/three.module.js (plus vendor/addons/).
No CDN and no runtime dependency: the import map points to the local file. Do not
add a CDN or a runtime package without opening an issue.
WebGL is the target, and a weak machine is a requirement. There is a quality: 'low' path with no
post-processing and a querystring kill-switch for every risky change (?bloom=0,
?ao=0, ?fxaa=0, ?water=0). Every graphics change that requires extra rendering has to
declare its measured cost.
src/ — the SITE: Astro with SSR on Vercel
The site is Astro with the Vercel adapter. astro.config.mjs is set to
output: 'static' with adapter, and the routes that need a server opt in with
export const prerender = false one by one — the case of /ranking, /u/*,
/sitemap.xml and all the /api/* routes.
Here a framework is welcome. The rules that apply:
- the Supabase
service_rolelives server-side only and never reaches the browser; siteinastro.config.mjsis withwww, and every canonical comes from there;vercel.jsoncarries the security headers (CSP, HSTS, nosniff, Referrer-Policy, Permissions-Policy) and the CDN cache.
And the gotcha that costs everyone their first hour: src/pages/index.astro IS the
game, served at the / route. There is no public/index.html.
Database — managed Postgres, RLS and telemetry
The ranking and the telemetry live in a managed Postgres. Schema and migrations are private (outside the repo — a security decision); the runtime only uses the envs. optional obfuscation that was delivered ready and deliberately not applied.
Security does not come from hiding the anon key — it is public by design, although the current
client does not receive it. It comes from the policies, from the per-column grants and from the rate limit counted
in Postgres (src/lib/ratelimit.ts + RPC rl_take), not in lambda memory.
Player identity uses a stable UID to select the account and a token to authenticate
the session; the nickname is display data. Old clients and a database pending the
private migration retain a temporary nickname + token fallback.
The ranking is off today (RANKING_ON in src/lib/site.ts) and was replaced by
anonymous telemetry. It is a flag, not a removal — details in Current state.
Without the Supabase variables the site boots the same: the ranking routes respond
503 not_configured and the pages show the notice. The game in public/ uses none of
them. See .env.example.
Asset generation — what is AI-generated, and by which service
Almost every 3D and 2D asset in this game is generated, not hand-modeled. The real flow, not the hypothetical one:
| Service | What it generates | Script | Key |
|---|---|---|---|
| mint.gg (Mint MCP) | rigged characters, packs, animation | MCP tools; mint-assets.json records the result | owner account via MCP |
| Tripo3D | 3D props from text | tools/gen-asset.mjs --provider tripo | TRIPO_API_KEY |
| Meshy | 3D props and rigging | tools/gen-asset.mjs --provider meshy | MESHY_API_KEY |
| OpenRouter | 2D art | tools/gen-image.mjs | OPENROUTER_API_KEY |
mint-assets.json records 7 assets generated through Mint (3 mint-model · 4 mint-asset-pack).
API keys live in the gitignored root .env; generation is offline and the game runs without them.
Block generated by
node tools/gen-docs.mjs. Source:git grep -l SDK -- tools/ | grep .mjs · mint-assets.json
Characters: mint.gg
The playable characters are rigged GLBs generated by Mint (mint.gg), through the MCP
tools — start_model_generation with riggable_character in T-pose and empty hands, then
animate_generated_model to come out with a skeleton.
Two non-obvious facts that save money and rounds:
- Mint's base model does not come rigged. The skeleton only appears at the animation step.
The cheap path for a new character is: generate the base → rig it with one clip →
use its
rigged_character_glb→ reuse the shared clips. - Meshy rigs share the same bone names (
Hips,Spine,Head,RightHand…), so a clip pack generated once matches by name on any rig in the family. That is whypublic/models/anims/has shared clips and per-character clips at the same time, and why theindex.jsonmanifest exists (npm run anims) — without it the game requested clips from characters that do not have them and filled the console with 404s.
mint-assets.json is the record of what was generated: assetId, chatUrl and a note on what
went wrong in the previous attempt. Without that record there is no reviewing and no regenerating —
the asset becomes a binary with no provenance sitting in the repository.
3D props: Tripo3D and Meshy
tools/gen-asset.mjs generates a prop from text, downloads the GLB and writes it already optimized into
public/models/props/:
node tools/gen-asset.mjs --prompt "caixa de som de baile" --id caixa_som
node tools/gen-asset.mjs --provider meshy --prompt "carro tunado" --id carro_tunado
node tools/gen-asset.mjs --resume <task_id> --id caixa_som # task already paid for
Tripo is the default; Meshy is the alternative. --face-limit (default 12000), --raw-only to
skip optimization and --timeout complete the options.
Maps do not need this. The registered maps are procedural geometry in Three.js —
street, shack, alley, sidewalk and roundabout are box and plane, which is what map_*.js already does. What
comes from GLB are props.
2D art: OpenRouter
tools/gen-image.mjs is the 2D sibling: it generates faction posters, wallpapers and splash art from text
(+ reference images), and delivers the file already framed and compressed for the box
the screen will draw it in.
The cropping lives in the script on purpose. A faction board is a 245×620 box with
background-size: cover; landscape art enters it showing ~26% of its width — that is
how four cast posters became four portraits of ONE character. The generator
does not offer that aspect ratio, so whoever publishes is who settles the bill: generate at the closest
aspect and center-crop down to the box's real ratio. That way what you look at before
committing is byte for byte what the player sees.
The keys
TRIPO_API_KEY, MESHY_API_KEY and OPENROUTER_API_KEY are read from a .env at the root —
gitignored, mode 600. Three rules the two scripts share, each with a
reason:
- The key never comes from
argv. A command-line argument leaks in thepsof any process on the machine. - The
Authorizationheader only goes out to the API's own host. The finished GLB comes from a third-party CDN (signed link); sending the key along on the download would hand the credential to a host that is not the provider's. There is an allowlist, andredirect: 'error'prevents a 3xx from carrying the header to another domain. - Nothing is printed without going through
redact().
.env.example.env.example covers only Supabase and the audio bundle. The asset-generation keys
exist only in the owner's .env. Anyone who clones and wants to generate assets needs to create them by hand
with the names above — it is documented here and in each script's header, not in the example.
GLB optimization: gltf-transform and meshoptimizer
Every GLB that enters the repository goes through @gltf-transform (dedup, prune,
textureCompress with sharp to WebP) and, on the static path, through meshoptimizer.
The reason is a real ceiling: 250 MB on CrazyGames. A raw character GLB comes in at 4-5 MB, dominated by 2K PNG texture — and the optimization is almost entirely texture, not mesh.
The pipeline scripts live in tools/: optimize-props.mjs, optimize-static.mjs,
optimize-fpvm.mjs, optimize-tribos.mjs, plus the rig ones (rig-from-donor.mjs,
reskin-glb.mjs, retarget-glb.mjs) and the inspection ones (inspect-glb.mjs,
inspect-anim.mjs, bones.mjs).
Playwright — every harness that needs a browser
A ruler (quality gate) that depends on pixels runs in Chromium via Playwright. That is the case of
tools/eval/*-capture.mjs, telas-*.mjs, select-inflate.mjs, crash-watch.mjs and
fv-verify.mjs, among others.
Two things you need to know before running any of them:
- It is expensive. Software rendering (SwiftShader) runs the game at ~0,3 FPS; an in-game
capture costs minutes per map/aspect. That cost is exactly what pushed the gate
to pure node — and it is why the pixel invariants (
PX1–PX4) are skipped, with the reason stated. - One session at a time. Two headless captures in parallel take down the boot and produce a "frozen countdown" that looks like a bug and is load. A single agent runs the browser.
Some harnesses need the server up: npm run eval:serve & first.
Agent skills
This repository versions skills — packaged instructions an agent loads before
working. They live in .agents/skills/, and .claude/skills/ are symlinks to there.
| Count | How many | Meaning |
|---|---|---|
Declared in skills-lock.json | 39 | third-party skills pinned by source and hash |
| Versioned | 10 | content available in a clean clone |
Versioned with SKILL.md | 10 | directly readable instructions |
The counts differ by design: the lock records more third-party skills than the repository vendors.
The house workflow skill, gauntlet-fps, is present locally and does not belong to the third-party lock.
Block generated by
node tools/gen-docs.mjs. Source:git ls-files .agents/skills · skills-lock.json
The vast majority are third-party and cover Three.js (materials, lighting, shaders, post-processing, glTF loading, geometry, animation) and game design. They are optional context: nothing in the game depends on them.
The gauntlet loop
The skill that is not third-party is gauntlet-fps, and it encodes this house's work
cycle:
adversarial critic → builders in parallel → measured capture → A/B verification → regression hunter
When to use: improving, evaluating or reviewing any part of the game — graphics, map fidelity, weapon feel, menu, HUD, bots, movement — and when something is reported as ugly, weird or "doesn't look professional". When not to use: a one-line mechanical task, or a conceptual question that does not touch the game.
The whole cycle — the three rules, the problem each one solves and each one's measured case — has its own page: AI instrumentation. This section exists only to say that the skill exists and when to trigger it.
The documentation
This doc is a separate Docusaurus, in docs/, with its own package.json and its
own node_modules. Nothing here is imported by the game or by the site.
cd docs && npm install && npm start # http://localhost:3000/docs/
cd docs && npm run build # docs/build/
cd docs && npm run build:site # builds INTO public/docs/
baseUrl is /docs/ because the output can be built into public/docs/, and Astro copies
all of public/ into dist/client/.
The tables above come from node tools/gen-docs.mjs and are checked by
npm run docs:check, inside check:fast. The mechanism — what goes into a generated block, what
stays hand-written, and how to paste a new block — is in
Architecture.