meshpad 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,222 @@
1
- # meshpad
1
+ # Meshpad
2
2
 
3
- Upcoming project. This initial package does not provide functionality yet.
3
+ Build mobile-first 3D game prototypes in one TypeScript file. Meshpad includes TypeScript, Three.js, Vite, a portrait game shell, a HUD, level navigation, win/fail screens, and headless PNG rendering.
4
+
5
+ Requires Node.js 22.12 or newer and a WebGL 2 browser.
6
+
7
+ ```sh
8
+ npx meshpad init my-game
9
+ cd my-game
10
+ npm install
11
+ npm run dev
12
+ ```
13
+
14
+ The HUD keeps a dropdown-style level chooser on the left, an optional hearts/time/moves counter at the true center, and restart on the right in one row. Rounded buttons, simple SVG icons, Baloo 2, and a purple/cream/yellow palette give it a casual game feel. Win popups offer next level or replay. Fail popups offer retry, plus play on for hearts, time, and moves modes. Baloo 2 loads from Google Fonts, with rounded system fonts as a fallback.
15
+
16
+ The generated project has one dependency, `meshpad`, and one implementation file, `game.ts`. No HTML, CSS, Vite config, or separate TypeScript or Three.js install is required. `package.json`, `tsconfig.json`, `.gitignore`, and `vercel.json` are provided. The starter is a three-level tap-the-cube game with a countdown.
17
+
18
+ For an empty directory:
19
+
20
+ ```sh
21
+ npx meshpad init
22
+ npm install
23
+ ```
24
+
25
+ If a `package.json` already exists, install `meshpad`, add `game.ts`, and run `npx meshpad dev`. Initialization refuses to overwrite existing files.
26
+
27
+ ```ts
28
+ import { createGame, THREE } from 'meshpad';
29
+
30
+ createGame({
31
+ title: 'My game',
32
+ levels: [{
33
+ name: 'Hello, world',
34
+ setup({ scene, win, fail }) {
35
+ const cube = new THREE.Mesh(
36
+ new THREE.BoxGeometry(),
37
+ new THREE.MeshStandardMaterial({ color: '#f9c203' }),
38
+ );
39
+ scene.add(cube);
40
+ return {
41
+ update(dt, elapsed) {
42
+ cube.rotation.y += dt;
43
+ if (elapsed >= 10) fail();
44
+ },
45
+ pointer({ type, hits }) {
46
+ if (type === 'pointerdown' && hits([cube]).length) win();
47
+ },
48
+ };
49
+ },
50
+ }],
51
+ });
52
+ ```
53
+
54
+ `THREE` is the included Three.js namespace. Named imports are also available from `meshpad/three`. Three.js's add-on barrel is available through `meshpad/addons`, with the same exports and no extra dependency:
55
+
56
+ ```ts
57
+ import { BufferGeometryUtils, OrbitControls, GLTFLoader } from 'meshpad/addons';
58
+ const geometry = BufferGeometryUtils.mergeVertices(new THREE.BoxGeometry());
59
+ ```
60
+
61
+ ### TypeScript projects
62
+
63
+ Game entries must be `.ts` files; JavaScript and declaration-only entries are rejected. `game.ts` is the default for dev, build, rendering, and type checking. Custom entries such as `src/main.ts` also work. Builds and renders check types before starting, and development checks types at startup and after edits, displaying errors in the terminal and browser.
64
+
65
+ ```sh
66
+ npx meshpad typecheck
67
+ # or in generated projects:
68
+ npm run typecheck
69
+ ```
70
+
71
+ The generated `tsconfig.json` extends `meshpad/tsconfig`, which provides strict checking, DOM types, bundler module resolution, and no JavaScript emission. Editors use the same preset. With no project config, the CLI uses these defaults. Checks cover the project's TypeScript files and the entry's imports; type errors stop production builds. Meshpad bundles the compiler, so projects keep a single dependency. For separate setup helpers, import `type LevelContext` and `type LevelHooks` from `meshpad`.
72
+
73
+ ### Game API
74
+
75
+ `createGame({ levels, title?, root?, initialLevel?, failure?, background?, maxPixelRatio?, maxWidth?, renderer?, onError? })` returns a game. Level indexes in JavaScript start at **0**. The CLI's `--level` starts at **1**.
76
+
77
+ Each level is `{ name?, difficulty?, failure?, setup(context) }`. `difficulty` accepts `normal` (the default), `hard`, `very-hard`, or `ultra-hard`. The full-screen, scrollable level chooser shows each level’s number, optional name, and difficulty badge, with an X button to close it. Initial setup starts in a microtask, after the entry module's synchronous code has run, so it can use classes and constants declared below `createGame()`. Setup may be asynchronous. Context provides:
78
+
79
+ | Value | Purpose |
80
+ | --- | --- |
81
+ | `scene`, `camera`, `renderer` | Fresh scene, perspective camera, and shared renderer. Default lighting is included. |
82
+ | `level`, `index` | Current level and index. |
83
+ | `width`, `height` | Current stage dimensions in CSS pixels. |
84
+ | `win()`, `fail()` | Finish the level and display a result popup. `fail()` always fails immediately. Repeated results are ignored. |
85
+ | `mistake()`, `move()` | Lose one heart or spend one move in the matching mode. Returns `false` in other modes, outside active play, or from an old level. |
86
+ | `failure` | Read-only resource snapshot: `type`, `initial`, `remaining`, and `playOnAmount`. |
87
+ | `signal` | Aborted on level exit. Use for fetches and event listeners. |
88
+
89
+ Setup can return the following optional hooks and settings:
90
+
91
+ | Hook or setting | Purpose |
92
+ | --- | --- |
93
+ | `update(dt, elapsed)` | Advance gameplay; time values are seconds. |
94
+ | `pointer(event)` | Handle input with `type`, `originalEvent`, normalized `point`, `raycaster`, and recursive `hits(objects?)`. The canvas captures pointers for dragging and forwards cancellation. |
95
+ | `resize(width, height)` | Update layout or a custom camera using stage dimensions in CSS pixels. |
96
+ | `beforeRender()` | Run a shadow map or other pre-pass immediately before every draw, including resize, paused frames, and `step(0)`. Restore any renderer state changed by the pass. |
97
+ | `debug()` | Return JSON-serializable game state for `game.debug()` and headless JSON. Keep this hook free of side effects; inspection may call it repeatedly. |
98
+ | `camera` | Supply a Three.js camera, including an orthographic camera. Meshpad leaves its projection alone. |
99
+ | `autoResizeCamera` | Set `false` to preserve the default camera's custom projection, or `true` to update a supplied perspective camera's aspect automatically. |
100
+ | `onPlayOn()` | Optional recovery hook after the resource is restored. The scene, elapsed time, and level signal are preserved. Hook errors show retry. |
101
+ | `dispose()` | Release non-scene resources. |
102
+
103
+ For example, return `{ camera: myOrthographicCamera, resize(width, height) { /* update its frustum */ }, debug: () => ({ moves, grabbed }) }`. The default perspective camera remains available in setup's context; `game.camera` exposes the active camera after setup.
104
+
105
+ The game exposes `ready`, `loadLevel(index)`, `restart()`, `next()`, `previous()`, `win()`, `fail()`, `mistake()`, `move()`, `playOn()`, `pause()`, `resume()`, `step(seconds)`, `debug()`, and `dispose()`, plus `scene`, `camera`, `renderer`, and read-only `state`. Await `game.ready` before interacting programmatically; `scene` and `camera` are initially undefined. `debug()` returns `null` without a debug hook. Loading returns a promise and resolves `false` if a newer load supersedes it. An explicit load or disposal before initial setup supersedes the deferred load as well. Navigation stops at the first/last level. `state.status` is `loading`, `playing`, `paused`, `won`, `failed`, `error`, or `disposed`.
106
+
107
+ Each level owns its scene: Meshpad disposes geometry, materials, textures, skeletons, and shadows on exit. Avoid sharing these resources across levels. Use `dispose()` for controls, timers, audio, or render targets. Use `signal` for listeners (`element.addEventListener('click', handler, { signal })`) and async work; check `signal.aborted` after awaiting work that cannot be cancelled. Old level callbacks cannot alter the active level. Setup failures reject the load promise and show a retry screen; runtime hook failures show the same screen and call `onError`.
108
+
109
+ ### Failure modes
110
+
111
+ Try all four HUDs with `node bin/meshpad.js dev examples/failure-modes/game.ts` in this repository. Tap the cube to progress; in this demo, missed taps trigger mistakes or consume moves.
112
+
113
+ Set `failure` on the game, or override it on an individual level. Omitting it preserves the existing direct fail/retry behavior.
114
+
115
+ | Mode | Configuration | Failure trigger | Play on |
116
+ | --- | --- | --- | --- |
117
+ | Default | `{ type: 'default' }` | `fail()` | Unavailable; retry only |
118
+ | Hearts | `{ type: 'hearts', hearts: 3 }` | Each `mistake()` loses one heart; zero fails | Refill all hearts |
119
+ | Time | `{ type: 'time', seconds: 90, playOnSeconds: 20 }` | Countdown reaches zero | Add 20 seconds, configurable |
120
+ | Moves | `{ type: 'moves', moves: 25, playOnMoves: 10 }` | Each `move()` spends one move; zero fails | Add 10 moves, configurable |
121
+
122
+ Hearts default to 3 and support 3, 4, or 5. Time and moves require a positive starting value. Move counts and bonuses must be positive safe integers; seconds and time bonuses must be positive finite numbers. Invalid settings throw before a renderer is created.
123
+
124
+ ```ts
125
+ createGame({
126
+ failure: { type: 'hearts', hearts: 4 },
127
+ levels: [
128
+ { setup({ mistake }) { /* Call mistake() for each incorrect action. */ } },
129
+ { difficulty: 'hard', failure: { type: 'time', seconds: 60, playOnSeconds: 30 }, setup() {} },
130
+ { failure: { type: 'moves', moves: 15 }, setup({ move, win }) {
131
+ // After applying an accepted move: if solved, win(); otherwise, move().
132
+ // Checking the win first lets the final available move complete the level.
133
+ } },
134
+ { failure: { type: 'default' }, setup({ fail }) { /* Direct fail/retry. */ } },
135
+ ],
136
+ });
137
+ ```
138
+
139
+ The timer is automatic and displays remaining seconds rounded up as `MM:SS`. It advances with simulation time, including `step()`, and pauses while gameplay is paused, the level list is open, or the tab is hidden. Hearts show filled and empty heart icons; moves show a box labeled **Moves** above the count. Every variant keeps the level index and the same navigation controls. The dropdown-style level label opens a scrollable list of level rows with difficulty badges and allows selecting any level; closing it restores the prior pause state. Programmatic `next()` and `previous()` remain available.
140
+
141
+ `game.state.failure` and `context.failure` expose immutable snapshots. Resource fields are `null` in default mode. On failure, **Play on** restores hearts or adds the configured time/moves without rerunning setup, resetting elapsed time, or replacing the board. **Retry** and the HUD restart button start the level fresh. Play on can be used after each failure and returns `false` unless the current level has failed in a resource mode. Win and error screens never offer play on.
142
+
143
+ ### Mobile defaults
144
+
145
+ The stage fills the available height and stays portrait, centered on wider screens. The default maximum width is 540px and pixel ratio is capped at 2. Included styles handle dynamic viewport height, safe-area insets, fixed positioning, overscroll, text selection, tap highlights, and touch gestures. Viewport resizing follows `visualViewport`, orientation, and page restoration. Rendering sleeps in hidden tabs, clamps frame deltas after interruptions, and resumes after WebGL context restoration. `pause()` remains in effect across tab changes.
146
+
147
+ The generated HTML includes mobile viewport and standalone-web-app metadata. The default camera updates its aspect automatically; custom cameras own their projection and can use `resize` to update it. Styles apply a full-page game layout; pass `root` to mount in a specific element with its own dimensions.
148
+
149
+ ### Commands
150
+
151
+ ```sh
152
+ meshpad dev [game.ts] --host 0.0.0.0 --port 5173
153
+ meshpad build [game.ts] --out-dir dist
154
+ meshpad typecheck [game.ts]
155
+ meshpad preview --port 4173
156
+ meshpad --help
157
+ ```
158
+
159
+ Put static assets in `public/` and load them using paths such as `/models/piece.glb`. Imported assets and `new URL('./asset.png', import.meta.url)` work through Vite. Builds target Safari 15+ and Chrome 96+ with WebGL 2. Development on a phone uses your computer's LAN address and `--host 0.0.0.0`.
160
+
161
+ ### Headless rendering
162
+
163
+ Install the bundled Playwright Chromium browser once:
164
+
165
+ ```sh
166
+ npx meshpad browsers
167
+ # Linux CI machines can also install system dependencies:
168
+ npx meshpad browsers --with-deps
169
+
170
+ npx meshpad render game.ts --output renders/level.png \
171
+ --width 390 --height 844 --level 1 --time 1.5
172
+ ```
173
+
174
+ The renderer starts a local Vite server, loads the actual game in headless Chromium with software WebGL, waits for setup, advances simulation in fixed substeps, and saves a PNG plus adjacent JSON containing `state`, `debug`, DPR, and draw statistics. Animation starts at time zero; `--time` advances game hooks up to 60 seconds. Keep randomness seeded and animations in `update` for reproducible captures. Browser timers, external services, and asynchronously loaded assets outside setup are not deterministic; await required assets in setup. `--timeout` bounds setup (default 30000ms). `--json` also prints the result to stdout for other tools.
175
+
176
+ `--scene-only` omits the HUD. `--dpr 2` doubles PNG dimensions and drawing-buffer resolution without changing the layout; capture DPR ranges from 0.25 to 4 and overrides the game's pixel-ratio cap. The output captures the portrait stage within the requested viewport; on a wide viewport the PNG is narrower than the viewport. Rendering needs Chromium locally or in CI; deployed games are static and need no server or browser installation.
177
+
178
+ ### Scripted play
179
+
180
+ `play` uses the same headless runner as `render`, with browser pointer input projected from world positions. Write `steps.json` as an array:
181
+
182
+ ```json
183
+ [
184
+ { "type": "pointerdown", "world": [-1, 0, 0] },
185
+ { "type": "state", "label": "grabbed" },
186
+ { "type": "pointermove", "world": [1, 0, 0] },
187
+ { "type": "advance", "seconds": 0.5 },
188
+ { "type": "screenshot", "output": "renders/dragging.png" },
189
+ { "type": "pointerup" },
190
+ { "type": "state", "label": "released" }
191
+ ]
192
+ ```
193
+
194
+ ```sh
195
+ meshpad play game.ts --level 2 --script steps.json --dpr 2 \
196
+ --output renders/final.png
197
+ ```
198
+
199
+ `play` requires `--script`; `render --script steps.json` also works. Both accept the same capture options and always save a final PNG and adjacent JSON. `play` prints the full result JSON, including labeled state and screenshot `snapshots`, so tests can assert on `debug` and `state.status`.
200
+
201
+ Pointer steps use a single left mouse pointer. `world` is `[x, y, z]`, projected through the active camera; `pointerup` may omit it to release at the current position. Down/up with a world position first moves the pointer there, emitting normal browser move events. World positions target the canvas within the portrait stage, and HUD controls can intercept input as they do during manual play. Use `--scene-only` to hide the HUD for unobstructed input. A move is instantaneous; combine moves and `advance` steps for a timed drag. Each advance accepts 0–60 seconds and runs fixed substeps; gameplay stops advancing on win, failure, or pause.
202
+
203
+ Screenshot paths inside the script are relative to the script file; CLI `--output` is relative to the working directory. Each screenshot gets adjacent JSON. The whole script is validated before launch, and runtime errors identify the failing step. Captures with a failing setup, frame hook, or non-serializable debug snapshot exit with a nonzero status.
204
+
205
+ ### Deploy
206
+
207
+ ```sh
208
+ npm run build
209
+ ```
210
+
211
+ Deploy the generated project to Vercel using the included `vercel.json`, which sets `npm run build` and `dist`. The output is a static site and can also be hosted anywhere that serves `dist/`. The toolkit repository itself builds its hello-world example with the same commands.
212
+
213
+ ### Development
4
214
 
5
215
  ```sh
6
- npm install meshpad
216
+ npm install
217
+ npm run typecheck
218
+ npm test
219
+ npm run test:browser # requires meshpad browsers first
220
+ npm run dev
221
+ npm run render -- --time 1
7
222
  ```
package/addons.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from 'three/addons';
package/bin/meshpad.js ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { spawn } from 'node:child_process';
4
+ import { createRequire } from 'node:module';
5
+ import { dirname, join } from 'node:path';
6
+ import { buildProject, initProject, manifest, previewProject, startDev } from '../src/tooling.js';
7
+ import { checkProject } from '../src/typecheck.js';
8
+
9
+ const help = `Meshpad · single-file 3D game prototypes
10
+
11
+ Usage:
12
+ meshpad init [directory] Create a TypeScript hello-world project
13
+ meshpad dev [game.ts] Start Vite with live type checking
14
+ meshpad build [game.ts] Type check and build into dist/
15
+ meshpad typecheck [game.ts] Check project types without building
16
+ meshpad preview Serve a production build locally
17
+ meshpad browsers [--with-deps] Install Chromium for rendering
18
+ meshpad render [game.ts] Type check and render to PNG + JSON
19
+ meshpad play [game.ts] --script <steps.json>
20
+ Run scripted input; print capture JSON
21
+
22
+ Options:
23
+ --host <host> Dev/preview host (default: 127.0.0.1)
24
+ --port <number> Dev/preview port
25
+ --out-dir <path> Build/preview output directory
26
+ --output, -o <path.png> Render output (default: renders/game.png)
27
+ --width <pixels> Render viewport width (default: 390)
28
+ --height <pixels> Render viewport height (default: 844)
29
+ --dpr <number> Capture pixel ratio, 0.25–4 (default: 1)
30
+ --script <steps.json> Pointer, advance, screenshot, and state steps
31
+ --json Print render result as JSON (play default)
32
+ --level <number> Render level, starting at 1
33
+ --time <seconds> Advance simulation before capture (0–60)
34
+ --timeout <milliseconds> Render setup timeout (default: 30000)
35
+ --scene-only Capture without the HUD
36
+ --help, -h Show help
37
+ --version, -v Show version
38
+ `;
39
+
40
+ try {
41
+ const { values, positionals } = parseArgs({
42
+ allowPositionals: true,
43
+ options: {
44
+ help: { type: 'boolean', short: 'h' }, version: { type: 'boolean', short: 'v' },
45
+ host: { type: 'string' }, port: { type: 'string' }, 'out-dir': { type: 'string' },
46
+ output: { type: 'string', short: 'o' }, width: { type: 'string' }, height: { type: 'string' },
47
+ level: { type: 'string' }, time: { type: 'string' }, timeout: { type: 'string' },
48
+ dpr: { type: 'string' }, script: { type: 'string' }, json: { type: 'boolean' },
49
+ 'scene-only': { type: 'boolean' }, 'with-deps': { type: 'boolean' },
50
+ },
51
+ });
52
+ const [command = 'dev', entry, ...extra] = positionals;
53
+ if (values.version) console.log(manifest.version);
54
+ else if (values.help) console.log(help);
55
+ else {
56
+ if (extra.length || (entry && ['preview', 'browsers'].includes(command))) throw new Error('Too many arguments. Run "meshpad --help".');
57
+ const options = { host: values.host, outDir: values['out-dir'] };
58
+ if (values.port != null) {
59
+ options.port = Number(values.port);
60
+ if (!Number.isInteger(options.port) || options.port < 0 || options.port > 65535) throw new Error('port must be an integer from 0 to 65535.');
61
+ }
62
+ switch (command) {
63
+ case 'init': {
64
+ const directory = await initProject(entry);
65
+ console.log(`Created ${directory}\n\nNext: cd ${JSON.stringify(directory)}\n npm install\n npm run dev`);
66
+ break;
67
+ }
68
+ case 'dev': {
69
+ const server = await startDev(entry, options);
70
+ server.printUrls();
71
+ break;
72
+ }
73
+ case 'build': await buildProject(entry, options); break;
74
+ case 'typecheck': {
75
+ await checkProject(entry);
76
+ console.log('TypeScript check passed.');
77
+ break;
78
+ }
79
+ case 'preview': {
80
+ const server = await previewProject(options);
81
+ server.printUrls();
82
+ break;
83
+ }
84
+ case 'browsers': {
85
+ const require = createRequire(import.meta.url);
86
+ const cli = join(dirname(require.resolve('playwright/package.json')), 'cli.js');
87
+ const child = spawn(process.execPath, [cli, 'install', 'chromium', ...(values['with-deps'] ? ['--with-deps'] : [])], { stdio: 'inherit' });
88
+ process.exitCode = await new Promise((resolve, reject) => { child.on('error', reject); child.on('exit', (code) => resolve(code ?? 1)); });
89
+ break;
90
+ }
91
+ case 'render': case 'play': {
92
+ if (command === 'play' && !values.script) throw new Error('play requires --script <steps.json>.');
93
+ const { captureGame } = await import('../src/capture.js');
94
+ const renderOptions = { output: values.output, sceneOnly: values['scene-only'], script: values.script };
95
+ for (const name of ['width', 'height', 'level', 'time', 'timeout', 'dpr']) if (values[name] != null) renderOptions[name] = Number(values[name]);
96
+ const result = await captureGame(entry, renderOptions);
97
+ if (command === 'play' || values.json) console.log(JSON.stringify(result, null, 2));
98
+ else console.log(`Rendered level ${result.level} → ${result.output}\n${result.three.calls} draw calls · ${result.three.triangles} triangles`);
99
+ break;
100
+ }
101
+ default: throw new Error(`Unknown command: ${command}. Run "meshpad --help".`);
102
+ }
103
+ }
104
+ } catch (error) {
105
+ console.error(`meshpad: ${error.message}`);
106
+ process.exitCode = 1;
107
+ }
package/index.d.ts ADDED
@@ -0,0 +1,133 @@
1
+ /// <reference types="vite/client" />
2
+ import type * as Three from 'three';
3
+ export * as THREE from 'three';
4
+
5
+ export interface GamePointer {
6
+ type: 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel';
7
+ originalEvent: PointerEvent;
8
+ /** Normalized device coordinates, from -1 to 1. */
9
+ point: Three.Vector2;
10
+ raycaster: Three.Raycaster;
11
+ hits(objects?: Three.Object3D[]): Three.Intersection[];
12
+ }
13
+
14
+ export interface LevelHooks {
15
+ /** Runs after play on restores the resource, without setting up the level again. */
16
+ onPlayOn?(): void;
17
+ /** Replace the default camera. Custom cameras own their projection by default. */
18
+ camera?: Three.Camera;
19
+ /** Automatically update a perspective camera's aspect on resize. */
20
+ autoResizeCamera?: boolean;
21
+ /** Seconds since the last frame and since this level started. */
22
+ update?(dt: number, elapsed: number): void;
23
+ /** Runs immediately before every draw, including resize and step(0). */
24
+ beforeRender?(): void;
25
+ /** Return JSON-serializable state for inspection and headless captures. */
26
+ debug?(): unknown;
27
+ pointer?(event: GamePointer): void;
28
+ /** Use to update an orthographic camera or custom layout. */
29
+ resize?(width: number, height: number): void;
30
+ /** Dispose controls, audio, timers, or other non-scene resources. */
31
+ dispose?(): void;
32
+ }
33
+
34
+ export interface LevelContext {
35
+ scene: Three.Scene;
36
+ camera: Three.PerspectiveCamera;
37
+ renderer: Three.WebGLRenderer;
38
+ level: Level;
39
+ /** Zero-based level index. */
40
+ index: number;
41
+ /** Aborted on level exit, setup failure, or game disposal. */
42
+ signal: AbortSignal;
43
+ readonly width: number;
44
+ readonly height: number;
45
+ readonly failure: Readonly<FailureState>;
46
+ win(): boolean;
47
+ /** Fail immediately, regardless of the configured mode. */
48
+ fail(): boolean;
49
+ /** Lose one heart. Ignored outside hearts mode or active play. */
50
+ mistake(): boolean;
51
+ /** Spend one move. Check for a win before spending the final move. */
52
+ move(): boolean;
53
+ }
54
+
55
+ export type FailureMode =
56
+ | { type: 'default' }
57
+ | { type: 'hearts'; hearts?: 3 | 4 | 5 }
58
+ | { type: 'time'; seconds: number; playOnSeconds?: number }
59
+ | { type: 'moves'; moves: number; playOnMoves?: number };
60
+
61
+ export interface FailureState {
62
+ readonly type: FailureMode['type'];
63
+ /** Null in default mode; seconds may be fractional. */
64
+ readonly remaining: number | null;
65
+ readonly initial: number | null;
66
+ readonly playOnAmount: number | null;
67
+ }
68
+
69
+ export type LevelDifficulty = 'normal' | 'hard' | 'very-hard' | 'ultra-hard';
70
+
71
+ export interface Level {
72
+ name?: string;
73
+ /** Displayed in the level chooser. Defaults to normal. */
74
+ difficulty?: LevelDifficulty;
75
+ /** Overrides the game's failure mode for this level. */
76
+ failure?: FailureMode;
77
+ setup(context: LevelContext): LevelHooks | void | Promise<LevelHooks | void>;
78
+ }
79
+
80
+ export interface GameOptions {
81
+ levels: Level[];
82
+ /** Defaults to { type: 'default' }, with retry and no play on. */
83
+ failure?: FailureMode;
84
+ title?: string;
85
+ /** Element or selector. Defaults to document.body. */
86
+ root?: HTMLElement | string;
87
+ /** Zero-based index. Defaults to 0. */
88
+ initialLevel?: number;
89
+ background?: Three.ColorRepresentation;
90
+ /** Defaults to 2. */
91
+ maxPixelRatio?: number;
92
+ /** Portrait stage width cap in CSS pixels. Defaults to 540. */
93
+ maxWidth?: number;
94
+ renderer?: Three.WebGLRendererParameters;
95
+ onError?(error: Error): void;
96
+ }
97
+
98
+ export interface GameState {
99
+ readonly status: 'loading' | 'playing' | 'paused' | 'won' | 'failed' | 'error' | 'disposed';
100
+ readonly index: number;
101
+ readonly elapsed: number;
102
+ readonly error: Error | null;
103
+ readonly failure: Readonly<FailureState>;
104
+ }
105
+
106
+ export interface Game {
107
+ readonly renderer: Three.WebGLRenderer;
108
+ readonly scene: Three.Scene | undefined;
109
+ readonly camera: Three.Camera | undefined;
110
+ readonly state: GameState;
111
+ /** Initial setup starts in a microtask. Rejects on setup failure. */
112
+ ready: Promise<boolean>;
113
+ /** Current level's debug snapshot, or null when no debug hook is available. */
114
+ debug(): unknown;
115
+ /** Resolves false if superseded by another load or disposal. */
116
+ loadLevel(index: number): Promise<boolean>;
117
+ restart(): Promise<boolean>;
118
+ next(): Promise<boolean>;
119
+ previous(): Promise<boolean>;
120
+ win(): boolean;
121
+ fail(): boolean;
122
+ mistake(): boolean;
123
+ move(): boolean;
124
+ /** Continue a failed non-default level, preserving its scene and elapsed time. */
125
+ playOn(): boolean;
126
+ pause(): void;
127
+ resume(): void;
128
+ /** Advance up to 60 seconds in fixed substeps, then draw. */
129
+ step(seconds?: number): void;
130
+ dispose(): void;
131
+ }
132
+
133
+ export function createGame(options: GameOptions): Game;
package/index.js CHANGED
@@ -1,3 +1,3 @@
1
- "use strict";
2
-
3
- module.exports = {};
1
+ export { createGame } from './src/game.js';
2
+ export * as THREE from 'three';
3
+ import './src/styles.css';
package/package.json CHANGED
@@ -1,16 +1,74 @@
1
1
  {
2
2
  "name": "meshpad",
3
- "version": "0.0.0",
4
- "description": "Meshpad — upcoming project.",
3
+ "version": "0.1.0",
4
+ "description": "A single-file toolkit for mobile-first Three.js game prototypes.",
5
+ "type": "module",
5
6
  "main": "index.js",
7
+ "types": "index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "import": "./index.js"
12
+ },
13
+ "./three": {
14
+ "types": "./three.d.ts",
15
+ "import": "./src/three.js"
16
+ },
17
+ "./addons": {
18
+ "types": "./addons.d.ts",
19
+ "import": "./src/addons.js"
20
+ },
21
+ "./package.json": "./package.json",
22
+ "./tsconfig": "./tsconfig.json"
23
+ },
24
+ "bin": {
25
+ "meshpad": "./bin/meshpad.js"
26
+ },
27
+ "engines": {
28
+ "node": ">=22.12.0"
29
+ },
30
+ "scripts": {
31
+ "dev": "node bin/meshpad.js dev examples/hello-world/game.ts",
32
+ "build": "node bin/meshpad.js build examples/hello-world/game.ts",
33
+ "preview": "node bin/meshpad.js preview",
34
+ "render": "node bin/meshpad.js render examples/hello-world/game.ts",
35
+ "typecheck": "node bin/meshpad.js typecheck examples/hello-world/game.ts",
36
+ "test": "node --test test/*.test.js",
37
+ "test:browser": "node --test test/browser/*.test.js"
38
+ },
39
+ "dependencies": {
40
+ "@types/three": "^0.185.0",
41
+ "playwright": "^1.64.1",
42
+ "three": "^0.187.0",
43
+ "vite": "^8.3.6",
44
+ "typescript": "~6.0.3"
45
+ },
46
+ "sideEffects": [
47
+ "./index.js",
48
+ "**/*.css"
49
+ ],
6
50
  "files": [
7
- "index.js"
51
+ "index.js",
52
+ "index.d.ts",
53
+ "three.d.ts",
54
+ "addons.d.ts",
55
+ "tsconfig.json",
56
+ "src",
57
+ "bin",
58
+ "templates"
8
59
  ],
9
60
  "repository": {
10
61
  "type": "git",
11
62
  "url": "git+https://github.com/havaliza/meshpad.git"
12
63
  },
13
- "keywords": [],
64
+ "keywords": [
65
+ "three",
66
+ "vite",
67
+ "game",
68
+ "prototype",
69
+ "mobile",
70
+ "3d"
71
+ ],
14
72
  "author": "",
15
73
  "license": "ISC"
16
74
  }
package/src/addons.js ADDED
@@ -0,0 +1 @@
1
+ export * from 'three/addons';