@nano133/nanoterra 0.0.0-stage → 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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +361 -2
- package/ai/PROMPT.md +16 -0
- package/ai/SPEC.md +110 -0
- package/dist/character/assemble.d.ts +17 -0
- package/dist/character/assemble.js +39 -0
- package/dist/character/bases/bee.d.ts +2 -0
- package/dist/character/bases/bee.js +39 -0
- package/dist/character/bases/classic.d.ts +4 -0
- package/dist/character/bases/classic.js +28 -0
- package/dist/character/bases/critter.d.ts +2 -0
- package/dist/character/bases/critter.js +43 -0
- package/dist/character/bases/index.d.ts +5 -0
- package/dist/character/bases/index.js +8 -0
- package/dist/character/bases/pineapple.d.ts +2 -0
- package/dist/character/bases/pineapple.js +43 -0
- package/dist/character/bases/robot.d.ts +2 -0
- package/dist/character/bases/robot.js +33 -0
- package/dist/character/bases/round.d.ts +2 -0
- package/dist/character/bases/round.js +65 -0
- package/dist/character/bases/shared.d.ts +17 -0
- package/dist/character/bases/shared.js +17 -0
- package/dist/character/bases/spec.d.ts +17 -0
- package/dist/character/bases/spec.js +1 -0
- package/dist/character/build.d.ts +65 -0
- package/dist/character/build.js +223 -0
- package/dist/character/clothes.d.ts +21 -0
- package/dist/character/clothes.js +193 -0
- package/dist/character/color.d.ts +2 -0
- package/dist/character/color.js +9 -0
- package/dist/character/create.d.ts +58 -0
- package/dist/character/create.js +164 -0
- package/dist/character/ctx.d.ts +97 -0
- package/dist/character/ctx.js +1 -0
- package/dist/character/edit.d.ts +11 -0
- package/dist/character/edit.js +73 -0
- package/dist/character/face.d.ts +15 -0
- package/dist/character/face.js +181 -0
- package/dist/character/fit.d.ts +12 -0
- package/dist/character/fit.js +66 -0
- package/dist/character/hair.d.ts +4 -0
- package/dist/character/hair.js +139 -0
- package/dist/character/index.d.ts +36 -0
- package/dist/character/index.js +25 -0
- package/dist/character/kit.d.ts +16 -0
- package/dist/character/kit.js +47 -0
- package/dist/character/limbs.d.ts +30 -0
- package/dist/character/limbs.js +45 -0
- package/dist/character/lists.d.ts +63 -0
- package/dist/character/lists.js +59 -0
- package/dist/character/look.d.ts +27 -0
- package/dist/character/look.js +75 -0
- package/dist/character/make.d.ts +53 -0
- package/dist/character/make.js +64 -0
- package/dist/character/motion.d.ts +7 -0
- package/dist/character/motion.js +95 -0
- package/dist/character/pose.d.ts +42 -0
- package/dist/character/pose.js +98 -0
- package/dist/character/pure.d.ts +4 -0
- package/dist/character/pure.js +7 -0
- package/dist/character/record.d.ts +137 -0
- package/dist/character/record.js +190 -0
- package/dist/character/skeleton.d.ts +15 -0
- package/dist/character/skeleton.js +77 -0
- package/dist/character/solver.d.ts +36 -0
- package/dist/character/solver.js +145 -0
- package/dist/character/toon.d.ts +16 -0
- package/dist/character/toon.js +95 -0
- package/dist/character/torso.d.ts +8 -0
- package/dist/character/torso.js +9 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +8 -0
- package/dist/looks/change.d.ts +20 -0
- package/dist/looks/change.js +41 -0
- package/dist/looks/index.d.ts +7 -0
- package/dist/looks/index.js +4 -0
- package/dist/looks/json.d.ts +8 -0
- package/dist/looks/json.js +35 -0
- package/dist/looks/ladder.d.ts +14 -0
- package/dist/looks/ladder.js +24 -0
- package/dist/looks/look.d.ts +36 -0
- package/dist/looks/look.js +57 -0
- package/dist/ticket/index.d.ts +99 -0
- package/dist/ticket/index.js +191 -0
- package/package.json +39 -4
- package/src/character/assemble.ts +44 -0
- package/src/character/bases/bee.ts +42 -0
- package/src/character/bases/classic.ts +30 -0
- package/src/character/bases/critter.ts +44 -0
- package/src/character/bases/index.ts +12 -0
- package/src/character/bases/pineapple.ts +45 -0
- package/src/character/bases/robot.ts +34 -0
- package/src/character/bases/round.ts +69 -0
- package/src/character/bases/shared.ts +25 -0
- package/src/character/bases/spec.ts +18 -0
- package/src/character/build.ts +244 -0
- package/src/character/clothes.ts +204 -0
- package/src/character/color.ts +10 -0
- package/src/character/create.ts +214 -0
- package/src/character/ctx.ts +95 -0
- package/src/character/edit.ts +78 -0
- package/src/character/face.ts +176 -0
- package/src/character/fit.ts +78 -0
- package/src/character/hair.ts +142 -0
- package/src/character/index.ts +29 -0
- package/src/character/kit.ts +51 -0
- package/src/character/limbs.ts +78 -0
- package/src/character/lists.ts +77 -0
- package/src/character/look.ts +85 -0
- package/src/character/make.ts +82 -0
- package/src/character/motion.ts +100 -0
- package/src/character/pose.ts +132 -0
- package/src/character/pure.ts +7 -0
- package/src/character/record.ts +265 -0
- package/src/character/skeleton.ts +82 -0
- package/src/character/solver.ts +160 -0
- package/src/character/toon.ts +104 -0
- package/src/character/torso.ts +13 -0
- package/src/index.ts +12 -0
- package/src/looks/change.ts +46 -0
- package/src/looks/index.ts +7 -0
- package/src/looks/json.ts +34 -0
- package/src/looks/ladder.ts +35 -0
- package/src/looks/look.ts +70 -0
- package/src/ticket/index.ts +241 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
First version (Pre-alpha). **One package for Nanoterra.** It replaces `@nano133/character` 0.1.0, `@nano133/ticket` 0.1.0 and
|
|
6
|
+
`@nano133/looks` 0.1.0 (their npm versions stay; they are deprecated and point here).
|
|
7
|
+
|
|
8
|
+
- **`@nano133/nanoterra/character`**: the character is now the one that nano133.com's island and 3D room draw, from the same code. Six bases
|
|
9
|
+
(Classic, Round, Critter, Bee, Robot, Pineapple), faces, hair, clothes, sizes and colours, from a **record** of whole numbers on closed lists.
|
|
10
|
+
`createCharacter({ record, look, colors, quality, reducedMotion })` gives `object`, `update(dt, { speed, airborne, verticalSpeed })`,
|
|
11
|
+
`wave()`, `setLook(recordOrFiveColours)`, `setColors(colors)`, `record`, `look`, `reducedMotion` and `dispose()`. `setLook` to another body builds new
|
|
12
|
+
geometry and frees the old; a new colour only repaints. The default Classic is the old package's character, vertex for vertex.
|
|
13
|
+
Also exported: `buildCharacter` and `assemble` (the geometry and the skinned meshes on the island's 28-bone skeleton), the lists, `WornAdapter`.
|
|
14
|
+
- **Changed from `@nano133/character` 0.1.0:** the blob shadow is a disc in the body (on the skeleton's shadow bone), so there is no `c.shadow` and one
|
|
15
|
+
`scene.add(c.object)` is enough; it stays on the ground under a jump. A colour that is not on the island's lists (a shop dye) goes through `setColors`
|
|
16
|
+
(`setLook` takes a record or the five colours of the lists). `HEIGHT` and `RADIUS` are the default Classic's.
|
|
17
|
+
- **`@nano133/nanoterra/character/record`**: the record with no Three.js and no package at all: `packRecord` and `unpackRecord` (two whole numbers; `null` for
|
|
18
|
+
anything off a list), `recordProblem`, `clean`, `fromLegacy` and `toLegacy` (the five colours; all 8,192 old looks come back whole), `withLegacy`, `withBase`,
|
|
19
|
+
`diceRecord`, `fromBlob` and `toBlob` (the room's blob), the lists, `defaultLook`, `isLook`, `lookProblem`, `assertLook`, `LOOK_PARTS`.
|
|
20
|
+
- **`@nano133/nanoterra/ticket`**: `readTicket`, `verifyTicket`, `fetchKeys`, `ticketFromHash`, `hashWithoutTicket`, as in `@nano133/ticket` 0.1.0, and the
|
|
21
|
+
ticket's **`record`** when it carries one (read with care; absent for a ticket from before it, and then the answer reads exactly as it did). The same pinned
|
|
22
|
+
ticket is checked byte for byte here and in nano133.com's own test.
|
|
23
|
+
- **`@nano133/nanoterra/looks`**: `@nano133/looks` 0.1.0 as it was (`checkLooks`, `lookProblems`, `removalProblems`, `diffLooks`, `looksJson`, `slugId`,
|
|
24
|
+
`PRICE_LADDER`, `rungOf`, `priceText`, `usdText`, `wholeCents`, `HATS`).
|
|
25
|
+
- **`@nano133/nanoterra`**: the types `Look` and `LookRecord`, and nothing that runs. Importing the record, the ticket, the looks or this path loads no Three.js
|
|
26
|
+
(a test reads the built files and proves it). `three` is an optional peer dependency (`>=0.174.0`).
|
|
27
|
+
- Body only. The island's 14 worn items are not in this package: the builder takes them through the `WornAdapter` interface, and a later version (0.2.0) adds
|
|
28
|
+
the item art. A player's item ids are in the sign-in answer.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nano133.com
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,362 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nano133/nanoterra
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Stage: Pre-alpha. What this means: https://nano133.com/stages
|
|
4
|
+
|
|
5
|
+
Nanoterra for your game, in **one package**. [Nanoterra](https://nano133.com/build/nanoterra) is
|
|
6
|
+
nano133.com's island. A player who is signed in there presses Play, and arrives at your game as
|
|
7
|
+
themselves. This package gives your game three things:
|
|
8
|
+
|
|
9
|
+
- **The sign-in ticket** (`/ticket`): the player's name, Nano address and character, read in the browser. No server, no secret.
|
|
10
|
+
- **The player's character** (`/character`): the same toon body that the player has on the island, drawn with Three.js. Six bases, faces, hair, clothes, sizes and colours.
|
|
11
|
+
- **The looks ladder** (`/looks`): the rules of `looks.json`, the price steps, and what a change does to the players who own a look.
|
|
12
|
+
|
|
13
|
+
It replaces `@nano133/character` 0.1.0, `@nano133/ticket` 0.1.0 and `@nano133/looks` 0.1.0
|
|
14
|
+
(see "Moving from the three old packages"). Sign-in, checkout and late returns stay in their own packages
|
|
15
|
+
([`@nano133/signin`](../signin), [`@nano133/checkout`](../checkout), [`@nano133/late-returns`](../late-returns)).
|
|
16
|
+
|
|
17
|
+
nano133.com's island and its 3D room draw the player with **this same code**: the geometry, the skeleton, the colours and the record
|
|
18
|
+
are one source. A game made with this package shows the character exactly as the island does.
|
|
19
|
+
|
|
20
|
+
Pre-alpha means: the shape of the API may still change, and the character has not been run on a real phone yet.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install @nano133/nanoterra three
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`three` is an **optional** peer dependency (`>=0.174.0`). A game that only reads a ticket does not need it.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
A page with a Three.js scene. The player arrives from Nanoterra, signed in; any other visitor is a guest.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { readTicket } from "@nano133/nanoterra/ticket";
|
|
36
|
+
import { createCharacter } from "@nano133/nanoterra/character";
|
|
37
|
+
|
|
38
|
+
const result = await readTicket(); // the ticket in the address, checked; { ok: false } is a guest (ask "Sign in as <name>? Continue / Not me" first)
|
|
39
|
+
const hero = createCharacter(result.ok ? { record: result.ticket.record, look: result.ticket.look } : {}); // their character, or the island's first look
|
|
40
|
+
scene.add(hero.object); // feet at y = 0, facing +Z, about 2.29 m tall; move it with position and rotation.y
|
|
41
|
+
renderer.setAnimationLoop(() => {
|
|
42
|
+
hero.update(clock.getDelta(), { speed: 3.75 }); // idle, walk (3.75 m/s), run (6.5), jump: your numbers; dt in seconds
|
|
43
|
+
renderer.render(scene, camera);
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
That is the whole of it. Add `hero.wave()`, `hero.setLook(next)` and `hero.dispose()` as the game needs them.
|
|
48
|
+
|
|
49
|
+
**The one switch.** The [game starter](https://github.com/nano133-com/nano-game-starter) keeps one switch in one file:
|
|
50
|
+
`game.config.json` says `"character": "nanoterra"` (the player is their Nanoterra character: the default) or `"own"` (the game's own
|
|
51
|
+
character). Nothing else reads it. If you do not use the starter, make your own switch where it suits your game.
|
|
52
|
+
|
|
53
|
+
## The paths
|
|
54
|
+
|
|
55
|
+
Each path loads only what it needs. A page that reads a ticket loads no Three.js.
|
|
56
|
+
|
|
57
|
+
| Path | What it is | Loads Three.js | Size (minified, gzip) |
|
|
58
|
+
|---|---|---|---|
|
|
59
|
+
| `@nano133/nanoterra` | the types `Look` and `LookRecord`, and nothing that runs | no | 0 |
|
|
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
|
+
| `@nano133/nanoterra/ticket` | read and check a sign-in ticket | no | 4.7 kB (all), 3.1 kB (`readTicket` alone) |
|
|
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 (all), 17.8 kB (`createCharacter` alone) |
|
|
64
|
+
|
|
65
|
+
Measured 2026-10-26: `esbuild --bundle --minify --format=esm --external:three`, then `gzip -9`, without Three.js.
|
|
66
|
+
All four paths together: 23.6 kB. A test of the package proves that the record, the ticket, the looks and the small part import no package at all.
|
|
67
|
+
|
|
68
|
+
## The character
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { createCharacter, HEIGHT, RADIUS } from "@nano133/nanoterra/character";
|
|
72
|
+
|
|
73
|
+
const c = createCharacter({ record, quality: "high" });
|
|
74
|
+
scene.add(c.object);
|
|
75
|
+
c.update(dt, { speed, airborne, verticalSpeed });
|
|
76
|
+
c.wave();
|
|
77
|
+
c.setLook(otherRecord); // or five colours
|
|
78
|
+
c.setColors({ shirt: "#c8461a" }); // any colour, over the record's
|
|
79
|
+
c.dispose();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Call | What it does |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `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. |
|
|
85
|
+
| `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). |
|
|
86
|
+
| `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". |
|
|
87
|
+
| `c.wave()` | Waves the left arm for 1.8 s (`WAVE_SECONDS`), as the island does. |
|
|
88
|
+
| `c.setLook(next)` | `next` is a record or five colours. A record with the same body only repaints; another base, hair, face or clothes builds new geometry (the old is freed) and the walk carries on. Five colours go into the record that is there (its base, face and clothes stay). A bad value throws and changes nothing. |
|
|
89
|
+
| `c.setColors(colors \| null)` | Paints these colours over the record's; `null` gives the record's own back. No new geometry. |
|
|
90
|
+
| `c.record`, `c.look` | The record now (a frozen copy), and its five colours (the older view). |
|
|
91
|
+
| `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`. |
|
|
92
|
+
| `c.dispose()` | Frees the geometries, materials and textures that the character made. The other calls do nothing after it. |
|
|
93
|
+
|
|
94
|
+
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`.
|
|
95
|
+
|
|
96
|
+
### Motion
|
|
97
|
+
|
|
98
|
+
`speed` is the speed over the ground in metres a second (not a velocity: the direction is `object.rotation.y`, which you set). It is eased, as on the island.
|
|
99
|
+
|
|
100
|
+
| `speed` | What shows |
|
|
101
|
+
|---|---|
|
|
102
|
+
| 0 to 0.25 | Idle: arms 0.2 rad out, elbows a little bent. |
|
|
103
|
+
| 0.25 and more | A walk: legs and arms swing against each other, the hips bob and lean. The swing is full at 3 m/s. The island walks at 3.75 m/s. |
|
|
104
|
+
| 3.4 to 5 | The walk blends into a run: more swing, more bend at the elbows, more lean. The island runs at 6.5 m/s. |
|
|
105
|
+
| 9 and more | The same as 9. |
|
|
106
|
+
|
|
107
|
+
`airborne: true` shows the jump pose at once, whatever the speed: arms out, legs apart. `verticalSpeed` (metres a second, up is positive)
|
|
108
|
+
sizes the shadow: it adds up while the character is airborne to an estimate of the height over the take-off place. The shadow disc stays on the
|
|
109
|
+
ground (it moves down as the body rises, in the character's own space) and shrinks to 0.55 of its size, as on the island. A character made
|
|
110
|
+
smaller or larger by `object.scale` should be given `speed / scale` and `verticalSpeed / scale`. A base with a tail, wings or long hair moves them too.
|
|
111
|
+
|
|
112
|
+
### The record
|
|
113
|
+
|
|
114
|
+
A record is the whole character as whole numbers, each an index into a **closed list**. There is no free text, no URL and no colour string,
|
|
115
|
+
so there is nothing to moderate and nothing to inject.
|
|
116
|
+
|
|
117
|
+
| Field | What it picks |
|
|
118
|
+
|---|---|
|
|
119
|
+
| `base` | one of six: `classic`, `round`, `critter`, `bee`, `robot`, `pineapple` |
|
|
120
|
+
| `skin`, `hair`, `shirt`, `pants`, `shoeColor`, `trim`, `visor` | a colour from that part's list (the body's list depends on the base) |
|
|
121
|
+
| `hairStyle`, `face`, `brows`, `blush`, `freckles`, `glasses` | the head |
|
|
122
|
+
| `top`, `bottom`, `shoes`, `pattern` | the clothes (a base with its own body, such as the Bee, wears none) |
|
|
123
|
+
| `height`, `build`, `head` | the sizes (a scale; the build and the head size change the shape) |
|
|
124
|
+
|
|
125
|
+
It packs into **two whole numbers** (`packRecord`: 28 and 30 bits) for a wire, a ticket or a database; `unpackRecord(a, b)` gives the record back, or
|
|
126
|
+
`null` when a number is not whole, is out of its bits or is off a list. `recordProblem(x)` says in one sentence what is wrong with a record,
|
|
127
|
+
`clean(x)` puts a wrong field back to its default, and `diceRecord(rand)` makes a random one. A record of zeros is the island's first avatar.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { fromLegacy, toLegacy, packRecord, unpackRecord, diceRecord, baseRecord } from "@nano133/nanoterra/character/record";
|
|
131
|
+
|
|
132
|
+
const rec = fromLegacy(ticket.look); // five colours (a ticket from before the record) to a record: a Classic in those colours
|
|
133
|
+
toLegacy(rec); // the record's five colours again: every one of the 8,192 old looks comes back whole (a test holds it)
|
|
134
|
+
const [a, b] = packRecord(rec); // two numbers
|
|
135
|
+
unpackRecord(a, b); // the record, or null
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The five colours, `Look`, are `{ skin, hair, shirt, pants, shoes }`, each `"#rrggbb"`. `LOOK_PARTS` lists the colours the island offers
|
|
139
|
+
(8 skins, 8 hairs, 8 shirts, 4 pants, 4 shoes), `defaultLook()` is what a new player starts with, and `isLook(x)`, `lookProblem(x)` and `assertLook(x)` check one.
|
|
140
|
+
|
|
141
|
+
### Units, size and the collider
|
|
142
|
+
|
|
143
|
+
The model is built in metres, at the size of the island's avatar.
|
|
144
|
+
|
|
145
|
+
| | |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `HEIGHT` = 2.29 m | From the soles (they dip 0.008 m under y = 0) to the top of the hair, for the default Classic at its normal size. Another base, hair or size is not this tall: measure with `new THREE.Box3().setFromObject(c.object)`. |
|
|
148
|
+
| `RADIUS` = 0.49 m | The Classic's trunk: the widest part of the body without the arms. Use it as the radius of a cylinder or capsule collider. |
|
|
149
|
+
| Facing | +Z. `object.rotation.y = Math.atan2(dx, dz)` turns it to walk along (dx, dz). |
|
|
150
|
+
| Its left | +X (the character's own left hand is at +X). |
|
|
151
|
+
|
|
152
|
+
The body is **display only**. A game decides its own collider, jump and reach: use the same numbers for every base and size, so that
|
|
153
|
+
a player's choice of body never changes a rule. nano133.com's island and room do the same. A size changes a scale of the drawing (`height`) and nothing else.
|
|
154
|
+
|
|
155
|
+
### Draws, quality, speed and size
|
|
156
|
+
|
|
157
|
+
| | `quality: "high"` | `quality: "low"` |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| Draws | 2 (toon body with its shadow disc, ink outline) | 1 (toon body with its shadow disc) |
|
|
160
|
+
| What it costs | | No outline: the character reads flatter against a background of its own colours. |
|
|
161
|
+
| What it saves | | One draw and the skinned ink vertices each frame. |
|
|
162
|
+
|
|
163
|
+
`"low"` keeps every shape and colour: only the ink pass goes. Choose it for slow phones. It is fixed when the character is made.
|
|
164
|
+
|
|
165
|
+
- Each character builds its own geometry. A scene with many characters of the same look should share it; this package does not yet.
|
|
166
|
+
- The meshes are not frustum-culled by their bones (they move every frame): the package gives each a fixed bound. Set `c.object.visible = false` for a character that is far from the camera or off screen.
|
|
167
|
+
- 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`.
|
|
168
|
+
- 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.
|
|
169
|
+
|
|
170
|
+
## Worn items (the next version)
|
|
171
|
+
|
|
172
|
+
**Version 0.1.0 draws the body only**, as `@nano133/character` 0.1.0 did. The island's 14 shop items (hats, scarves, packs, tops, a buddy, a blade) are
|
|
173
|
+
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**,
|
|
174
|
+
`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
|
|
175
|
+
may bring its own, and the island brings its adapter the same way.
|
|
176
|
+
|
|
177
|
+
A player's item ids come in the sign-in answer (`ticket.items` here, `character.items` in the code exchange). A later version (0.2.0) adds the item art so
|
|
178
|
+
that a game can draw them with one call. Until then a game may ignore the ids or draw its own things.
|
|
179
|
+
|
|
180
|
+
## The sign-in ticket
|
|
181
|
+
|
|
182
|
+
`@nano133/nanoterra/ticket` reads and checks a sign-in ticket in the player's browser. It is for a game on a **static site**: no server, no secret.
|
|
183
|
+
A player who is signed in at nano133.com presses Play in Nanoterra, agrees once on a card, and arrives at your game with a signed ticket after
|
|
184
|
+
the `#` of its address. This package checks the ticket and gives you the player's **username**, **Nano address** and **character** (the five colours,
|
|
185
|
+
the whole record, and the cosmetics the player owns).
|
|
186
|
+
|
|
187
|
+
If your game has a server, use the code exchange instead (the [game starter](https://github.com/nano133-com/nano-game-starter) does): a server that
|
|
188
|
+
holds a secret can know who is at the other end. A ticket cannot.
|
|
189
|
+
|
|
190
|
+
`readTicket()` does four things, in this order:
|
|
191
|
+
|
|
192
|
+
1. Finds `#nanoterra=<ticket>` in the page's address. With none, it answers `{ ok: false, reason: "none" }` and does nothing else (no network call).
|
|
193
|
+
2. **Takes the ticket out of the address bar** (`history.replaceState`), so a reload, a copied link, a screenshot or a bookmark never carries it.
|
|
194
|
+
3. Reads the public keys from `https://nano133.com/api/nanoterra/keys` (once for the session, and again if a ticket names a key it does not know; it waits 5 seconds at most, then the player is a guest).
|
|
195
|
+
4. Checks the signature, the kind, that the ticket is **for this origin** and that it has not run out. It never throws.
|
|
196
|
+
|
|
197
|
+
Call it once, when the page loads.
|
|
198
|
+
|
|
199
|
+
### Rules
|
|
200
|
+
|
|
201
|
+
- **A ticket proves nothing to your server.** The ticket sits in an address bar: anyone who sees it can use it for two minutes. Use it to show the
|
|
202
|
+
player's name and character. **Never give a sale, a prize or a score on a ticket alone.**
|
|
203
|
+
- **Ask before you treat the player as that name.** Anyone can send a link that holds their *own* ticket (login CSRF). Show "Sign in as *name*? Continue / Not me"
|
|
204
|
+
and keep the player a guest until Continue.
|
|
205
|
+
- **Show the name** wherever the player is "signed in", and say whose account it is before a payment.
|
|
206
|
+
- **The username comes from another site.** Put it in the page as text, never as HTML.
|
|
207
|
+
- A guest is a normal visit. Every refusal (`reason`) means "play as a guest".
|
|
208
|
+
|
|
209
|
+
### What you get
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
type Ticket = {
|
|
213
|
+
username: string;
|
|
214
|
+
address: string; // the whole Nano address; also the player's id
|
|
215
|
+
look: { skin: string; hair: string; shirt: string; pants: string; shoes: string }; // "#rrggbb": every ticket has these
|
|
216
|
+
record?: LookRecord; // the whole character, when the ticket carries one (see below); a ticket from before it has no `record` at all
|
|
217
|
+
items: string[]; // the Nanoterra cosmetics the player owns, in the character's slots
|
|
218
|
+
game: string; // your game's id at nano133.com
|
|
219
|
+
audience: string; // the origin the ticket is for
|
|
220
|
+
issuedAt: number; // ms since 1970
|
|
221
|
+
expiresAt: number;
|
|
222
|
+
id: string;
|
|
223
|
+
};
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`record` is read with care: the ticket's claim is two numbers, and it is kept only when both are whole numbers on the closed lists (`unpackRecord`).
|
|
227
|
+
A ticket without a record, or with one that is not whole, is still a good ticket and reads **exactly as the `@nano133/ticket` 0.1.0 ticket did**: the field is not there.
|
|
228
|
+
`createCharacter({ record: ticket.record, look: ticket.look })` takes both.
|
|
229
|
+
|
|
230
|
+
### Refusals
|
|
231
|
+
|
|
232
|
+
| `reason` | Meaning |
|
|
233
|
+
|---|---|
|
|
234
|
+
| `none` | no ticket in the address: a plain visit |
|
|
235
|
+
| `unsupported` | this browser has no Ed25519 in Web Crypto (Chrome 137, Safari 17, Firefox 129 and later have it) |
|
|
236
|
+
| `malformed` | not a ticket |
|
|
237
|
+
| `keys` | the public keys could not be read (offline, or nano133.com did not answer) |
|
|
238
|
+
| `unknown-key`, `signature` | signed with a key that is not nano133.com's, or the signature does not hold |
|
|
239
|
+
| `wrong-kind` | signed right, but not a version 1 ticket from nano133.com |
|
|
240
|
+
| `audience` | a ticket for another origin |
|
|
241
|
+
| `expired`, `early`, `life` | the time does not hold. A player's clock may be a minute off, either way |
|
|
242
|
+
|
|
243
|
+
### Options
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
readTicket({
|
|
247
|
+
keys, // pin the public keys: no fetch at all
|
|
248
|
+
keysUrl, // another place for the keys (tests)
|
|
249
|
+
keysTimeoutMs, // how long to wait for the keys; default 5,000 (a slow nano133.com never holds your first screen)
|
|
250
|
+
audience, // the origin to expect; default: this page's own
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`verifyTicket(token, { audience, keys })` checks a ticket you already hold, and `fetchKeys()`, `ticketFromHash(hash)` and `hashWithoutTicket(hash)` are the parts.
|
|
255
|
+
|
|
256
|
+
### The format
|
|
257
|
+
|
|
258
|
+
For any language. A ticket is `base64url(JSON claims) "." base64url(signature)`: an Ed25519 signature over the ASCII of the first part, made with the key whose `kid` the claims name.
|
|
259
|
+
|
|
260
|
+
```json
|
|
261
|
+
{ "v": 1, "typ": "ticket", "iss": "nano133.com", "kid": "fe812c12f3ab4ce6", "aud": "https://game.example",
|
|
262
|
+
"game": "gem-island", "sub": "nano_3i7m…", "name": "Ann",
|
|
263
|
+
"look": { "skin": "#f2c29b", "hair": "#3a2a1e", "shirt": "#2a9df4", "pants": "#1e2a4a", "shoes": "#ffffff" },
|
|
264
|
+
"record": { "v": 2, "k": [2359336, 1642] },
|
|
265
|
+
"items": ["hub.sun-hat"], "iat": 1800000000, "exp": 1800000120, "jti": "AAAAAAAAAAAAAAAA" }
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`record` is optional: `k` is the two numbers of `packRecord`. `GET https://nano133.com/api/nanoterra/keys` answers
|
|
269
|
+
`{ "keys": [{ "kid", "kty": "OKP", "crv": "Ed25519", "x", "alg": "EdDSA", "use": "sig" }] }` and may be read from any page.
|
|
270
|
+
A ticket lives two minutes (`exp - iat` is 120; a longer life is refused).
|
|
271
|
+
|
|
272
|
+
### Turn it on
|
|
273
|
+
|
|
274
|
+
Your `nano-game.json` says `"signin": "ticket"`. Nanoterra's Play key then sends signed-in players to your play address with a ticket.
|
|
275
|
+
A player who has not agreed, or who plays as a guest, arrives with no ticket.
|
|
276
|
+
|
|
277
|
+
## The looks ladder
|
|
278
|
+
|
|
279
|
+
`@nano133/nanoterra/looks` is the code behind the **Shelf** screen in the game starter's `/admin`. Use it in any game that sells looks for Nano (XNO).
|
|
280
|
+
It has no dependencies, runs in a Worker, a browser and Node, and never makes a network call. **It never takes part in a sale:** nothing here sends a price, a sale or a
|
|
281
|
+
player to nano133.com. Your game is the seller.
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
import { checkLooks, removalProblems, looksJson, PRICE_LADDER } from "@nano133/nanoterra/looks";
|
|
285
|
+
|
|
286
|
+
const looks = checkLooks(JSON.parse(fileText)); // throws an Error with the first mistake in plain words
|
|
287
|
+
const problems = removalProblems(looks, newLooks, ownedLookIds); // a look that a player owns must stay
|
|
288
|
+
const text = looksJson(newLooks); // the new looks.json: one look on each line
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
(A shop look here, `Look` of `/looks`, is a thing a player can buy. It is not the five colours `Look` of the small part: they live on different paths.)
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
type Look = {
|
|
295
|
+
id: string; // 1 to 40 lowercase letters, digits or dashes: "mint-cap"
|
|
296
|
+
name: string; // 1 to 40 characters
|
|
297
|
+
priceUsd: number; // US dollars, at most 2 decimals; 0 is the free look
|
|
298
|
+
change: { body: string; hat?: "cap" | "beanie" | "crown" | "tophat"; hatColor?: string }; // colours are "#rrggbb"
|
|
299
|
+
};
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
The rules (the same as the starter's `AGENTS.md`):
|
|
303
|
+
|
|
304
|
+
- A look only changes how the character looks. **It never changes a score, a speed or a rule.**
|
|
305
|
+
- Every look shows its price before the player buys. **Nothing is random: no loot boxes, no mystery looks.**
|
|
306
|
+
- The list has **at least one free look** (`priceUsd: 0`): every player starts with it.
|
|
307
|
+
- **Never change or remove the `id` of a look that someone owns.** Owned looks are kept by id. To stop selling a look, keep it in the file.
|
|
308
|
+
|
|
309
|
+
| | |
|
|
310
|
+
|---|---|
|
|
311
|
+
| `lookProblems(raw)` | Every mistake in a list of looks, as `{ look, field, message }`. An empty array is a good list. |
|
|
312
|
+
| `checkLooks(raw)` | The looks, or an `Error` with the first mistake in plain words. |
|
|
313
|
+
| `removalProblems(before, after, owned)` | The looks in `before` that a player owns (`owned` is a set of ids) and that `after` leaves out. |
|
|
314
|
+
| `diffLooks(before, after)` | `{ added, removed, changed }` by id. A new order is not a change. |
|
|
315
|
+
| `looksJson(looks)` | The text of `looks.json`, one look on each line, as the starter writes it. A change of one look is a change of one line. |
|
|
316
|
+
| `slugId(name, taken)` | A free id for a name: `"Mint Cap"` gives `mint-cap`, then `mint-cap-2`. |
|
|
317
|
+
| `PRICE_LADDER`, `rungOf(usd)`, `priceText(usd)` | The six steps nano133.com uses, from 1¢ to $1, and a price as a page writes it (`25¢`, `$1.10`). |
|
|
318
|
+
| `usdText(usd)`, `wholeCents(usd)`, `HATS` | `"$0.25"`; whether a price has no fraction of a cent; the hats. |
|
|
319
|
+
|
|
320
|
+
| Step | Price | For |
|
|
321
|
+
|---|---|---|
|
|
322
|
+
| dye | 1¢ | a colour |
|
|
323
|
+
| charm | 2¢ | a small mark or a badge |
|
|
324
|
+
| piece | 5¢ | one worn thing, like a hat or a scarf |
|
|
325
|
+
| statement | 25¢ | a bigger piece that people notice |
|
|
326
|
+
| set | 50¢ | a few pieces that go together |
|
|
327
|
+
| outfit | $1 | a whole look |
|
|
328
|
+
|
|
329
|
+
The ladder is a suggestion. Any price with at most 2 decimals, up to $1000, is good. `wholeCents` accepts every price in whole cents (the starter's old check,
|
|
330
|
+
`Math.round(x * 100) === x * 100`, refused $0.07, $0.29, $0.57 and $1.10 because of floating point).
|
|
331
|
+
|
|
332
|
+
## Moving from the three old packages
|
|
333
|
+
|
|
334
|
+
| Before | Now |
|
|
335
|
+
|---|---|
|
|
336
|
+
| `npm i @nano133/character` | `npm i @nano133/nanoterra three` |
|
|
337
|
+
| `import { createCharacter } from "@nano133/character"` | `import { createCharacter } from "@nano133/nanoterra/character"` |
|
|
338
|
+
| `import { defaultLook, isLook, LOOK_PARTS } from "@nano133/character"` | `import { defaultLook, isLook, LOOK_PARTS } from "@nano133/nanoterra/character/record"` (no Three.js) |
|
|
339
|
+
| `createCharacter({ look })` | the same; or `createCharacter({ record })` for the whole character |
|
|
340
|
+
| `scene.add(c.object, c.shadow)` | `scene.add(c.object)`: the shadow is a disc in the body and stays on the ground by itself (`c.shadow` is gone) |
|
|
341
|
+
| `c.setLook({ ...look, shirt: "#c8461a" })` (a colour not on the island's lists) | `c.setColors({ shirt: "#c8461a" })`. `setLook` takes a record or the five colours of the lists |
|
|
342
|
+
| `import { readTicket } from "@nano133/ticket"` | `import { readTicket } from "@nano133/nanoterra/ticket"`: the same answer, and `ticket.record` when the ticket has one |
|
|
343
|
+
| `import { checkLooks } from "@nano133/looks"` | `import { checkLooks } from "@nano133/nanoterra/looks"` |
|
|
344
|
+
|
|
345
|
+
## What it does NOT do yet
|
|
346
|
+
|
|
347
|
+
- Worn items (see above): 0.2.0.
|
|
348
|
+
- The katana, the saya, the headband and the dojo look of the island's Ananos game. (The skeleton keeps their bones, with no vertices.)
|
|
349
|
+
- First-person arms, swimming, climbing, sitting and the room's other moves: only idle, walk, run, jump and wave.
|
|
350
|
+
- Shared geometry for many characters at once, and a react-three-fiber wrapper.
|
|
351
|
+
- `WebGPURenderer`: the ink is a GLSL `ShaderMaterial`, so use `WebGLRenderer`.
|
|
352
|
+
- Any server or Nano code. This package draws a character and reads a ticket; it moves no money.
|
|
353
|
+
|
|
354
|
+
## Build and test
|
|
355
|
+
|
|
356
|
+
```sh
|
|
357
|
+
npm install # at the kit's root
|
|
358
|
+
npm run build -w @nano133/nanoterra
|
|
359
|
+
npm test -w @nano133/nanoterra
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
MIT license.
|
package/ai/PROMPT.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Nanoterra for a game: the prompt
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
Add Nanoterra to this game with the npm package @nano133/nanoterra and follow its ai/SPEC.md
|
|
5
|
+
step by step: https://nano133.com/build/nanoterra
|
|
6
|
+
Show the player's own Nanoterra character in the Three.js scene with createCharacter from
|
|
7
|
+
@nano133/nanoterra/character: add its object to the scene and call update(dt, { speed, airborne,
|
|
8
|
+
verticalSpeed }) once each frame with the game's own numbers (dt in seconds). Keep the choice
|
|
9
|
+
between the Nanoterra character and the game's own in ONE place (the starter's game.config.json,
|
|
10
|
+
"character": "nanoterra" or "own"). Use the same collider, jump and reach for every body.
|
|
11
|
+
If this game is a static site, read the sign-in ticket once with readTicket from
|
|
12
|
+
@nano133/nanoterra/ticket, ask "Sign in as <name>? Continue / Not me" first, and pass
|
|
13
|
+
ticket.record and ticket.look to createCharacter. Never give a sale, a prize or a score on a ticket
|
|
14
|
+
alone. Check every record with recordProblem and every look with isLook before it reaches the
|
|
15
|
+
character. Call dispose when a character leaves the game. Ask me which quality (high or low) to use.
|
|
16
|
+
```
|
package/ai/SPEC.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Nanoterra for a game: implementation spec
|
|
2
|
+
|
|
3
|
+
Package: `@nano133/nanoterra` (0.1.0 or later). Follow these steps in order. Each step ends with a check. Ask the owner when a step says
|
|
4
|
+
"ask". The package has three parts, on three paths: the sign-in ticket (`/ticket`), the player's character (`/character`, Three.js) and the
|
|
5
|
+
looks ladder (`/looks`). Use only the parts the game needs. It has no network, wallet or Nano code. Read the package README first.
|
|
6
|
+
|
|
7
|
+
## 0. What the owner decides (ask)
|
|
8
|
+
|
|
9
|
+
1. Does the game have a server? If yes, sign the player in with the code exchange (the game starter has it) and do **not** use `/ticket`: a ticket cannot prove
|
|
10
|
+
who is at the other end. If no (a static site), use `/ticket`.
|
|
11
|
+
2. Where the character comes from: the player's own Nanoterra character (the default), or the game's own character. Keep that choice in **one** place:
|
|
12
|
+
in the game starter it is `"character": "nanoterra" | "own"` in `game.config.json` (nothing else reads it).
|
|
13
|
+
3. The quality: `"high"` (toon body and ink outline, 2 draws) or `"low"` (no outline, 1 draw, for slow phones).
|
|
14
|
+
4. How the game moves the character: speed in metres a second, gravity and jump. The package animates; it does not move the object.
|
|
15
|
+
5. Does the game sell looks? If yes, the game is the seller (its own checkout); see step 7.
|
|
16
|
+
|
|
17
|
+
**Check:** the owner has named the server (yes or no), the source of the character and the quality.
|
|
18
|
+
|
|
19
|
+
## 1. Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install @nano133/nanoterra three
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`three` is an optional peer (`>=0.174.0`): leave it out for a game that only reads a ticket.
|
|
26
|
+
|
|
27
|
+
**Check:** `import { createCharacter } from "@nano133/nanoterra/character"` builds and runs.
|
|
28
|
+
|
|
29
|
+
## 2. The ticket (a static site only)
|
|
30
|
+
|
|
31
|
+
In `nano-game.json` set `"signin": "ticket"`. Call once, when the page loads:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { readTicket } from "@nano133/nanoterra/ticket";
|
|
35
|
+
const result = await readTicket();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- It takes the ticket out of the address bar first. If `result.ok` is false the player is a guest (`result.reason` is `"none"` for a plain visit).
|
|
39
|
+
- Show "Sign in as *name*? Continue / Not me" before you treat the player as that name. Anyone can send a link that holds their own ticket. Put the name in the page as text, never as HTML.
|
|
40
|
+
- `result.ticket`: `username`, `address`, `look` (five `#rrggbb` colours), `record` (the whole character, when the ticket has one), `items` (catalogue ids).
|
|
41
|
+
- **Never give a sale, a prize or a score on a ticket alone.**
|
|
42
|
+
|
|
43
|
+
**Check:** a plain visit makes no network call; a ticket for another origin, a changed ticket and an old ticket each give a guest; the address bar has no ticket after the page loaded.
|
|
44
|
+
|
|
45
|
+
## 3. The scene
|
|
46
|
+
|
|
47
|
+
- Draw with no tone mapping (the Three.js default). The toon material needs at least one light. The island's look is a
|
|
48
|
+
`HemisphereLight("#d6e9ff", "#e2b98c", 1.8)` and a `DirectionalLight("#fff0c8", 2.75)` from up, front and to one side.
|
|
49
|
+
- The character is 2.29 m tall (`HEIGHT`) for the default Classic. Size the game's world to metres, or scale `object` and pass `speed / scale` to `update`.
|
|
50
|
+
|
|
51
|
+
**Check:** a character added to the scene shows its toon colours and an outline.
|
|
52
|
+
|
|
53
|
+
## 4. Make the character
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { createCharacter } from "@nano133/nanoterra/character";
|
|
57
|
+
const c = createCharacter({ record: ticket.record, look: ticket.look, quality: "high" }); // a guest: createCharacter()
|
|
58
|
+
scene.add(c.object);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- `c.object` has its feet at y = 0 and faces +Z. Set `c.object.position` to the player's feet and `c.object.rotation.y = Math.atan2(dx, dz)` for a direction.
|
|
62
|
+
- The blob shadow is in the body. It lies on the ground under a jump by itself. There is no `c.shadow`.
|
|
63
|
+
- A record that comes from a ticket, a server or a file is data you did not write. `createCharacter` throws a `TypeError` for one that is not whole; check it first
|
|
64
|
+
with `recordProblem(record)` (the reason as a sentence, or `null`) from `@nano133/nanoterra/character/record`. A look: `isLook(look)` or `lookProblem(look)`.
|
|
65
|
+
- The body is **display only**. Use the same collider, jump and reach for every base and size: a player's choice of body must never change a rule. For the Classic, `RADIUS` is 0.49 m.
|
|
66
|
+
|
|
67
|
+
**Check:** the character stands on the ground, faces the way it walks, and its shadow lies under it.
|
|
68
|
+
|
|
69
|
+
## 5. Drive it
|
|
70
|
+
|
|
71
|
+
Call once each frame, before the render:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
c.update(dt, { speed, airborne, verticalSpeed });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- `dt` is in seconds, not milliseconds. A frame longer than 0.05 s counts as 0.05 s.
|
|
78
|
+
- `speed`: metres a second over the ground. 0 is idle, from 0.25 a walk (full swing at 3), from 3.4 to 5 a run. The island walks at 3.75 and runs at 6.5.
|
|
79
|
+
- `airborne`: true while off the ground. `verticalSpeed`: metres a second, up is positive; it sizes the shadow in a jump.
|
|
80
|
+
- Set `c.reducedMotion = true` when the player asks for less motion. `c.wave()` waves the left arm for 1.8 s.
|
|
81
|
+
|
|
82
|
+
**Check:** idle, walk, run and jump each look different, on every base. The legs do not slide on the ground at the game's walking speed.
|
|
83
|
+
|
|
84
|
+
## 6. Change and clean up
|
|
85
|
+
|
|
86
|
+
- `c.setLook(record)` (or five colours): the same body only repaints; another base or clothes builds new geometry and frees the old.
|
|
87
|
+
- `c.setColors({ shirt: "#c8461a" })`: any colour over the record's (a shop dye). `c.setColors(null)` gives the record's own back.
|
|
88
|
+
- `c.dispose()` when a character leaves the game: it frees the geometries, materials and textures that it made.
|
|
89
|
+
|
|
90
|
+
**Check:** after `dispose`, the renderer's `info.memory` counts fall.
|
|
91
|
+
|
|
92
|
+
## 7. Looks (only if the game sells them)
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { checkLooks, lookProblems, removalProblems, diffLooks, looksJson } from "@nano133/nanoterra/looks";
|
|
96
|
+
export const LOOKS = checkLooks(rawLooksJson); // throws with the first mistake in plain words
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- Before a change goes live: `[...lookProblems(next), ...removalProblems(current, next, ownedLookIds)]`; show `diffLooks(current, next)`; write the file with `looksJson(next)`.
|
|
100
|
+
- One free look (`priceUsd: 0`) at least. Never change or remove the id of a look that someone owns. Prices in US dollars with at most 2 decimals; the server reads the price from the file.
|
|
101
|
+
- A look only changes how the character looks. No random look, no loot box, no look that changes a score.
|
|
102
|
+
- The game is the seller. Never send looks, prices or sales to another site.
|
|
103
|
+
|
|
104
|
+
**Check:** `checkLooks` passes at start; a removed look that is owned is refused.
|
|
105
|
+
|
|
106
|
+
## 8. Do not
|
|
107
|
+
|
|
108
|
+
- Do not edit the geometry, the skeleton or the toon and ink materials.
|
|
109
|
+
- Do not add worn items, the katana, the headband or other brands' art: 0.1.0 has none of them (a later version adds the item art).
|
|
110
|
+
- Do not use a look or a record for a game's rules, a score or a payment: it is display only.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import type { Made } from "./make.js";
|
|
3
|
+
import { type Mover } from "./motion.js";
|
|
4
|
+
export declare function assemble(made: Made, mats: {
|
|
5
|
+
toon: THREE.Material;
|
|
6
|
+
ink: THREE.Material;
|
|
7
|
+
}): {
|
|
8
|
+
group: THREE.Group<THREE.Object3DEventMap>;
|
|
9
|
+
mesh: THREE.SkinnedMesh<THREE.BufferGeometry<THREE.NormalBufferAttributes, THREE.BufferGeometryEventMap>, THREE.Material<THREE.MaterialEventMap>, THREE.Object3DEventMap>;
|
|
10
|
+
hullMesh: THREE.SkinnedMesh<THREE.BufferGeometry<THREE.NormalBufferAttributes, THREE.BufferGeometryEventMap>, THREE.Material<THREE.MaterialEventMap>, THREE.Object3DEventMap>;
|
|
11
|
+
skeleton: THREE.Skeleton;
|
|
12
|
+
list: THREE.Bone<THREE.Object3DEventMap>[];
|
|
13
|
+
bones: Record<"head" | "root" | "hips" | "spine" | "chest" | "neck" | "clavicle_L" | "upperarm_L" | "forearm_L" | "hand_L" | "clavicle_R" | "upperarm_R" | "forearm_R" | "hand_R" | "grip_R" | "thigh_L" | "shin_L" | "foot_L" | "thigh_R" | "shin_R" | "foot_R" | "saya" | "weapon" | "band_L1" | "band_L2" | "band_R1" | "band_R2" | "shadow", THREE.Bone<THREE.Object3DEventMap>>;
|
|
14
|
+
named: Map<string, THREE.Bone<THREE.Object3DEventMap>>;
|
|
15
|
+
mover: Mover | null;
|
|
16
|
+
};
|
|
17
|
+
export type Assembled = ReturnType<typeof assemble>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { makeMover } from "./motion.js";
|
|
3
|
+
import { BONES, makeBones } from "./skeleton.js";
|
|
4
|
+
// A character's skinned meshes on the island's skeleton. The host brings the two materials (a toon material with vertex colours, and the
|
|
5
|
+
// ink's hull material) and its own solver for the 28 bones; this puts the bones together (the 28, then the character's own), binds ONE skinned
|
|
6
|
+
// toon mesh and ONE skinned ink mesh to them, and makes the function that turns the character's own bones.
|
|
7
|
+
/** The avatar's bound for the frustum test, in the avatar's own space (its feet at the origin): 3 m round a point 1 m up. */
|
|
8
|
+
const CULL_SPHERE = new THREE.Sphere(new THREE.Vector3(0, 1, 0), 3);
|
|
9
|
+
export function assemble(made, mats) {
|
|
10
|
+
const { list, byName } = makeBones();
|
|
11
|
+
// The character's own bones: each under its parent (an avatar bone, or an earlier extra one), at its rest place.
|
|
12
|
+
const named = new Map(list.map((b) => [b.name, b]));
|
|
13
|
+
const placeOf = new Map(BONES.map((b) => [b[0], [b[2], b[3], b[4]]]));
|
|
14
|
+
const extraBones = made.extra.map((e) => {
|
|
15
|
+
const bone = new THREE.Bone();
|
|
16
|
+
bone.name = e.name;
|
|
17
|
+
const at = placeOf.get(e.parent);
|
|
18
|
+
bone.position.set(e.at[0] - at[0], e.at[1] - at[1], e.at[2] - at[2]);
|
|
19
|
+
// Where it rests, for a part that moves about its place (a buddy that floats, a pack's strap): the island's wear bones keep this too.
|
|
20
|
+
bone.userData.rest = bone.position.clone();
|
|
21
|
+
named.get(e.parent).add(bone);
|
|
22
|
+
named.set(e.name, bone);
|
|
23
|
+
placeOf.set(e.name, e.at);
|
|
24
|
+
return bone;
|
|
25
|
+
});
|
|
26
|
+
const mesh = new THREE.SkinnedMesh(made.body, mats.toon);
|
|
27
|
+
const hullMesh = new THREE.SkinnedMesh(made.hull, mats.ink);
|
|
28
|
+
mesh.add(byName.root);
|
|
29
|
+
const group = new THREE.Group();
|
|
30
|
+
group.add(mesh, hullMesh);
|
|
31
|
+
group.updateMatrixWorld(true);
|
|
32
|
+
const skeleton = new THREE.Skeleton(extraBones.length ? [...list, ...extraBones] : list);
|
|
33
|
+
mesh.bind(skeleton);
|
|
34
|
+
hullMesh.bind(skeleton);
|
|
35
|
+
// It moves every frame, so three's own bound (the bind pose) is stale: a fixed sphere holds every pose.
|
|
36
|
+
mesh.boundingSphere = hullMesh.boundingSphere = CULL_SPHERE;
|
|
37
|
+
const mover = makeMover(named, made.extra, group);
|
|
38
|
+
return { group, mesh, hullMesh, skeleton, list, bones: byName, named, mover };
|
|
39
|
+
}
|