js-game-zero 0.0.0-stage → 0.4.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.
Files changed (53) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +138 -2
  3. package/dist/active-service.d.ts +21 -0
  4. package/dist/actor.d.ts +182 -0
  5. package/dist/animation.d.ts +61 -0
  6. package/dist/assets.d.ts +148 -0
  7. package/dist/audio.d.ts +150 -0
  8. package/dist/browser-asset-backends.d.ts +8 -0
  9. package/dist/browser.d.ts +10 -0
  10. package/dist/chunk-q9m20c2h.js +7972 -0
  11. package/dist/clock.d.ts +29 -0
  12. package/dist/color.d.ts +15 -0
  13. package/dist/events.d.ts +145 -0
  14. package/dist/friendly-names.d.ts +43 -0
  15. package/dist/geometry.d.ts +125 -0
  16. package/dist/helpers.d.ts +28 -0
  17. package/dist/host.d.ts +58 -0
  18. package/dist/host.js +435 -0
  19. package/dist/index.d.ts +340 -0
  20. package/dist/index.js +258 -0
  21. package/dist/input.d.ts +200 -0
  22. package/dist/inspection.d.ts +58 -0
  23. package/dist/jgz.js +8545 -0
  24. package/dist/node.d.ts +14 -0
  25. package/dist/node.js +145 -0
  26. package/dist/pixel-density.d.ts +23 -0
  27. package/dist/random.d.ts +31 -0
  28. package/dist/reference/actor.md +197 -0
  29. package/dist/reference/audio.md +128 -0
  30. package/dist/reference/clock.md +150 -0
  31. package/dist/reference/helpers.md +46 -0
  32. package/dist/reference/hooks.md +379 -0
  33. package/dist/reference/index.d.ts +2 -0
  34. package/dist/reference/index.js +39 -0
  35. package/dist/reference/loaders.md +164 -0
  36. package/dist/reference/rect.md +129 -0
  37. package/dist/reference/runtime.md +478 -0
  38. package/dist/reference/screen.md +392 -0
  39. package/dist/reference/storage.md +90 -0
  40. package/dist/rendering.d.ts +216 -0
  41. package/dist/runtime-contracts.d.ts +117 -0
  42. package/dist/runtime-diagnostics.d.ts +37 -0
  43. package/dist/runtime-persistence.d.ts +32 -0
  44. package/dist/runtime-source.d.ts +82 -0
  45. package/dist/runtime.d.ts +81 -0
  46. package/dist/source-scan.d.ts +49 -0
  47. package/dist/storage-path.d.ts +3 -0
  48. package/dist/storage.d.ts +88 -0
  49. package/dist/tone.d.ts +81 -0
  50. package/dist/touch-controls.d.ts +24 -0
  51. package/dist/touch-pointer.d.ts +11 -0
  52. package/dist/util.d.ts +62 -0
  53. package/package.json +69 -4
@@ -0,0 +1,392 @@
1
+ # Screen And Drawing
2
+
3
+ `screen` is the global canvas surface. Everything you draw happens inside
4
+ `draw()`, in screen pixels: `[0, 0]` is the top-left corner, x grows right,
5
+ y grows down, and `[WIDTH, HEIGHT]` is the bottom-right corner.
6
+
7
+ ## Short forms
8
+
9
+ Six short calls draw the most common shapes and text, with each position
10
+ given as separate numbers rather than an `[x, y]` list:
11
+
12
+ | Call | Draws |
13
+ | -------------------------------------------------------- | ------------------------------------------------------- |
14
+ | `screen.circle(x, y, radius, color)` | a filled circle centered on `x, y` |
15
+ | `screen.rect(x, y, width, height, color)` | a filled rectangle whose top-left corner is `x, y` |
16
+ | `screen.roundedRect(x, y, width, height, radius, color)` | the same rectangle with its corners rounded `radius` px |
17
+ | `screen.triangle(x1, y1, x2, y2, x3, y3, color)` | a filled triangle with those three corners |
18
+ | `screen.line(x1, y1, x2, y2, color, width = 1)` | a line from `x1, y1` to `x2, y2`, `width` px thick |
19
+ | `screen.text(text, x, y, color, size = 24)` | text whose top-left corner is `x, y`, `size` tall |
20
+
21
+ `color` is optional and defaults to `"white"`; it takes any of the
22
+ [colors](#colors) below.
23
+
24
+ ```js
25
+ function draw() {
26
+ screen.clear();
27
+ screen.rect(0, 220, 400, 80, "green");
28
+ screen.roundedRect(280, 20, 100, 40, 12, "purple");
29
+ screen.circle(200, 150, 25, "gold");
30
+ screen.triangle(160, 40, 120, 120, 200, 120);
31
+ screen.line(20, 20, 200, 120, "white", 3);
32
+ screen.text("Score: " + score, 20, 30);
33
+ }
34
+ ```
35
+
36
+ `screen.text` takes words in quotes or a number. To place text by its
37
+ center, wrap it or give it an outline, use
38
+ [`screen.draw.text`](#screendrawtexttext-pos-options--screendrawtexttext-options).
39
+
40
+ The short forms fill their shapes. The outline versions, and the versions
41
+ that take `[x, y]` lists, stay under `screen.draw` below. Mind the names:
42
+ **`screen.circle` fills, but `screen.draw.circle` draws only an outline**,
43
+ and the same goes for `screen.rect` and `screen.draw.rect`, and for
44
+ `screen.roundedRect` and `screen.draw.roundedRect`.
45
+
46
+ A wrong call throws a `TypeError` that says what to change. A list, as in
47
+ `screen.circle([200, 150], 25)`, points to the `screen.draw` method that
48
+ takes lists (here `screen.draw.filledCircle`). A missing or non-number
49
+ argument is named, and an extra one, such as an outline width, points to
50
+ the outline method. `screen.roundedRect` takes its radius before its color,
51
+ as `screen.circle` does, and a color where the radius belongs says so.
52
+
53
+ ## Colors
54
+
55
+ Every drawing method accepts a `Color`, which is either a CSS color string
56
+ (`"white"`, `"#0f172a"`, `"rgb(20, 30, 40)"`) or an RGB/RGBA tuple of
57
+ integers `0`–`255`: `[249, 115, 22]` or `[249, 115, 22, 128]` (the fourth
58
+ value is alpha out of 255, not a `0`–`1` fraction). Passing anything else
59
+ throws a `TypeError` that says what was wrong:
60
+
61
+ - A misspelled color name suggests the right one: `"lightblu"` fails with
62
+ `Did you mean "lightblue"?`. (A canvas would otherwise ignore the color
63
+ and keep drawing in the previous one.)
64
+ - A hex code needs 3 or 6 hex digits (or 4 or 8, with alpha).
65
+ - A fractional alpha such as `[255, 0, 0, 0.5]` fails with the 0–255 value
66
+ to use instead (`128`).
67
+
68
+ Drawing options are checked the same way: an option a method doesn't have,
69
+ such as `{ colour: "red" }`, throws and names the closest real option.
70
+
71
+ ### `rgb(red, green, blue, alpha?)`
72
+
73
+ Mixes a color from red, green and blue light, each `0`–`255`, with an
74
+ optional alpha out of 255. It returns the matching list, so
75
+ `rgb(255, 140, 105)` is `[255, 140, 105]` and works anywhere a color does.
76
+ It's ready to use in a game file; in import mode, import it from
77
+ `js-game-zero`.
78
+
79
+ ```js
80
+ let coral = rgb(255, 140, 105);
81
+ let glass = rgb(120, 200, 255, 128); // half see-through
82
+ screen.circle(200, 150, 25, coral);
83
+ ```
84
+
85
+ Its numbers are checked like a list's, with the same hints (a fractional
86
+ alpha such as `0.5` names `128` instead), and a missing one is named:
87
+ `rgb(255, 140)` fails with `rgb is missing blue`.
88
+
89
+ ## `screen.clear()`
90
+
91
+ Resets the whole canvas to opaque black.
92
+
93
+ ```js
94
+ function draw() {
95
+ screen.clear();
96
+ }
97
+ ```
98
+
99
+ ## `screen.fill(color, bottomColor?)`
100
+
101
+ Fills the canvas with a solid color, or a top-to-bottom gradient when a
102
+ second color is given: `color` at the top fading to `bottomColor` at the
103
+ bottom.
104
+
105
+ ```js
106
+ screen.fill("#0f172a"); // solid
107
+ screen.fill("#0f172a", "#164e63"); // gradient, dark at top to teal at bottom
108
+ ```
109
+
110
+ ## `screen.draw.image(image, pos)` / `screen.blit(image, pos)`
111
+
112
+ `screen.draw.image` and `screen.blit` (Pygame Zero's name) do the same
113
+ thing. Draws an already-loaded image (by asset name, or a `SurfaceLike` you already
114
+ hold) at a position. `pos` is a `[x, y]` point for the image's top-left
115
+ corner, or a rect-like value (its top-left corner is used; blit does not
116
+ scale the image to the rect's size).
117
+
118
+ ```js
119
+ screen.draw.image("player.svg", [40, 40]);
120
+ ```
121
+
122
+ If the named image hasn't finished loading yet, nothing is drawn: it returns
123
+ a `Promise` that resolves once the image has loaded, and the runtime holds
124
+ the frames that follow until then, so the image shows from the next one.
125
+ If it can't be loaded, the promise rejects and the run reports the missing
126
+ image by name, so `draw()` needn't handle the promise itself.
127
+ Images touched while the script starts up load before the first frame (see
128
+ [`docs/reference/runtime.md`](runtime.md#preload-gate)).
129
+ Prefer `Actor` for anything that moves or needs collision — drawing an image
130
+ directly is for background art and tiles.
131
+
132
+ ## `screen.bounds()`
133
+
134
+ Returns a `ZRect` covering the whole canvas (`[0, 0, WIDTH, HEIGHT]`), handy
135
+ for clamping or collision against the screen edge.
136
+
137
+ ```js
138
+ if (!screen.bounds().contains(player.rect()))
139
+ player.center = [WIDTH / 2, HEIGHT / 2];
140
+ ```
141
+
142
+ ## `PIXEL_DENSITY` and `screen.density`
143
+
144
+ On a phone, one screen pixel covers several of the device's own pixels.
145
+ The canvas has a pixel for each of them, so text, outlines and SVG pictures
146
+ stay sharp. `screen.density` says how many canvas pixels make up one screen
147
+ pixel along each side: between 2 and 3 on most phones and 1 on a computer
148
+ monitor. Your game's numbers don't change with it: a game 360 pixels wide
149
+ is 360 wide at any density.
150
+
151
+ Set `PIXEL_DENSITY` next to `WIDTH` and `HEIGHT` to choose the density
152
+ yourself. Without it, the game uses the device's own, up to 3.
153
+
154
+ ```js
155
+ WIDTH = 360;
156
+ HEIGHT = 640;
157
+ PIXEL_DENSITY = 1; // one canvas pixel per screen pixel, even on a phone
158
+ ```
159
+
160
+ See [pixel density](runtime.md#pixel-density) for how hosts size and read
161
+ the canvas.
162
+
163
+ ## `screen.draw.line(start, end, color, width = 1)`
164
+
165
+ ```js
166
+ screen.draw.line([20, 20], [200, 120], "white", 3);
167
+ ```
168
+
169
+ ## `screen.draw.circle(pos, radius, color, width = 1)` / `filledCircle(pos, radius, color)`
170
+
171
+ `screen.draw.circle` draws an outline; `filledCircle` and the short form
172
+ [`screen.circle`](#short-forms) fill.
173
+
174
+ ```js
175
+ screen.draw.circle([160, 90], 40, "#38bdf8", 2); // outline
176
+ screen.draw.filledCircle([160, 90], 40, "#38bdf8"); // filled
177
+ ```
178
+
179
+ ## `screen.draw.rect(rect, color, width = 1)` / `filledRect(rect, color)`
180
+
181
+ `rect` accepts anything geometry accepts: a `Rect`, an `Actor` (via its
182
+ `.rect()` duck type — see [`docs/reference/rect.md`](rect.md)), or a plain
183
+ `[left, top, width, height]` tuple.
184
+
185
+ ```js
186
+ screen.draw.filledRect([40, 40, 120, 60], "#f97316");
187
+ screen.draw.rect(player.rect(), "yellow", 2); // highlight a hitbox
188
+ ```
189
+
190
+ ## `screen.draw.roundedRect(rect, radius, color, width = 1)` / `filledRoundedRect(rect, radius, color)`
191
+
192
+ A rectangle with its corners rounded: each corner is a quarter of a circle
193
+ `radius` pixels round. `rect` takes what `screen.draw.rect` takes, and the
194
+ radius comes before the color, as a circle's does. `roundedRect` draws an
195
+ outline, `width` pixels thick; `filledRoundedRect` and the short form
196
+ [`screen.roundedRect`](#short-forms) fill.
197
+
198
+ ```js
199
+ screen.draw.filledRoundedRect([40, 40, 160, 48], 12, "#f97316"); // a button
200
+ screen.draw.roundedRect([40, 40, 160, 48], 12, "white", 2); // its border
201
+ screen.draw.filledRoundedRect([40, 110, 160, 30], 15, "#22c55e"); // a pill
202
+ ```
203
+
204
+ A radius of `0` gives square corners. A radius bigger than half the
205
+ rectangle's height (or width, if that's shorter) is drawn as that half, so
206
+ its short sides are half circles: a pill, or a circle for a square. A
207
+ negative radius, or a color where the radius belongs, throws a `TypeError`
208
+ that says what to change.
209
+
210
+ ## `screen.draw.polygon(points, color, width = 1)` / `filledPolygon(points, color)`
211
+
212
+ Needs at least three points.
213
+
214
+ ```js
215
+ screen.draw.filledPolygon(
216
+ [
217
+ [238, 40],
218
+ [292, 64],
219
+ [270, 126],
220
+ [214, 118],
221
+ ],
222
+ "#f8fafc",
223
+ );
224
+ ```
225
+
226
+ ## `screen.draw.text(text, pos?, options?)` / `screen.draw.text(text, options)`
227
+
228
+ Draws text with a large set of beginner-friendly options — this is a
229
+ JavaScript-native reimplementation of pgzero's `ptext` module, including its
230
+ word-wrapping algorithm and gradient/outline/shadow effects.
231
+
232
+ ```js
233
+ screen.draw.text("Score: 12", [16, 16], {
234
+ size: 24,
235
+ color: "white",
236
+ });
237
+ ```
238
+
239
+ When the options say where the text goes (`center`, `topRight`, ...), pass
240
+ them second, in place of the position:
241
+
242
+ ```js
243
+ screen.draw.text("Game Over", { center: [WIDTH / 2, HEIGHT / 2], size: 40 });
244
+ ```
245
+
246
+ Several options have two names: a readable one, and the shorter one Pygame
247
+ Zero uses. Either works, but not both at once:
248
+
249
+ | Readable name | Pygame Zero name |
250
+ | --------------- | ---------------- |
251
+ | `size` | `fontsize` |
252
+ | `outline` | `owidth` |
253
+ | `outlineColor` | `ocolor` |
254
+ | `shadowColor` | `scolor` |
255
+ | `gradientColor` | `gcolor` |
256
+
257
+ ### Position options
258
+
259
+ Give **exactly one** of the following ways to position the text (mixing
260
+ them throws a `TypeError`):
261
+
262
+ - `pos` (or the second positional argument) — a `[x, y]` point. Combined
263
+ with `anchor` (default `"topLeft"`) to decide which part of the text box
264
+ sits at that point.
265
+ - A named anchor point directly: `topLeft`, `topRight`, `bottomLeft`,
266
+ `bottomRight`, `midTop`, `midLeft`, `midRight`, `midBottom`, `center` —
267
+ each takes a `[x, y]` point for that corner/edge/center of the text.
268
+ - Scalar axis keywords, combined per-axis: `left`/`centerX`/`right` for the
269
+ horizontal position, `top`/`centerY`/`bottom` for the vertical position.
270
+ You can mix one horizontal and one vertical scalar (`{ right: 300, top: 10
271
+ }`), or use just one axis and fall back to `pos` for the other.
272
+
273
+ ```js
274
+ screen.draw.text("Game Over", {
275
+ center: [WIDTH / 2, HEIGHT / 2],
276
+ size: 40,
277
+ });
278
+ screen.draw.text("HUD", { right: WIDTH - 10, top: 10 }); // top-right corner, 10px margin
279
+ ```
280
+
281
+ ### Font options
282
+
283
+ - `size` / `fontsize` (default `24`)
284
+ - `fontname` — the stem of a font asset under `fonts/` (e.g. `"pixel"` for
285
+ `fonts/pixel.ttf`); loads asynchronously the first time it's used.
286
+ - `sysfontname` — a literal CSS font family name instead (e.g. `"Georgia"`).
287
+ Mutually exclusive with `fontname`.
288
+ - `bold`, `italic`, `underline` — booleans.
289
+ - `color` (default `"white"`), `background` (fills a box behind the text).
290
+ - `gradientColor` / `gcolor` — a second color for a top-to-bottom gradient
291
+ across the text.
292
+
293
+ ### Layout options
294
+
295
+ - `width` — wraps text to fit this many pixels; without it, text is only
296
+ broken at explicit newlines (`\n`). Wrapping is greedy word-wrap: a single
297
+ word longer than `width` is placed on its own (overflowing) line rather
298
+ than being cut.
299
+ - `widthem` — like `width`, but expressed as a multiple of `size`.
300
+ - `lineheight` — pixels between lines (default `size * 1.2`).
301
+ - `align` — `"left"` (default), `"center"`, or `"right"`, for multi-line text.
302
+ A [`textbox`](#screendrawtextboxtext-rect-options) follows its `anchor`
303
+ instead, so it centers by default.
304
+ - `strip` — trims trailing spaces from each wrapped line (default `true`).
305
+ - `anchor` — overrides which part of the text's bounding box sits at the
306
+ resolved position; either one of the named anchor strings above, or a
307
+ `[horizontal, vertical]` pair where each side is `"left"`/`"center"`/`"right"`,
308
+ `"top"`/`"center"`/`"bottom"`, or a literal pixel-offset number.
309
+ - `angle` — rotates the text counterclockwise around the anchor point, in
310
+ degrees.
311
+
312
+ ### Effects
313
+
314
+ - `outline` / `owidth` + `outlineColor` / `ocolor` — an outline stroke
315
+ around the glyphs, `outline` pixels thick.
316
+ - `shadow` (a number or `[x, y]` offset) + `shadowColor` / `scolor`
317
+ (default `"black"`) — a drop shadow.
318
+ - `alpha` — overall opacity, `0`–`1` (default `1`).
319
+
320
+ ## `screen.draw.textbox(text, rect, options?)`
321
+
322
+ Like `draw.text`, but instead of a fixed `size` you give a rectangle and
323
+ `textbox` picks the **largest font size that fits inside it** (shrinking
324
+ down to `minSize`, default `4`, if necessary; `minFontsize` is another name
325
+ for it, but not both at once). Text is centered in the rect by default.
326
+ Pass `anchor` (e.g. `"topLeft"`) to place it elsewhere in the rect; its
327
+ lines line up the same way (on the left for `"topLeft"`) unless you give
328
+ `align`. The rect places the text, so `textbox` takes none of
329
+ `draw.text`'s position options (`pos`, `center`, `topLeft`, `left`, ...)
330
+ and no `width`; giving one throws a `TypeError` that says so.
331
+
332
+ ```js
333
+ screen.draw.textbox("You win!", [40, 60, 240, 80], {
334
+ color: "white",
335
+ size: 48, // upper bound; shrinks to fit if "You win!" would overflow
336
+ });
337
+ ```
338
+
339
+ ## `screen.measureText(text, options?)`
340
+
341
+ How big `screen.draw.text` would draw some text. It returns
342
+ `{ width, height, lines }`, in screen pixels, and draws nothing:
343
+
344
+ - `width` — the widest line's width.
345
+ - `height` — the lines' height together: one `lineheight` (`size * 1.2`
346
+ unless given) for each line.
347
+ - `lines` — the lines the text breaks into, at each `\n` and, with a
348
+ `width` option, where it wraps.
349
+
350
+ It takes the options `screen.draw.text` takes (`size`, `fontname`,
351
+ `sysfontname`, `bold`, `italic`, `width`, `widthem`, `lineheight`,
352
+ `strip`) and measures the text the way `screen.draw.text` lays it out
353
+ with them. Options that only place or color the text (`center`, `color`,
354
+ `outline`, `angle`, ...) are allowed too, and change nothing, so you can
355
+ measure a label with the same options you draw it with. A position before
356
+ the options, as in `screen.draw.text(text, [x, y], options)`, is allowed
357
+ for the same reason.
358
+
359
+ ```js
360
+ let options = { midLeft: [40, 200], size: 20 };
361
+ screen.draw.text("Play again", options);
362
+ let label = screen.measureText("Play again", options);
363
+ // A dot just right of the label, whatever its font and size.
364
+ screen.circle(40 + label.width + 12, 200, 5, "gold");
365
+ ```
366
+
367
+ Measuring a button's label tells you how big to draw the button:
368
+
369
+ ```js
370
+ let size = screen.measureText("Start", { size: 24 });
371
+ let box = [100, 100, size.width + 32, size.height + 16];
372
+ screen.draw.filledRoundedRect(box, 12, "#2563eb");
373
+ screen.draw.text("Start", {
374
+ center: [100 + box[2] / 2, 100 + box[3] / 2],
375
+ size: 24,
376
+ });
377
+ ```
378
+
379
+ Without a `width` option, the size it returns is the size of the box
380
+ `screen.draw.text` places, so `center` (or any other anchor) puts the text
381
+ where that size says it goes. With a `width` option, `screen.draw.text`'s
382
+ box is that wide, and the `width` it returns says how much of it the
383
+ lines fill. An outline or shadow draws a little outside the measured size.
384
+
385
+ Measure where you draw, in `draw()`: a font from `fonts/` measures right
386
+ once it has loaded, and the game holds its frames while it loads.
387
+
388
+ `text` is words in quotes or a number. A wrong call throws a `TypeError`
389
+ that says what to change: an option `screen.draw.text` doesn't have is
390
+ named with the closest real one, and a number after the text, as in
391
+ `screen.measureText("Score", 32)`, says to give the size as an option,
392
+ `{ size: 32 }`.
@@ -0,0 +1,90 @@
1
+ # Storage
2
+
3
+ `storage` is a persistent, dictionary-like object for saving small amounts
4
+ of data between play sessions — high scores, unlocked levels, settings.
5
+
6
+ ```js
7
+ let highScore = storage.get("highScore") ?? 0;
8
+
9
+ function update() {
10
+ if (score > highScore) {
11
+ highScore = score;
12
+ storage.set("highScore", highScore);
13
+ }
14
+ }
15
+ ```
16
+
17
+ ## Auto-load and auto-save
18
+
19
+ You don't need to call `save()` or `load()` yourself in normal use:
20
+
21
+ - **Auto-load**: the default browser adapter has a synchronous `loadSync()`
22
+ path, so a top-level read like `let score = storage.get("score") ?? 0`
23
+ sees previously-saved data immediately. So does a custom adapter with a
24
+ `loadSync()`, or whose `load()` returns its text rather than a Promise. A
25
+ custom asynchronous adapter is loaded once during startup and delays
26
+ frames/`handle.ready`, but cannot retroactively change values already
27
+ copied into top-level variables, and what it loads replaces anything the
28
+ top level stored before it arrived.
29
+ - **Auto-save**: any mutation (`storage.set(...)`, `storage.score = 5`,
30
+ `storage.delete(...)`, `storage.clear()`) schedules a save a fraction of a
31
+ second later, and within a second even while the game keeps changing it
32
+ every frame; rapid changes coalesce into one write instead of saving on
33
+ every single mutation. Storage is also flushed immediately when the tab is
34
+ hidden or closed, and when the game stops, so nothing is lost. A change
35
+ inside a stored object or list, such as `storage.highScores.push(score)`,
36
+ is saved too, with the next of those saves; set the key again
37
+ (`storage.highScores = storage.highScores`) to save it sooner.
38
+
39
+ ## Reading and writing
40
+
41
+ Two equivalent styles work — plain method calls, or attribute-style access
42
+ through a proxy (closer to pgzero's `storage` object):
43
+
44
+ ```js
45
+ storage.set("score", 10);
46
+ storage.get("score"); // 10
47
+
48
+ storage.score = 10; // same as storage.set("score", 10)
49
+ storage.score; // 10
50
+ ```
51
+
52
+ | Method | Behavior |
53
+ | --------------------------------------- | -------------------------------------------------------------------------------------------------- |
54
+ | `storage.get(key, defaultValue?)` | Returns the stored value, or `defaultValue` (`undefined` if omitted) if `key` isn't set. |
55
+ | `storage.set(key, value)` | Stores `value` under `key`. |
56
+ | `storage.setDefault(key, defaultValue)` | Sets `key` to `defaultValue` only if it isn't already set; returns the resulting value either way. |
57
+ | `storage.has(key)` | `boolean`. |
58
+ | `storage.delete(key)` | Removes `key`; also works as `delete storage.key`. Returns `true` if something was removed. |
59
+ | `storage.clear()` | Removes everything. |
60
+ | `storage.keys()` | Every stored key, alphabetically sorted. |
61
+ | `storage.path` | The backing location: a scoped `localStorage` key in the browser, or a file path in the sandbox. |
62
+
63
+ Values must be JSON-serializable — plain objects, arrays, strings, numbers,
64
+ booleans, and `null`. Storing anything else (a class instance, a function,
65
+ `undefined`, `NaN`, `Infinity`, a circular reference), anywhere inside the
66
+ value, throws a `JsonStorageError` naming the offending path, e.g.
67
+ `NaN is not a JSON value at storage.highScores[2].score`, and stores nothing.
68
+ A value changed inside a stored object or list after it was stored is only
69
+ checked when storage saves: until it's a JSON value again, saving fails,
70
+ with a warning naming its path, and nothing in storage is saved.
71
+
72
+ ## Gotchas
73
+
74
+ - **Don't shadow method names.** `storage.save`, `storage.load`, `storage.path`
75
+ and the other method names above are real properties on `storage` itself,
76
+ not just proxy-forwarded keys — writing `storage.save = 5` overwrites the
77
+ actual `save` method instead of storing a value, and another spelling of
78
+ a method, such as `storage.setdefault`, throws naming the method. Pick key
79
+ names that don't collide with the method list.
80
+ - Storage is scoped per project: in the browser it's namespaced by project
81
+ id (so two different games on the same site don't share data, as long as
82
+ each has its own `projectId`; games run without one all share the id
83
+ `default`); in the sandbox it's a JSON file inside that project's own
84
+ directory.
85
+
86
+ ## Manual save/load
87
+
88
+ For advanced cases — forcing an immediate write instead of waiting for the
89
+ debounce, or working outside of `run()` — `storage.save()` and
90
+ `storage.load()` are both available directly and return `Promise<void>`.