@nano133/nanoterra 0.2.0 → 0.3.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/CHANGELOG.md +24 -0
- package/README.md +82 -12
- package/ai/SPEC.md +5 -1
- package/dist/admin/panel/index.d.ts +1 -1
- package/dist/admin/panel/index.js +1 -1
- package/dist/admin/panel/shelf.d.ts +7 -0
- package/dist/admin/panel/shelf.js +24 -0
- package/dist/character/bases/bee.js +7 -5
- package/dist/character/bases/spec.d.ts +7 -0
- package/dist/character/create.d.ts +15 -0
- package/dist/character/create.js +69 -2
- package/dist/character/ctx.d.ts +12 -2
- package/dist/character/index.d.ts +2 -0
- package/dist/character/index.js +2 -0
- package/dist/character/make.js +2 -1
- package/dist/character/preview.d.ts +32 -0
- package/dist/character/preview.js +112 -0
- package/dist/character/sockets.d.ts +37 -0
- package/dist/character/sockets.js +66 -0
- package/dist/model/check.d.ts +59 -0
- package/dist/model/check.js +681 -0
- package/dist/model/index.d.ts +2 -0
- package/dist/model/index.js +5 -0
- package/dist/model/limits.d.ts +103 -0
- package/dist/model/limits.js +132 -0
- package/package.json +3 -2
- package/src/admin/panel/index.ts +1 -1
- package/src/admin/panel/shelf.ts +24 -0
- package/src/admin/panel/style.css +9 -0
- package/src/character/bases/bee.ts +7 -5
- package/src/character/bases/spec.ts +4 -0
- package/src/character/create.ts +73 -2
- package/src/character/ctx.ts +17 -2
- package/src/character/index.ts +2 -0
- package/src/character/make.ts +2 -1
- package/src/character/preview.ts +148 -0
- package/src/character/sockets.ts +81 -0
- package/src/model/check.ts +624 -0
- package/src/model/index.ts +5 -0
- package/src/model/limits.ts +193 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 (2026-10-28)
|
|
4
|
+
|
|
5
|
+
- **Sockets** (`@nano133/nanoterra/character`): three attach points on the character, `c.socket("head" | "hand" | "back")`, each a `THREE.Object3D` on a bone
|
|
6
|
+
(`head`, `grip_R`, `chest`) that follows the walk, the run, the jump and the wave. The head socket sits at the base's hat frame and scales with it and the head's
|
|
7
|
+
size; the hand socket is in the middle of the drawn right hand (a wide build or base draws its hands further out than the bone) and holds a thing pointing
|
|
8
|
+
forward (`HAND_HOLD`); the back socket is on the back of the torso at the chest. A socket is the same object for the character's whole life: a rebuild (a new
|
|
9
|
+
base, hair or clothes) moves it, with what hangs on it, to the new bones. New: `SOCKETS`, `socketFrame`, `placeSocket`, `isSocketName`, `HAND_HOLD`, the types
|
|
10
|
+
`SocketName`, `SocketFrame`, `SocketLook`; `BaseSpec` may set `handAt` and `back`.
|
|
11
|
+
- **A model on a socket**: `WornAdapter` takes an optional `socket` and `object` (and `add` is now optional). `createCharacter({ worn })` and `c.setWorn(worn)`
|
|
12
|
+
hang each object on its socket; a model on a socket builds no geometry, and its `fit`, `hidesBadge`, `hideTail` and the new `hideWings` (a back item takes the Bee's wings off, shapes and bones, as `hideTail` takes a tail off) still apply. The character never frees an
|
|
13
|
+
object that the game hung: `setWorn` takes the old ones off, and `dispose` takes the sockets off the dead bones.
|
|
14
|
+
- **`@nano133/nanoterra/model`**: the 3D model check, moved here from nano133.com (`app/launch/model`, which now keeps the same code). `checkModel(bytes,
|
|
15
|
+
limits)` reads a `.glb` from its bytes, never throws, and answers the problems with a fix each, the facts and, new, **advice** (a large PNG or JPEG, 32-bit
|
|
16
|
+
positions, parts that share a material). New facts: `draws` (each part of each drawn mesh instance), `materials`, `anchor`. New limits: `maxDraws`, and
|
|
17
|
+
`anchor` (a marker whose origin is the attach point: the zone is measured from it). The cosmetic presets of Wes's table (2026-10-28): `HEAD_ITEM`,
|
|
18
|
+
`HAND_ITEM` (its anchor is `grip_R`; `grip_L` is allowed; the zone is 0.96 x 0.68 m, from 1.17 m below the grip to 0.43 m above: Wes's choice A of 2026-10-28, so that a shovel held by its T-handle fits), `BACK_ITEM`, and `COSMETIC_LIMITS` by socket; `ISLAND_MODEL` is the island's (a disc of 10 m
|
|
19
|
+
radius, 18 m high). A misspelt marker with capitals in its name (`Grip_R`) now gets its rename line too. A fix line gives a zone's number with two decimals when one is not exact (1.17 m), and as before otherwise (the island's 20.0 m).
|
|
20
|
+
|
|
21
|
+
- **The Shelf's pictures on the real character**: `createLookPreview` (in `@nano133/nanoterra/character`) draws a look on a character (the game's own
|
|
22
|
+
`body`, else the package's character with its shirt in the look's colour) with one shared offscreen WebGL renderer, one picture at a time, kept by the
|
|
23
|
+
look's text; with no WebGL it answers null. `previewPicture(load)` (in `@nano133/nanoterra/admin/panel`, which still loads no Three.js) is a Shelf
|
|
24
|
+
`lookPicture` that shows the flat picture first and the drawn one when it is ready; `load` is called once, so a game imports its preview file only when
|
|
25
|
+
the Shelf opens. New CSS: `.shelf-preview`.
|
|
26
|
+
|
|
3
27
|
## 0.2.0 (2026-10-27)
|
|
4
28
|
|
|
5
29
|
- **Slots** on a look (`@nano133/nanoterra/looks`, and the Shelf of `/admin/panel`): `slots?: Record<string, string>`, what a game defines for itself (a pair of wings, a trail colour). Names are 1 to 24
|
package/README.md
CHANGED
|
@@ -60,10 +60,11 @@ Each path loads only what it needs. A page that reads a ticket loads no Three.js
|
|
|
60
60
|
| `@nano133/nanoterra/character/record` | the character's **record**: pack, unpack, check, `fromLegacy`, `toLegacy`, `diceRecord`, the lists | no | 4.4 kB (all), 1.9 kB (pack, unpack and the two legacy calls) |
|
|
61
61
|
| `@nano133/nanoterra/ticket` | read and check a sign-in ticket | no | 4.7 kB (all), 3.1 kB (`readTicket` alone) |
|
|
62
62
|
| `@nano133/nanoterra/looks` | the looks ladder | no | 1.9 kB (all), 0.9 kB (`checkLooks` alone) |
|
|
63
|
-
| `@nano133/nanoterra/character` | the builder and `createCharacter` (it includes the record) | yes | 20.4 kB
|
|
63
|
+
| `@nano133/nanoterra/character` | the builder and `createCharacter` (it includes the record), with the three sockets | yes | 21.8 kB (all, with the sockets and `createLookPreview` of 0.3.0; 20.4 kB on 0.2.0), 17.8 kB (`createCharacter` alone, measured on 0.2.0) |
|
|
64
|
+
| `@nano133/nanoterra/model` | the 3D model check: a `.glb` against the island's limits or a cosmetic's (since 0.3.0) | no | 8.8 kB |
|
|
64
65
|
| `@nano133/nanoterra/admin` | the admin of a game: the guard, the handler, the store type (server code; since 0.2.0) | no | 3.7 kB |
|
|
65
66
|
| `@nano133/nanoterra/admin/d1`, `/admin/firestore` | the admin store for D1 (the starter's tables) or Firestore (a small interface, no Firebase dependency): `createD1Store`, `createFirestoreStore` (since 0.2.0) | no | 1.5 kB each |
|
|
66
|
-
| `@nano133/nanoterra/admin/panel` | the admin page, drawn in the browser into one element, and its stylesheet `@nano133/nanoterra/admin/style.css` (since 0.2.0) | no | 8.5 kB + 2.4 kB of CSS |
|
|
67
|
+
| `@nano133/nanoterra/admin/panel` | the admin page, drawn in the browser into one element, and its stylesheet `@nano133/nanoterra/admin/style.css` (since 0.2.0) | no | 10.6 kB on 0.3.0 (8.5 kB measured on 0.2.0) + 2.4 kB of CSS |
|
|
67
68
|
| `@nano133/nanoterra/admin/conformance` | test cases that a game runs against its own admin (a dev dependency in practice; since 0.2.0) | no | n/a |
|
|
68
69
|
|
|
69
70
|
Measured 2026-10-26: `esbuild --bundle --minify --format=esm --external:three`, then `gzip -9`, without Three.js.
|
|
@@ -85,7 +86,7 @@ c.dispose();
|
|
|
85
86
|
|
|
86
87
|
| Call | What it does |
|
|
87
88
|
|---|---|
|
|
88
|
-
| `createCharacter({ record?, look?, colors?, quality?, reducedMotion? })` | Builds a character. `record` is the player's whole character (see "The record"). `look` is the five colours `{ skin, hair, shirt, pants, shoes }` of a player whose ticket has no record: the Classic body in those colours (read only when `record` is left out). Neither: the island's first look. `colors` paints colours over the record's (any `"#rrggbb"`: a shop dye is not on the island's lists). `quality` is `"high"` (default) or `"low"`. Something that is not good throws a `TypeError` that names the part and the value. |
|
|
89
|
+
| `createCharacter({ record?, look?, colors?, quality?, reducedMotion?, worn? })` | Builds a character. `record` is the player's whole character (see "The record"). `look` is the five colours `{ skin, hair, shirt, pants, shoes }` of a player whose ticket has no record: the Classic body in those colours (read only when `record` is left out). Neither: the island's first look. `colors` paints colours over the record's (any `"#rrggbb"`: a shop dye is not on the island's lists). `quality` is `"high"` (default) or `"low"`. Something that is not good throws a `TypeError` that names the part and the value. |
|
|
89
90
|
| `c.object` | A `THREE.Group`: feet at y = 0, facing +Z. Add it to your scene; move it with `position`, turn it with `rotation.y`. It holds the skinned meshes **and the blob shadow** (a disc in the body). |
|
|
90
91
|
| `c.update(dt, { speed, airborne, verticalSpeed })` | One frame. `dt` is in **seconds** (a frame longer than 0.05 s counts as 0.05 s). Idle, walk, run and jump come from your numbers. Every field is optional. See "Motion". |
|
|
91
92
|
| `c.wave()` | Waves the left arm for 1.8 s (`WAVE_SECONDS`), as the island does. |
|
|
@@ -93,7 +94,9 @@ c.dispose();
|
|
|
93
94
|
| `c.setColors(colors \| null)` | Paints these colours over the record's; `null` gives the record's own back. No new geometry. |
|
|
94
95
|
| `c.record`, `c.look` | The record now (a frozen copy), and its five colours (the older view). |
|
|
95
96
|
| `c.reducedMotion` | A boolean you may change: true turns off the swing of the arms and legs and the bob, and makes the wave still. Set it from `prefers-reduced-motion`. |
|
|
96
|
-
| `c.
|
|
97
|
+
| `c.socket(name)` | An attach point, a `THREE.Object3D` on a bone: `"head"`, `"hand"` or `"back"`. Add your own object to it and it follows the bone. It is the same object for the character's whole life: a rebuild moves it, with what hangs on it, to the new bones (since 0.3.0; see "Worn things and sockets"). |
|
|
98
|
+
| `c.setWorn(worn)` | The worn things (`WornAdapter`): shapes merged into the body, or a model on a socket. A model on a socket builds no geometry; the objects of things no longer worn come off their socket (since 0.3.0). |
|
|
99
|
+
| `c.dispose()` | Frees the geometries, materials and textures that the character made. The other calls do nothing after it. It never frees an object that you hung on a socket: that is yours. |
|
|
97
100
|
|
|
98
101
|
Also exported from `/character`: `HEIGHT`, `RADIUS`, `WAVE_SECONDS`, `buildCharacter` and `assemble` (the pieces `createCharacter` uses: the geometry, the skinned meshes on the island's 28-bone skeleton), `BASE_IDS`, the lists, and everything of `/character/record`.
|
|
99
102
|
|
|
@@ -171,15 +174,62 @@ a player's choice of body never changes a rule. nano133.com's island and room do
|
|
|
171
174
|
- The ink line is a second draw of the same skinned mesh, with its back faces, pushed out along its normal in screen space, so it keeps one width in pixels (2.6). Use `WebGLRenderer`: the ink is a GLSL `ShaderMaterial`.
|
|
172
175
|
- Frame rate has not been measured on a real phone by this package. nano133.com's island draws a room of twelve of these characters at 60 frames a second on a throttled phone profile.
|
|
173
176
|
|
|
174
|
-
## Worn
|
|
177
|
+
## Worn things and sockets
|
|
175
178
|
|
|
176
|
-
|
|
177
|
-
fitted to every base by nano133.com's own code, and are not in this package yet. The builder takes them through **one small interface**,
|
|
178
|
-
`WornAdapter` (what it needs of a worn thing: its shapes and bones through `add(ctx)`, what it does to the hair, `hidesBadge`, `hideTail`), so a game
|
|
179
|
-
may bring its own, and the island brings its adapter the same way.
|
|
179
|
+
A worn thing reaches the builder through **one small interface**, `WornAdapter`. It has two ways, and a thing may use both:
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
- **Merged shapes** (`add(ctx)`, and what it does to the hair `fit`, `hidesBadge`, `hideTail`, `hideWings`): the thing's shapes go into the body's own buffers, so it
|
|
182
|
+
**adds no draw**. nano133.com fits the island's 14 shop items (hats, scarves, packs, tops, a buddy, a blade) to every base this way, with its own code.
|
|
183
|
+
- **A model on a socket** (`socket` and `object`, since 0.3.0): your own `THREE.Object3D` (a `.glb` you loaded, say) hangs on one of the character's three
|
|
184
|
+
attach points and follows its bone. It costs **its own draws** (1 or 2 for each thing). The character never frees it: you load it and you free it.
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
const c = createCharacter({ record });
|
|
188
|
+
const shovel = (await new GLTFLoader().loadAsync("/looks/shovel.glb")).scene;
|
|
189
|
+
c.setWorn([{ socket: "hand", object: shovel }]); // or: c.socket("hand").add(shovel)
|
|
190
|
+
c.setLook(otherRecord); // another base: the shovel moves to the new hand
|
|
191
|
+
c.setWorn([]); // the shovel comes off; free its geometry and materials yourself
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
| Socket | Bone | Where | Its axes |
|
|
195
|
+
|---|---|---|---|
|
|
196
|
+
| `"head"` | `head` | the base's hat frame (`BASE_SPECS[base].hat`: about the middle of the head), scaled by the hat frame and the head's size | the character's: +Y up, +Z forward. A thing made round the middle of the Classic's head (radius 0.42 m) fits every head |
|
|
197
|
+
| `"hand"` | `grip_R` | the middle of the drawn right hand (a wide build or base draws its hands further out than the bone) | `HAND_HOLD`: a thing modelled standing up (+Y) with its grip at the origin is held pointing forward, its front (+Z) up |
|
|
198
|
+
| `"back"` | `chest` | on the back of the torso at the chest's height (`BASE_SPECS[base].back`, else the torso's radius at 1.12) | the character's: the side that touches the body at the origin, the bulk towards -Z. Give the thing `hideTail: true` and `hideWings: true` to take a Critter's tail and a Bee's wings off under it |
|
|
199
|
+
|
|
200
|
+
`socketFrame(spec, look, name)` gives a socket's bone, place, turn and scale for a base and a look, and `placeSocket(object, bones, spec, look, name)`
|
|
201
|
+
puts an object there: a host that uses `buildCharacter` and `assemble` (not `createCharacter`) makes its own sockets with them. Check a socket model
|
|
202
|
+
with the presets of `@nano133/nanoterra/model` (`HEAD_ITEM`, `HAND_ITEM`, `BACK_ITEM`) before you ship it.
|
|
203
|
+
|
|
204
|
+
A player's item ids come in the sign-in answer (`ticket.items` here, `character.items` in the code exchange). The island's item art is not in this package;
|
|
205
|
+
a game may ignore the ids or draw its own things.
|
|
206
|
+
|
|
207
|
+
## Model check (`@nano133/nanoterra/model`; since 0.3.0)
|
|
208
|
+
|
|
209
|
+
`checkModel(bytes, limits)` reads a `.glb` (binary glTF 2.0) from its bytes and answers `{ ok, problems, facts, advice }`. It is pure (no Three.js, no
|
|
210
|
+
network, no DOM) and it never throws: a file that cannot be read answers a problem. The file comes from a stranger, so every read is bounded (no size in the
|
|
211
|
+
file is trusted, the work has a cap and a time limit).
|
|
212
|
+
|
|
213
|
+
- `problems`: each `{ rule, fix }`, the fix in one or two plain sentences for the creator. Rules: `file`, `size`, `embedded`, `extensions`, `lights`,
|
|
214
|
+
`motion`, `triangles`, `textures`, `draws`, `zone`, `markers`, `time`.
|
|
215
|
+
- `facts`: the bytes, the triangles, the **draws** (each part of each drawn mesh instance), the **materials** the parts use, the textures (their type and size,
|
|
216
|
+
read from the picture itself), the box and the reach, the markers, the anchor.
|
|
217
|
+
- `advice`: lines that are never a failure: a large PNG or JPEG (KTX2 makes it smaller), 32-bit positions (`KHR_mesh_quantization` halves them),
|
|
218
|
+
parts that share a material (joining them saves draws).
|
|
219
|
+
- `modelSummary(facts)`: one short line ("646 triangles, 0 textures, 49 KB, 0.5 x 0.4 x 1.0 m").
|
|
220
|
+
|
|
221
|
+
The presets (`limits.ts`; a game may make its own `ModelLimits`). The cosmetic numbers are Wes's of 2026-10-28 (the hand zone his second choice that day, so that a shovel held by its T-handle fits); the zone is in metres for a character 2.29 m
|
|
222
|
+
tall, its origin the socket's point (a hand item's `grip_R` marker when it has one):
|
|
223
|
+
|
|
224
|
+
| Preset | Zone (w x d x h) | Triangles | Textures | Draws | File |
|
|
225
|
+
|---|---|---|---|---|---|
|
|
226
|
+
| `HEAD_ITEM` | 0.9 x 0.9 x 0.8, from 0.1 below the socket | 3,000 | 1 of 512 px | 2 | 300 KB |
|
|
227
|
+
| `HAND_ITEM` | 0.96 x 0.68 x 1.6, from 1.17 below the grip to 0.43 above (a tool held near its top) | 3,000 | 1 of 512 px | 2 | 300 KB |
|
|
228
|
+
| `BACK_ITEM` | 1.0 x 0.6 x 1.2, from 0.6 below the socket | 4,000 | 1 of 512 px | 2 | 400 KB |
|
|
229
|
+
| `ISLAND_MODEL` | a disc of 10 m radius, 18 m high, from the ground | 50,000 | 8 of 1,024 px | no limit | 5 MB |
|
|
230
|
+
|
|
231
|
+
Every preset refuses skins, animations, morph targets, lights, external files and compressed meshes (Draco, meshopt): a worn thing is rigid on its socket.
|
|
232
|
+
`COSMETIC_LIMITS` gives the preset of each socket by its name. The island preset is nano133.com's (its "Launch my game" check uses this same code).
|
|
183
233
|
|
|
184
234
|
## The sign-in ticket
|
|
185
235
|
|
|
@@ -509,6 +559,25 @@ The panel draws: the **Game** card (the two switches), what is still off (the st
|
|
|
509
559
|
sign-in; a wrong wallet, a Nanoterra sign-in and a missing owner address each get their own screen and see no data. Options: `api` (the handler's base path), `noAddressHelp`,
|
|
510
560
|
`lookPicture(look)` (your own picture of a look for the Shelf), `repoHelp`, and `fetch`.
|
|
511
561
|
|
|
562
|
+
**The Shelf's pictures on the real character** (since 0.3.0). The panel has no Three.js, so it takes the pictures from you, lazily:
|
|
563
|
+
|
|
564
|
+
```ts
|
|
565
|
+
// admin.ts
|
|
566
|
+
import { mountAdmin, previewPicture } from "@nano133/nanoterra/admin/panel";
|
|
567
|
+
mountAdmin(el, { looks, signIn, signOut, lookPicture: previewPicture(() => import("./lookPreview").then((m) => m.render)) });
|
|
568
|
+
|
|
569
|
+
// lookPreview.ts (loaded only when the Shelf shows a look)
|
|
570
|
+
import { createLookPreview } from "@nano133/nanoterra/character";
|
|
571
|
+
const preview = createLookPreview({ body: () => yourDressedCharacter() }); // left out: the package's character, its shirt in the look's colour
|
|
572
|
+
export const render = (look) => preview.render(look);
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
`previewPicture(load)` shows the flat picture at once and puts the drawn one in its place when it is ready; with no WebGL, or when `load` fails, the
|
|
576
|
+
flat picture stays. `createLookPreview({ size?, body?, record?, background?, key? })` draws each look once with ONE shared offscreen WebGL renderer
|
|
577
|
+
(one context for the whole Shelf; a page may have about 16), one picture at a time, and keeps each picture (a PNG data URL) by the look's text.
|
|
578
|
+
`body()` gives `{ object, dress(look), dispose() }`: your own character dressed in the look (a hat, a model that loads: `dress` may return a promise).
|
|
579
|
+
`dispose()` frees the renderer and its context. The page's Content-Security-Policy must allow `img-src data:`.
|
|
580
|
+
|
|
512
581
|
**The Shelf never writes.** It edits a draft in the owner's browser and gives text to copy (`looks.json`, or a message for an AI assistant): there is no call in the panel that
|
|
513
582
|
changes a look, a price or an order, and a test proves that editing the Shelf sends no request. A change of looks is a change of your repository. The **Game** card writes only the
|
|
514
583
|
two settings. The stylesheet is scoped under `.n133-admin`, so it cannot leak into your page; to theme it set `--n133-ink`, `--n133-ink-soft`, `--n133-paper`, `--n133-line`,
|
|
@@ -531,7 +600,8 @@ The wallet sign-in stays yours (`signIn`), because only your game can start a re
|
|
|
531
600
|
|
|
532
601
|
## What it does NOT do yet
|
|
533
602
|
|
|
534
|
-
-
|
|
603
|
+
- The island's item art (see "Worn things and sockets"): a game brings its own things.
|
|
604
|
+
- A pose for a held thing: the arms swing free, so a two-handed thing (a `grip_L` as well as a `grip_R`) is held in the right hand only.
|
|
535
605
|
- The katana, the saya, the headband and the dojo look of the island's Ananos game. (The skeleton keeps their bones, with no vertices.)
|
|
536
606
|
- First-person arms, swimming, climbing, sitting and the room's other moves: only idle, walk, run, jump and wave.
|
|
537
607
|
- Shared geometry for many characters at once, and a react-three-fiber wrapper.
|
package/ai/SPEC.md
CHANGED
|
@@ -107,5 +107,9 @@ export const LOOKS = checkLooks(rawLooksJson); // throws with the first mistake
|
|
|
107
107
|
## 8. Do not
|
|
108
108
|
|
|
109
109
|
- Do not edit the geometry, the skeleton or the toon and ink materials.
|
|
110
|
-
- Do not add
|
|
110
|
+
- Do not add the katana, the headband or other brands' art. A game's own worn model goes on a socket (`c.socket("head" | "hand" | "back")` or
|
|
111
|
+
`c.setWorn([{ socket, object }])`), and is checked first with `checkModel(bytes, HAND_ITEM)` (or `HEAD_ITEM`, `BACK_ITEM`) from `@nano133/nanoterra/model`.
|
|
112
|
+
A hand thing has an empty `grip_R` node where the hand holds it and stays inside 0.96 x 0.68 m, from 1.17 m below that grip to 0.43 m above. A back
|
|
113
|
+
thing gives `hideTail: true` and `hideWings: true`, so a tail or wings do not poke through it.
|
|
114
|
+
The game loads the model and frees it; the character never does.
|
|
111
115
|
- Do not use a look or a record for a game's rules, a score or a payment: it is display only.
|
|
@@ -3,7 +3,7 @@ export type { MountedAdmin, MountOptions } from "./mount.js";
|
|
|
3
3
|
export { ApiError, createApi } from "./api.js";
|
|
4
4
|
export type { Api } from "./api.js";
|
|
5
5
|
export { gameCard } from "./gameCard.js";
|
|
6
|
-
export { lookPicture, shelfView } from "./shelf.js";
|
|
6
|
+
export { lookPicture, previewPicture, shelfView } from "./shelf.js";
|
|
7
7
|
export { DEFAULT_LOGINS_NOTE, signinsCard } from "./signins.js";
|
|
8
8
|
export type { ShelfData } from "./shelf.js";
|
|
9
9
|
export { aiPrompt, centsOf, changeText, ownedIds, shelfStats, soldText } from "./shelfData.js";
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
export { mountAdmin } from "./mount.js";
|
|
9
9
|
export { ApiError, createApi } from "./api.js";
|
|
10
10
|
export { gameCard } from "./gameCard.js";
|
|
11
|
-
export { lookPicture, shelfView } from "./shelf.js";
|
|
11
|
+
export { lookPicture, previewPicture, shelfView } from "./shelf.js";
|
|
12
12
|
export { DEFAULT_LOGINS_NOTE, signinsCard } from "./signins.js";
|
|
13
13
|
export { aiPrompt, centsOf, changeText, ownedIds, shelfStats, soldText } from "./shelfData.js";
|
|
@@ -7,6 +7,13 @@ export type ShelfData = {
|
|
|
7
7
|
};
|
|
8
8
|
/** A small flat picture of a look: the body in its colour and the hat in its colour. (The game draws the real character; a game can give its own picture.) */
|
|
9
9
|
export declare function lookPicture(l: Look): Element;
|
|
10
|
+
/**
|
|
11
|
+
* A Shelf picture that shows the look on the real character. `load` gives the drawing function (a look to a PNG address, or null), and is
|
|
12
|
+
* called once, the first time a picture is asked for: let it import your preview file dynamically, so Three.js loads only when the Shelf
|
|
13
|
+
* opens (this file never imports it; the game starter's src/client/admin.ts shows how). Until the picture is drawn, and when it cannot be (no WebGL, a failure), the flat picture stays.
|
|
14
|
+
* `@nano133/nanoterra/character` has `createLookPreview` to make the drawing function.
|
|
15
|
+
*/
|
|
16
|
+
export declare function previewPicture(load: () => Promise<(l: Look) => Promise<string | null>>, fallback?: (l: Look) => Element, size?: number): (l: Look) => Element;
|
|
10
17
|
/**
|
|
11
18
|
* The screen. `looks` is the game's looks now (its looks.json): the draft starts from them. `update` hands the screen fresh numbers (after the page reloads
|
|
12
19
|
* the summary) and keeps the draft. `picture` draws a look (default: the flat picture above). `repoHelp` is what to tell a game that has no repository set.
|
|
@@ -30,6 +30,30 @@ export function lookPicture(l) {
|
|
|
30
30
|
};
|
|
31
31
|
return svg("svg", { viewBox: "0 0 64 72", width: 56, height: 63, role: "img", "aria-label": `${l.name}: ${l.change.hat ?? "no hat"}` }, shape("rect", { x: 23, y: 61, width: 7, height: 9, rx: 2, fill: "#3a3f5c" }), shape("rect", { x: 34, y: 61, width: 7, height: 9, rx: 2, fill: "#3a3f5c" }), shape("rect", { x: 16, y: 37, width: 32, height: 28, rx: 12, fill: l.change.body }), shape("circle", { cx: 32, cy: 26, r: 13, fill: "#f2c9a0" }), svg("circle", { cx: 27, cy: 27, r: 1.7, fill: INK }), svg("circle", { cx: 37, cy: 27, r: 1.7, fill: INK }), ...(hat ? hats[hat] : []));
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* A Shelf picture that shows the look on the real character. `load` gives the drawing function (a look to a PNG address, or null), and is
|
|
35
|
+
* called once, the first time a picture is asked for: let it import your preview file dynamically, so Three.js loads only when the Shelf
|
|
36
|
+
* opens (this file never imports it; the game starter's src/client/admin.ts shows how). Until the picture is drawn, and when it cannot be (no WebGL, a failure), the flat picture stays.
|
|
37
|
+
* `@nano133/nanoterra/character` has `createLookPreview` to make the drawing function.
|
|
38
|
+
*/
|
|
39
|
+
export function previewPicture(load, fallback = lookPicture, size = 64) {
|
|
40
|
+
let ready = null;
|
|
41
|
+
return (l) => {
|
|
42
|
+
const box = h("span", { class: "shelf-preview" }, fallback(l));
|
|
43
|
+
ready ??= load().catch(() => null);
|
|
44
|
+
const look = { ...l, change: { ...l.change }, ...(l.slots ? { slots: { ...l.slots } } : {}) };
|
|
45
|
+
void ready
|
|
46
|
+
.then((render) => (render ? render(look) : null))
|
|
47
|
+
.then((src) => {
|
|
48
|
+
if (!src)
|
|
49
|
+
return;
|
|
50
|
+
const img = h("img", { src, width: size, height: size, alt: `${l.name || "A look"} on the character` });
|
|
51
|
+
box.replaceChildren(img);
|
|
52
|
+
})
|
|
53
|
+
.catch(() => undefined);
|
|
54
|
+
return box;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
33
57
|
const clone = (ls) => ls.map((l) => ({ id: l.id, name: l.name, priceUsd: l.priceUsd, change: { ...l.change }, ...(l.slots ? { slots: { ...l.slots } } : {}) }));
|
|
34
58
|
/** "wings ember, trail #ffcf5a" */
|
|
35
59
|
const slotsText = (l) => Object.entries(l.slots ?? {}).map(([k, v]) => `${k} ${v}`).join(", ");
|
|
@@ -27,11 +27,13 @@ export const bee = {
|
|
|
27
27
|
addFace(c);
|
|
28
28
|
if (!c.headOnly) {
|
|
29
29
|
c.addBody(G.cone8, mtx(0, 0.72, -0.5, -Math.PI / 2 + 0.25, 0, 0, 0.1, 0.32, 0.1), "stripe", "hips");
|
|
30
|
-
// The wings: a bone at each shoulder blade; a long wing and a short one on it.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
// The wings: a bone at each shoulder blade; a long wing and a short one on it. A back item covers them (`hideWings`): no wing, no bone.
|
|
31
|
+
if (!c.hideWings) {
|
|
32
|
+
for (const [s, name] of [[1, "wing_L"], [-1, "wing_R"]]) {
|
|
33
|
+
c.b.bone(name, "chest", [s * 0.12, 1.4, -0.3]);
|
|
34
|
+
c.add(SPH_S, mtx(s * 0.3, 1.78, -0.4, 0.2, 0, s * 1.15, 0.4, 0.04, 0.22), "wing", name);
|
|
35
|
+
c.add(SPH_S, mtx(s * 0.36, 1.52, -0.42, 0.15, 0, s * 0.8, 0.28, 0.035, 0.15), "wing", name);
|
|
36
|
+
}
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
limbs(c, bee.limbs);
|
|
@@ -13,5 +13,12 @@ export type BaseSpec = {
|
|
|
13
13
|
z?: number;
|
|
14
14
|
s: readonly [number, number, number];
|
|
15
15
|
};
|
|
16
|
+
/** The hand socket's place from the middle of the drawn right hand, when it is not that middle (sockets.ts). */
|
|
17
|
+
handAt?: readonly [number, number, number];
|
|
18
|
+
/** The back socket: its height (left out: the chest bone's, 1.12) and its depth (left out: the torso's radius there, behind). */
|
|
19
|
+
back?: {
|
|
20
|
+
y: number;
|
|
21
|
+
z?: number;
|
|
22
|
+
};
|
|
16
23
|
build(c: Ctx): void;
|
|
17
24
|
};
|
|
@@ -2,6 +2,8 @@ import * as THREE from "three";
|
|
|
2
2
|
import { type Look } from "./look.js";
|
|
3
3
|
import { type Motion } from "./pose.js";
|
|
4
4
|
import { type CharColors, type LookRecord } from "./record.js";
|
|
5
|
+
import type { WornAdapter } from "./ctx.js";
|
|
6
|
+
import { type SocketName } from "./sockets.js";
|
|
5
7
|
/** The character's height in metres at the record's normal size: from the soles to the top of the hair (the Classic base). Another base, hair or size is not this tall: measure it with a Box3. */
|
|
6
8
|
export declare const HEIGHT = 2.29;
|
|
7
9
|
/** The radius of the Classic's trunk in metres at the record's normal size: the collider's radius. The hands hang to 0.6 m from the middle. */
|
|
@@ -19,6 +21,8 @@ export type CharacterOptions = {
|
|
|
19
21
|
quality?: Quality;
|
|
20
22
|
/** Starts with the walk's swing and bob off, as the island does for a player who asks for less motion. */
|
|
21
23
|
reducedMotion?: boolean;
|
|
24
|
+
/** Worn things (ctx.ts `WornAdapter`): shapes merged into the body, or a model on a socket. Change them with `setWorn`. */
|
|
25
|
+
worn?: readonly WornAdapter[];
|
|
22
26
|
};
|
|
23
27
|
export interface Character {
|
|
24
28
|
/**
|
|
@@ -48,6 +52,17 @@ export interface Character {
|
|
|
48
52
|
setLook(next: LookRecord | Look): void;
|
|
49
53
|
/** Paints these colours over the record's, or none (`null`): the record's own come back. It builds no geometry. */
|
|
50
54
|
setColors(colors: Partial<CharColors> | null): void;
|
|
55
|
+
/**
|
|
56
|
+
* An attach point: "head" (at the base's hat frame, scaled with the head), "hand" (in the right hand, on grip_R) or "back" (on the chest,
|
|
57
|
+
* behind). Add your own object to it and it follows the bone. It is the same object for the character's whole life: a new base, hair or
|
|
58
|
+
* clothes (a rebuild) moves it, with what hangs on it, to the new bones and to the new base's frame. An unknown name throws.
|
|
59
|
+
*/
|
|
60
|
+
socket(name: SocketName): THREE.Object3D;
|
|
61
|
+
/**
|
|
62
|
+
* Changes the worn things. Shapes that merge (`add`) and the hair's fit build new geometry; a thing that only hangs a model on a socket
|
|
63
|
+
* builds none. The objects of the things that are no longer worn come off their socket (the game frees them: the character never does).
|
|
64
|
+
*/
|
|
65
|
+
setWorn(worn: readonly WornAdapter[]): void;
|
|
51
66
|
/**
|
|
52
67
|
* Frees the geometries, materials and textures that the character made, and takes `object` out of its parent. Later calls to the other methods do
|
|
53
68
|
* nothing.
|
package/dist/character/create.js
CHANGED
|
@@ -6,10 +6,12 @@ import { assertLook, recordProblem } from "./look.js";
|
|
|
6
6
|
import { buildWith, paintCharacter } from "./make.js";
|
|
7
7
|
import { makePose, MAX_DT } from "./pose.js";
|
|
8
8
|
import { clean, DEFAULT_RECORD, resolve, toLegacy, withLegacy } from "./record.js";
|
|
9
|
+
import { isSocketName, placeSocket, SOCKETS } from "./sockets.js";
|
|
9
10
|
import { followCanvas, inkMaterial, makeRamp, toonMaterial } from "./toon.js";
|
|
10
11
|
// createCharacter: the character as ONE object for a plain Three.js game. It builds the player's record (record.ts) with the kit (make.ts), binds it
|
|
11
12
|
// to the island's skeleton (assemble.ts), gives it the toon material and the ink line (toon.ts) and plays the island's idle, walk, run, jump and wave
|
|
12
|
-
// on it (pose.ts, solver.ts) from the numbers the game gives each frame.
|
|
13
|
+
// on it (pose.ts, solver.ts) from the numbers the game gives each frame. A worn item is the host's (ctx.ts `WornAdapter`): its shapes merge into the
|
|
14
|
+
// body, or its own model hangs on one of the three sockets (sockets.ts), which stay on the new bones after a rebuild.
|
|
13
15
|
// The blob shadow is a part of the body (a disc on the skeleton's shadow bone): it lies on the ground under the character in a jump and shrinks.
|
|
14
16
|
/** The character's height in metres at the record's normal size: from the soles to the top of the hair (the Classic base). Another base, hair or size is not this tall: measure it with a Box3. */
|
|
15
17
|
export const HEIGHT = 2.29;
|
|
@@ -32,6 +34,26 @@ function checkColors(colors) {
|
|
|
32
34
|
}
|
|
33
35
|
return out;
|
|
34
36
|
}
|
|
37
|
+
/** A worn thing that changes the body's geometry: its own shapes, or what it does to the hair, the badge, the tail or the wings. */
|
|
38
|
+
const merges = (w) => !!(w.add || w.fit || w.hidesBadge || w.hideTail || w.hideWings);
|
|
39
|
+
function checkWorn(worn) {
|
|
40
|
+
if (!Array.isArray(worn))
|
|
41
|
+
throw new TypeError("@nano133/nanoterra: worn must be a list of worn things");
|
|
42
|
+
worn.forEach((w, i) => {
|
|
43
|
+
const v = w;
|
|
44
|
+
if (typeof v !== "object" || v === null)
|
|
45
|
+
throw new TypeError(`@nano133/nanoterra: worn[${i}] is not an object`);
|
|
46
|
+
if (v.add !== undefined && typeof v.add !== "function")
|
|
47
|
+
throw new TypeError(`@nano133/nanoterra: worn[${i}].add must be a function`);
|
|
48
|
+
if (v.socket !== undefined && !isSocketName(v.socket))
|
|
49
|
+
throw new TypeError(`@nano133/nanoterra: worn[${i}].socket must be one of ${SOCKETS.join(", ")}, not ${JSON.stringify(v.socket)}`);
|
|
50
|
+
if ((v.object === undefined) !== (v.socket === undefined))
|
|
51
|
+
throw new TypeError(`@nano133/nanoterra: worn[${i}] needs both a socket and an object, or neither`);
|
|
52
|
+
if (v.object !== undefined && !v.object?.isObject3D)
|
|
53
|
+
throw new TypeError(`@nano133/nanoterra: worn[${i}].object must be a THREE.Object3D`);
|
|
54
|
+
});
|
|
55
|
+
return [...worn];
|
|
56
|
+
}
|
|
35
57
|
const frozen = (rec) => Object.freeze({ ...clean(rec) });
|
|
36
58
|
/** A record from a record or five colours; `from`: the record the five colours go into. Throws a TypeError that says what is wrong. */
|
|
37
59
|
function recordOf(next, from) {
|
|
@@ -55,6 +77,13 @@ export function createCharacter(options = {}) {
|
|
|
55
77
|
}
|
|
56
78
|
let record = opts.record !== undefined ? recordOf(opts.record, null) : opts.look !== undefined ? recordOf(opts.look, null) : { ...DEFAULT_RECORD };
|
|
57
79
|
let over = opts.colors === undefined ? null : checkColors(opts.colors);
|
|
80
|
+
let worn = checkWorn(opts.worn ?? []);
|
|
81
|
+
/** The three sockets: made once, moved to the new bones on every rebuild. */
|
|
82
|
+
const sockets = Object.fromEntries(SOCKETS.map((n) => {
|
|
83
|
+
const g = new THREE.Group();
|
|
84
|
+
g.name = `socket-${n}`;
|
|
85
|
+
return [n, g];
|
|
86
|
+
}));
|
|
58
87
|
const ramp = makeRamp();
|
|
59
88
|
const toon = toonMaterial(ramp);
|
|
60
89
|
const ink = quality === "high" ? inkMaterial() : null;
|
|
@@ -67,7 +96,10 @@ export function createCharacter(options = {}) {
|
|
|
67
96
|
const unit = () => (HEIGHTS[record.height] ?? 1) * (object.scale.y || 1);
|
|
68
97
|
const colorsNow = () => ({ ...resolve(record).colors, ...over });
|
|
69
98
|
function build() {
|
|
70
|
-
const
|
|
99
|
+
const look = resolve(record);
|
|
100
|
+
const spec = BASE_SPECS[look.base];
|
|
101
|
+
const merged = worn.filter(merges);
|
|
102
|
+
const made = buildWith(spec, look, merged.length ? { worn: merged } : {});
|
|
71
103
|
paintCharacter(made, colorsNow());
|
|
72
104
|
const a = assemble(made, { toon, ink: ink ?? toon });
|
|
73
105
|
a.mesh.name = "character-toon";
|
|
@@ -82,10 +114,19 @@ export function createCharacter(options = {}) {
|
|
|
82
114
|
a.group.scale.setScalar(HEIGHTS[record.height] ?? 1);
|
|
83
115
|
object.add(a.group);
|
|
84
116
|
const pose = makePose(a.list, a.bones, unit);
|
|
117
|
+
for (const n of SOCKETS)
|
|
118
|
+
placeSocket(sockets[n], a.bones, spec, look, n);
|
|
85
119
|
return { made, a, pose };
|
|
86
120
|
}
|
|
87
121
|
let rig = build();
|
|
88
122
|
rig.pose.rest(reducedMotion);
|
|
123
|
+
/** Hangs each worn model on its socket (in the order of the list). */
|
|
124
|
+
const hang = () => {
|
|
125
|
+
for (const w of worn)
|
|
126
|
+
if (w.object && w.socket)
|
|
127
|
+
sockets[w.socket].add(w.object);
|
|
128
|
+
};
|
|
129
|
+
hang();
|
|
89
130
|
/** Frees a rig's geometry and skeleton. A rig that is replaced also leaves the object; a character that is disposed keeps its (dead) meshes, as it always did. */
|
|
90
131
|
const free = (r, replaced) => {
|
|
91
132
|
if (replaced)
|
|
@@ -150,11 +191,37 @@ export function createCharacter(options = {}) {
|
|
|
150
191
|
over = colors === null ? null : checkColors(colors);
|
|
151
192
|
paintCharacter(rig.made, colorsNow());
|
|
152
193
|
},
|
|
194
|
+
socket(name) {
|
|
195
|
+
if (!isSocketName(name))
|
|
196
|
+
throw new TypeError(`@nano133/nanoterra: there is no socket ${JSON.stringify(name)} (the sockets are ${SOCKETS.join(", ")})`);
|
|
197
|
+
return sockets[name];
|
|
198
|
+
},
|
|
199
|
+
setWorn(next) {
|
|
200
|
+
if (disposed)
|
|
201
|
+
return;
|
|
202
|
+
const to = checkWorn(next);
|
|
203
|
+
const rebuild = worn.some(merges) || to.some(merges);
|
|
204
|
+
for (const w of worn)
|
|
205
|
+
if (w.object && !to.some((x) => x.object === w.object))
|
|
206
|
+
w.object.removeFromParent();
|
|
207
|
+
worn = to;
|
|
208
|
+
if (rebuild) {
|
|
209
|
+
const was = rig;
|
|
210
|
+
rig = build();
|
|
211
|
+
Object.assign(rig.pose.state, was.pose.state);
|
|
212
|
+
rig.pose.rest(reducedMotion);
|
|
213
|
+
free(was, true);
|
|
214
|
+
}
|
|
215
|
+
hang();
|
|
216
|
+
},
|
|
153
217
|
dispose() {
|
|
154
218
|
if (disposed)
|
|
155
219
|
return;
|
|
156
220
|
disposed = true;
|
|
157
221
|
object.removeFromParent();
|
|
222
|
+
// The sockets leave the dead bones; what hangs on them is the game's.
|
|
223
|
+
for (const n of SOCKETS)
|
|
224
|
+
sockets[n].removeFromParent();
|
|
158
225
|
free(rig, false);
|
|
159
226
|
toon.dispose();
|
|
160
227
|
ink?.dispose();
|
package/dist/character/ctx.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as THREE from "three";
|
|
|
2
2
|
import type { Builder, Paint, Weights } from "./build.js";
|
|
3
3
|
import type { Limbs } from "./limbs.js";
|
|
4
4
|
import type { CharLook } from "./record.js";
|
|
5
|
+
import type { SocketName } from "./sockets.js";
|
|
5
6
|
/** A head as an ellipsoid (its middle and its radii in the avatar's space, at rest). A face, a hair and a hat sit on it. `flat`: a flat screen face at this z. */
|
|
6
7
|
export type HeadSpec = {
|
|
7
8
|
c: readonly [number, number, number];
|
|
@@ -53,13 +54,20 @@ export type HatPart = {
|
|
|
53
54
|
* island's adapter (the shop's hats, scarves, packs, tops, buddies and blades, each fitted to every base) builds these; a game may bring its own.
|
|
54
55
|
* `add` puts the thing's shapes and bones into the build after the body, in order, through the same context as the body's (`c.add`, `c.addHead`,
|
|
55
56
|
* `c.b.bone`). A head item also says what it does to the hair (`fit`); the first that does decides. `hidesBadge`: the chest badge would poke through it.
|
|
56
|
-
* `hideTail`: a back item (a cape, a pack, a plush) or a pinafore covers a base's tail.
|
|
57
|
+
* `hideTail`: a back item (a cape, a pack, a plush) or a pinafore covers a base's tail. `hideWings`: a back item covers a base's wings (the Bee's).
|
|
58
|
+
*
|
|
59
|
+
* A thing with its own model (a .glb, say) names a `socket` ("head", "hand" or "back", sockets.ts) and gives its `object`: `createCharacter`
|
|
60
|
+
* hangs the object on that socket (its own draws: 1 or 2 more for each thing) and still reads `fit`, `hidesBadge`, `hideTail` and `hideWings`. The game
|
|
61
|
+
* owns the object: the character never frees it. `add` is then optional (a thing can have both: merged shapes and a model).
|
|
57
62
|
*/
|
|
58
63
|
export type WornAdapter = {
|
|
59
64
|
fit?: HairFit;
|
|
60
65
|
hidesBadge?: boolean;
|
|
61
66
|
hideTail?: boolean;
|
|
62
|
-
|
|
67
|
+
hideWings?: boolean;
|
|
68
|
+
add?(c: Ctx): void;
|
|
69
|
+
socket?: SocketName;
|
|
70
|
+
object?: THREE.Object3D;
|
|
63
71
|
};
|
|
64
72
|
/** A worn thing, by its older name (the same type as `WornAdapter`). */
|
|
65
73
|
export type WornPart = WornAdapter;
|
|
@@ -81,6 +89,8 @@ export type Ctx = {
|
|
|
81
89
|
hidesBadge: boolean;
|
|
82
90
|
/** A worn back item covers the base's tail (a Critter's). */
|
|
83
91
|
hideTail: boolean;
|
|
92
|
+
/** A worn back item covers the base's wings (a Bee's). */
|
|
93
|
+
hideWings: boolean;
|
|
84
94
|
/** The base's arms and legs (a worn cuff follows the arm). */
|
|
85
95
|
limbs: Limbs;
|
|
86
96
|
/** Only the head and what sits on it (the pictures of faces and hats). */
|
|
@@ -34,3 +34,5 @@ export * from "./edit.js";
|
|
|
34
34
|
export * from "./look.js";
|
|
35
35
|
export { createCharacter, HEIGHT, RADIUS, type Character, type CharacterOptions, type Quality } from "./create.js";
|
|
36
36
|
export { WAVE_SECONDS, MAX_DT, type Motion } from "./pose.js";
|
|
37
|
+
export { createLookPreview, type LookPreview, type LookPreviewOptions, type PreviewBody } from "./preview.js";
|
|
38
|
+
export { SOCKETS, HAND_HOLD, socketFrame, placeSocket, isSocketName, type SocketName, type SocketFrame, type SocketLook } from "./sockets.js";
|
package/dist/character/index.js
CHANGED
|
@@ -23,3 +23,5 @@ export * from "./edit.js";
|
|
|
23
23
|
export * from "./look.js";
|
|
24
24
|
export { createCharacter, HEIGHT, RADIUS } from "./create.js";
|
|
25
25
|
export { WAVE_SECONDS, MAX_DT } from "./pose.js";
|
|
26
|
+
export { createLookPreview } from "./preview.js";
|
|
27
|
+
export { SOCKETS, HAND_HOLD, socketFrame, placeSocket, isSocketName } from "./sockets.js";
|
package/dist/character/make.js
CHANGED
|
@@ -32,6 +32,7 @@ export function makeContext(spec, look, b, opts = {}) {
|
|
|
32
32
|
fit: worn.find((w) => w.fit)?.fit ?? NO_FIT,
|
|
33
33
|
hidesBadge: worn.some((w) => w.hidesBadge),
|
|
34
34
|
hideTail: worn.some((w) => w.hideTail),
|
|
35
|
+
hideWings: worn.some((w) => w.hideWings),
|
|
35
36
|
limbs: spec.limbs,
|
|
36
37
|
headOnly,
|
|
37
38
|
add,
|
|
@@ -55,7 +56,7 @@ export function buildWith(spec, look, opts = {}) {
|
|
|
55
56
|
c.addBody(new THREE.CircleGeometry(0.62, 20), mtx(0, 0.02, 0, -Math.PI / 2), "shadow", "shadow", false);
|
|
56
57
|
}
|
|
57
58
|
for (const w of wornOf(opts))
|
|
58
|
-
w.add(c);
|
|
59
|
+
w.add?.(c);
|
|
59
60
|
return { ...b.finish(), toonTris: b.p.length / 9, inkTris: b.ip.length / 9, look };
|
|
60
61
|
}
|
|
61
62
|
/** Paints (or paints again) the character's colours: a colour change needs no new geometry. */
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { LookRecord } from "./record.js";
|
|
3
|
+
/** What draws one look: the game's own character dressed in it (its hat, its models), or by default the package's character. */
|
|
4
|
+
export type PreviewBody<L> = {
|
|
5
|
+
object: THREE.Object3D;
|
|
6
|
+
/** Puts the look on. A promise waits (a model that loads). */
|
|
7
|
+
dress(look: L): void | Promise<void>;
|
|
8
|
+
/** Frees what the body made. */
|
|
9
|
+
dispose(): void;
|
|
10
|
+
};
|
|
11
|
+
export type LookPreviewOptions<L> = {
|
|
12
|
+
/** The picture's side in CSS pixels (default 112); it is drawn at twice that for a sharp screen. */
|
|
13
|
+
size?: number;
|
|
14
|
+
/** A body for the next picture. Left out: the package's character in `record` (else the island's first look), its shirt in the look's body colour. */
|
|
15
|
+
body?: () => PreviewBody<L>;
|
|
16
|
+
/** The record of the default body. */
|
|
17
|
+
record?: LookRecord;
|
|
18
|
+
/** The background (default: none, a clear picture). */
|
|
19
|
+
background?: THREE.ColorRepresentation | null;
|
|
20
|
+
/** The key of a look in the cache (default: its JSON text). */
|
|
21
|
+
key?: (look: L) => string;
|
|
22
|
+
};
|
|
23
|
+
export type LookPreview<L> = {
|
|
24
|
+
/** A PNG data URL of the look on the character, or null when the page has no WebGL or the drawing failed. */
|
|
25
|
+
render(look: L): Promise<string | null>;
|
|
26
|
+
/** How many pictures are kept. */
|
|
27
|
+
readonly cached: number;
|
|
28
|
+
/** Frees the renderer (and its WebGL context) and the cache. `render` answers null after it. */
|
|
29
|
+
dispose(): void;
|
|
30
|
+
};
|
|
31
|
+
/** A picture maker. Make one for a page and keep it: it holds one WebGL context while it lives. */
|
|
32
|
+
export declare function createLookPreview<L>(options?: LookPreviewOptions<L>): LookPreview<L>;
|