@hopple/sdk 1.0.0-next.1 → 1.0.0-next.12

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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! @hopple/sdk 1.0.0-next.1 | Copyright (c) 2026 The Hopple authors | MIT License */
1
+ /*! @hopple/sdk | Copyright (c) 2026 The Hopple authors | MIT License */
2
2
  // The SDK runs from the runtime the page loads as its first script, which
3
3
  // defines the global `Hopple`; this forwards each export to it.
4
4
  const runtime = globalThis.Hopple;
package/manifest.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/manifest/src/manifest.d.ts
1
+ //#region ../../../../../../../tmp/hopple-sdk-types-hbSZtA/manifest/src/manifest.d.ts
2
2
  /**
3
3
  * The API a game is written against: `M.m`, a major and the least minor it
4
4
  * needs, or `next`, the pre-releases of the next stable release.
@@ -217,6 +217,26 @@ type ScoreBoard = {
217
217
  direction: ScoreDirection;
218
218
  /** See `ScoreFormat`. Absent means `integer`. */
219
219
  format?: ScoreFormat;
220
+ /**
221
+ * Which board, of the ones this id has named. Absent is `1`.
222
+ *
223
+ * **Raising it starts the board again from nobody.** The runs under the
224
+ * version before are kept and no longer counted, shown or ranked, so a board
225
+ * whose scores stopped being comparable — new levels, a new rule — begins
226
+ * again under the id the game already submits to.
227
+ *
228
+ * **Only a change to what a stored score means needs it.** The title changes
229
+ * freely, and so does the format between `integer` and `decimal`. Once the
230
+ * board holds scores, turning it around (`direction`) or writing it in
231
+ * another unit (to or from `duration-ms`) is refused at the same version.
232
+ * Going back to a version declared before brings its runs back, so it is
233
+ * held to what that version was.
234
+ *
235
+ * @asType integer
236
+ * @minimum 1
237
+ * @maximum 1000
238
+ */
239
+ version?: number;
220
240
  };
221
241
  /**
222
242
  * The boards this game keeps a score on.
@@ -380,6 +400,28 @@ type Achievement = {
380
400
  secret?: boolean;
381
401
  /** Present makes this measured rather than flagged. See `AchievementProgress`. */
382
402
  progress?: AchievementProgress;
403
+ /**
404
+ * Which achievement, of the ones this id has named. Absent is `1`.
405
+ *
406
+ * **Raising it starts the achievement again for everybody.** What players
407
+ * reported and unlocked under the version before is kept and no longer
408
+ * counted, so it is earned afresh under the id the game already reports to.
409
+ *
410
+ * **Only a change to what a stored value means needs it.** The words change
411
+ * freely — title, detail, icon, secret — and so does the target, and a tier
412
+ * added after the last one: progress is kept as the number reported, which
413
+ * stays true when the goal moves, and a player who unlocked it keeps it. At
414
+ * the same version, a published achievement may not turn from a flag into a
415
+ * measure or back, turn its direction around, write its value in another
416
+ * unit (to or from `duration-ms`), or move or drop a tier it already has.
417
+ * Going back to a version declared before brings back what was earned under
418
+ * it, so it is held to what that version was.
419
+ *
420
+ * @asType integer
421
+ * @minimum 1
422
+ * @maximum 1000
423
+ */
424
+ version?: number;
383
425
  };
384
426
  /**
385
427
  * The achievements this game keeps for a player.
@@ -445,7 +487,7 @@ type InputDeviceSet = InputDevice[];
445
487
  * doubled. A thumbstick is not one of these; see `InputStick`.
446
488
  *
447
489
  * @group Input
448
- * @pattern ^(?:key\.(?!Escape$)(?=.{1,48}$)[A-Za-z][A-Za-z0-9]*|pointer\.(?:primary|secondary)|mouse\.(?:left|middle|right)|touch\.tap|controls\.(?=.{1,48}$)[a-z0-9]+(?:[_-][a-z0-9]+)*|pad\.(?:north|south|east|west|start|select)|pad\.(?:left|right)\.(?:north|south|east|west|bumper|trigger))$
490
+ * @pattern ^(?:key\.(?!Escape$)(?=.{1,48}$)[A-Za-z][A-Za-z0-9]*|pointer\.(?:primary|secondary)|mouse\.(?:left|middle|right|wheelup|wheeldown)|touch\.tap|controls\.(?=.{1,48}$)[a-z0-9]+(?:[_-][a-z0-9]+)*|pad\.(?:north|south|east|west|start|select)|pad\.(?:left|right)\.(?:north|south|east|west|bumper|trigger))$
449
491
  * @errorMessage is not a binding this SDK knows (a thumbstick is read rather than bound, and Escape is the host's key)
450
492
  */
451
493
  type InputBinding = string;
@@ -521,7 +563,7 @@ type PlayerBinding = string;
521
563
  * mouse button.
522
564
  *
523
565
  * @group Input
524
- * @pattern ^(?:key\.(?!Escape$)(?=.{1,48}$)[A-Za-z][A-Za-z0-9]*|mouse\.(?:left|middle|right))$
566
+ * @pattern ^(?:key\.(?!Escape$)(?=.{1,48}$)[A-Za-z][A-Za-z0-9]*|mouse\.(?:left|middle|right|wheelup|wheeldown))$
525
567
  * @errorMessage must be a key or a mouse button (and Escape is the host's key)
526
568
  */
527
569
  type KeyboardBinding = string;
@@ -671,6 +713,20 @@ type ControlSlot = {
671
713
  as?: InputStick | Record<string, InputBinding>;
672
714
  /** `thumbstick` only. See `ControlAxis`. Absent, the stick moves both ways. */
673
715
  axis?: ControlAxis;
716
+ /**
717
+ * How big this control is next to the layout's others: `1.25` for a jump
718
+ * button a thumb lands on without looking, `0.8` for one pressed now and then.
719
+ * Absent, 1.
720
+ *
721
+ * A size in the layout rather than in the game's stylesheet, because whether
722
+ * the layout fits on a small screen is worked out from it before the
723
+ * controls are drawn. A game's stylesheet changes how they look, never how
724
+ * much of the screen they answer in.
725
+ *
726
+ * @minimum 0.75
727
+ * @maximum 1.5
728
+ */
729
+ scale?: number;
674
730
  };
675
731
  /**
676
732
  * The one direction an on-screen thumbstick moves in: `x` across, `y` up and
@@ -821,6 +877,23 @@ type PreviewsDeclaration = {
821
877
  */
822
878
  network?: PreviewRevision;
823
879
  };
880
+ /**
881
+ * What kind of game this is, as one word from a list every game shares.
882
+ *
883
+ * A closed list, because a client files games under these and has to name each
884
+ * one in front of a player: left to free text, one kind of game becomes three
885
+ * spellings of it with a game each. The list only grows, since a published
886
+ * manifest has to stay valid. Whoever reads one may be older than the game, and
887
+ * ignores a word it does not know.
888
+ *
889
+ * `local-multiplayer` and `online-multiplayer` say how a game is played rather
890
+ * than what it is, and only count where the manifest bears them out: see
891
+ * `declaredGenres`.
892
+ *
893
+ * @group Manifest
894
+ * @category Useful
895
+ */
896
+ type GameGenre = 'racing' | 'shooter' | 'platformer' | 'puzzle' | 'arcade' | 'physics' | 'roguelike' | 'strategy' | 'building' | 'idle' | 'sports' | 'horror' | 'stealth' | 'fighting' | 'casual' | 'simulation' | 'adventure' | 'rhythm' | 'rpg' | 'local-multiplayer' | 'online-multiplayer';
824
897
  /**
825
898
  * Publishing refuses a key the manifest's API does not know; a host reading a
826
899
  * published one ignores it (see `validateManifest`).
@@ -860,8 +933,19 @@ type GameManifest = {
860
933
  * @errorMessage must be a handle: lowercase letters, digits and hyphens
861
934
  */
862
935
  owner?: string;
936
+ /**
937
+ * `false` for a game that is never to be uploaded: an example, a test, a
938
+ * game kept to one machine. `hopple dev` and `hopple check` still run it;
939
+ * `hopple upload` and `hopple publish` refuse it, and so does a server it
940
+ * is sent to another way. Absent, it may be published.
941
+ *
942
+ * @category Useful
943
+ */
944
+ publishable?: boolean;
863
945
  /**
864
946
  * The game's own version, `MAJOR.MINOR.PATCH`. Ours to read, never to parse for meaning.
947
+ * People see it beside the build the server numbers each upload with, and
948
+ * each published version needs one no other published version of the game has.
865
949
  *
866
950
  * @category Important
867
951
  * @pattern ^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$
@@ -884,6 +968,18 @@ type GameManifest = {
884
968
  * @errorMessage must not be blank
885
969
  */
886
970
  description?: string;
971
+ /**
972
+ * What kind of game it is, the main one first. Read by whoever files it in a
973
+ * catalogue, which never runs it. At most `GENRE_MAX`, not counting
974
+ * `local-multiplayer` and `online-multiplayer`: a game that is every kind
975
+ * tells a player nothing. The publisher's claim, taken at its word, except
976
+ * for those two. See `GameGenre`.
977
+ *
978
+ * @category Useful
979
+ * @minItems 1
980
+ * @maxItems 5
981
+ */
982
+ genres?: GameGenre[];
887
983
  /**
888
984
  * A picture of the game, shown wherever a link to it is pasted — a chat, a
889
985
  * post, a message.
package/manifest.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! @hopple/sdk 1.0.0-next.1 | Copyright (c) 2026 The Hopple authors | MIT License */
1
+ /*! @hopple/sdk 1.0.0-next.12 | Copyright (c) 2026 The Hopple authors | MIT License */
2
2
  //#region ../manifest/src/manifest.schema.ts
3
3
  const GameManifest = {
4
4
  "type": "object",
@@ -16,6 +16,7 @@ const GameManifest = {
16
16
  "maxLength": 64,
17
17
  "errorMessage": "must be a handle: lowercase letters, digits and hyphens"
18
18
  },
19
+ "publishable": { "type": "boolean" },
19
20
  "version": {
20
21
  "type": "string",
21
22
  "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?$",
@@ -33,6 +34,12 @@ const GameManifest = {
33
34
  "maxLength": 500,
34
35
  "errorMessage": "must not be blank"
35
36
  },
37
+ "genres": {
38
+ "type": "array",
39
+ "items": { "$ref": "#/definitions/GameGenre" },
40
+ "minItems": 1,
41
+ "maxItems": 5
42
+ },
36
43
  "cover": {
37
44
  "type": "string",
38
45
  "pattern": "^(?!/)(?!.*\\.\\.)[A-Za-z0-9._/-]+\\.(?:png|jpe?g|webp)$",
@@ -65,6 +72,32 @@ const ApiVersion = {
65
72
  "pattern": "^(?:(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)|next)$",
66
73
  "errorMessage": "must be the API the game is written against, like \"1.0\", or \"next\""
67
74
  };
75
+ const GameGenre = {
76
+ "type": "string",
77
+ "enum": [
78
+ "racing",
79
+ "shooter",
80
+ "platformer",
81
+ "puzzle",
82
+ "arcade",
83
+ "physics",
84
+ "roguelike",
85
+ "strategy",
86
+ "building",
87
+ "idle",
88
+ "sports",
89
+ "horror",
90
+ "stealth",
91
+ "fighting",
92
+ "casual",
93
+ "simulation",
94
+ "adventure",
95
+ "rhythm",
96
+ "rpg",
97
+ "local-multiplayer",
98
+ "online-multiplayer"
99
+ ]
100
+ };
68
101
  const Orientation = {
69
102
  "type": "string",
70
103
  "enum": [
@@ -153,7 +186,12 @@ const ScoreBoard = {
153
186
  "errorMessage": "must not be blank"
154
187
  },
155
188
  "direction": { "$ref": "#/definitions/ScoreDirection" },
156
- "format": { "$ref": "#/definitions/ScoreFormat" }
189
+ "format": { "$ref": "#/definitions/ScoreFormat" },
190
+ "version": {
191
+ "type": "integer",
192
+ "minimum": 1,
193
+ "maximum": 1e3
194
+ }
157
195
  },
158
196
  "required": ["title", "direction"],
159
197
  "additionalProperties": false
@@ -208,7 +246,12 @@ const Achievement = {
208
246
  "errorMessage": "must not be blank"
209
247
  },
210
248
  "secret": { "type": "boolean" },
211
- "progress": { "$ref": "#/definitions/AchievementProgress" }
249
+ "progress": { "$ref": "#/definitions/AchievementProgress" },
250
+ "version": {
251
+ "type": "integer",
252
+ "minimum": 1,
253
+ "maximum": 1e3
254
+ }
212
255
  },
213
256
  "required": ["title"],
214
257
  "additionalProperties": false
@@ -332,7 +375,7 @@ const InputBindings = {
332
375
  };
333
376
  const InputBinding = {
334
377
  "type": "string",
335
- "pattern": "^(?:key\\.(?!Escape$)(?=.{1,48}$)[A-Za-z][A-Za-z0-9]*|pointer\\.(?:primary|secondary)|mouse\\.(?:left|middle|right)|touch\\.tap|controls\\.(?=.{1,48}$)[a-z0-9]+(?:[_-][a-z0-9]+)*|pad\\.(?:north|south|east|west|start|select)|pad\\.(?:left|right)\\.(?:north|south|east|west|bumper|trigger))$",
378
+ "pattern": "^(?:key\\.(?!Escape$)(?=.{1,48}$)[A-Za-z][A-Za-z0-9]*|pointer\\.(?:primary|secondary)|mouse\\.(?:left|middle|right|wheelup|wheeldown)|touch\\.tap|controls\\.(?=.{1,48}$)[a-z0-9]+(?:[_-][a-z0-9]+)*|pad\\.(?:north|south|east|west|start|select)|pad\\.(?:left|right)\\.(?:north|south|east|west|bumper|trigger))$",
336
379
  "errorMessage": "is not a binding this SDK knows (a thumbstick is read rather than bound, and Escape is the host's key)"
337
380
  };
338
381
  const ControlLayout = {
@@ -372,7 +415,12 @@ const ControlSlot = {
372
415
  "type": "object",
373
416
  "additionalProperties": { "$ref": "#/definitions/InputBinding" }
374
417
  }] },
375
- "axis": { "$ref": "#/definitions/ControlAxis" }
418
+ "axis": { "$ref": "#/definitions/ControlAxis" },
419
+ "scale": {
420
+ "type": "number",
421
+ "minimum": .75,
422
+ "maximum": 1.5
423
+ }
376
424
  },
377
425
  "required": ["type"],
378
426
  "additionalProperties": false,
@@ -412,6 +460,7 @@ const InputStick = {
412
460
  const definitions = {
413
461
  GameManifest,
414
462
  ApiVersion,
463
+ GameGenre,
415
464
  Orientation,
416
465
  LayoutDeclaration,
417
466
  InsetMode,
@@ -523,7 +572,7 @@ const definitions = {
523
572
  },
524
573
  KeyboardBinding: {
525
574
  "type": "string",
526
- "pattern": "^(?:key\\.(?!Escape$)(?=.{1,48}$)[A-Za-z][A-Za-z0-9]*|mouse\\.(?:left|middle|right))$",
575
+ "pattern": "^(?:key\\.(?!Escape$)(?=.{1,48}$)[A-Za-z][A-Za-z0-9]*|mouse\\.(?:left|middle|right|wheelup|wheeldown))$",
527
576
  "errorMessage": "must be a key or a mouse button (and Escape is the host's key)"
528
577
  },
529
578
  PlayersDeclaration: {
@@ -573,6 +622,7 @@ const definitions = {
573
622
  };
574
623
  AchievementProgress.properties.target.maximum;
575
624
  GameManifest.required;
625
+ GameGenre.enum;
576
626
  /**
577
627
  * The zones a layout places controls in, in the order they are drawn.
578
628
  *
@@ -750,6 +800,9 @@ function walkObject(node, value, path, fail, lenient) {
750
800
  }
751
801
  }
752
802
  function checkReferences(manifest, fail) {
803
+ const genres = manifest.genres ?? [];
804
+ for (const genre of new Set(genres.filter((one, index) => genres.indexOf(one) !== index))) fail("genres", `names "${genre}" twice`);
805
+ if (genres.filter((genre) => !MULTIPLAYER_GENRES.includes(genre)).length > 3) fail("genres", `must hold at most 3 genres, beside local-multiplayer and online-multiplayer`);
753
806
  const players = manifest.input?.player;
754
807
  const own = new Set(Object.keys(players?.actions ?? {}));
755
808
  const actions = /* @__PURE__ */ new Set([...Object.keys(manifest.input?.actions ?? {}), ...own]);
@@ -787,6 +840,8 @@ function checkReferences(manifest, fail) {
787
840
  }
788
841
  }
789
842
  }
843
+ /** The genres that say how a game is played together rather than what it is. */
844
+ const MULTIPLAYER_GENRES = ["local-multiplayer", "online-multiplayer"];
790
845
  /**
791
846
  * Whether `a` beats `b` on a board sorted this way. Ties are not improvements.
792
847
  *
package/manifest.md CHANGED
@@ -35,6 +35,7 @@ leave `api` and `previews` out: the CLI fills `api` from the installed
35
35
  | `version` | `MAJOR.MINOR.PATCH`, optional `-prerelease` | The game's own version. Shown to the game as `sdk.info.game.version`. |
36
36
  | `title` | at most 60 characters | What players see. |
37
37
  | `description` | optional; at most 500 characters | A sentence or two shown beside the title. |
38
+ | `genres` | optional; see [`genres`](#genres) | What kind of game it is, the main one first. |
38
39
  | `orientation` | `portrait`, `landscape` or `any` | The shape the game is designed for. Hosts lay the game out to match. |
39
40
  | `audio` | `null`, or an object | Whether the game makes sound, and its volume channels. See [`audio`](#audio). |
40
41
 
@@ -54,15 +55,17 @@ same function, so they agree on whether a manifest is valid.
54
55
 
55
56
  ### In an editor
56
57
 
57
- `@hopple/sdk` ships the hopple config's JSON Schema, the manifest's included,
58
- as `config.schema.json`, for completion, hover docs and errors as you type. A
59
- JSON config can point at it:
58
+ The `hopple` CLI ships the hopple config's JSON Schema, the manifest's
59
+ included, as `config.schema.json`, for completion, hover docs and errors as
60
+ you type. A JSON config in a project with `hopple` installed can point at it:
60
61
 
61
62
  ```json
62
- { "$schema": "./node_modules/@hopple/sdk/config.schema.json", "page": "dist/index.html", "manifest": { ... } }
63
+ { "$schema": "./node_modules/hopple/config.schema.json", "page": "dist/index.html", "manifest": { ... } }
63
64
  ```
64
65
 
65
- A TypeScript config gets the same checks from its type, `HoppleConfig`.
66
+ A TypeScript config gets the same checks from its type,
67
+ `import type { HoppleConfig } from 'hopple'`, whose manifest is the type of
68
+ the installed `@hopple/sdk`.
66
69
 
67
70
  A few rules relate one part of a manifest to another, which JSON Schema can't
68
71
  express: buttons must name declared actions, a control's `as` must name its
@@ -141,6 +144,7 @@ A game without this key can still use `sdk.save`.
141
144
  | `title` | yes | What players see. At most 40 characters. |
142
145
  | `direction` | yes | `desc`: higher is better. `asc`: lower is better (times, strokes). |
143
146
  | `format` | no | `integer` (default), `decimal`, or `duration-ms` (so `224310` shows as `3:44.31`). |
147
+ | `version` | no | 1 by default. Raising it starts the board again; see [Changing the manifest in a new version](#changing-the-manifest-in-a-new-version). |
144
148
 
145
149
  `boards` is a map even for one board, so a game can add a second one later
146
150
  without changing the meaning of the first. At most 16 boards.
@@ -179,6 +183,9 @@ There are two kinds:
179
183
  `secret: true` hides the title and detail from players until it is earned.
180
184
  The manifest is public, so this only stops spoilers.
181
185
 
186
+ `version` is 1 by default. Raising it starts the achievement again for
187
+ everybody; see [Changing the manifest in a new version](#changing-the-manifest-in-a-new-version).
188
+
182
189
  Limits: 128 achievements, titles up to 40 characters, details up to 120, icons
183
190
  up to 8 characters, 8 tiers.
184
191
 
@@ -219,7 +226,8 @@ up to 8 characters, 8 tiers.
219
226
  `buttons` (naming actions) arranged as a `row`, `column`, `diamond` or
220
227
  `cluster`. An unknown shape is drawn as a column. A stick held to an axis is
221
228
  drawn as a track and always reads 0 on the other axis; a real pad's stick
222
- still reports both.
229
+ still reports both. Any control may have a `scale` from 0.75 to 1.5, its
230
+ size next to the layout's others (1 when left out).
223
231
 
224
232
  Binding names:
225
233
 
@@ -228,6 +236,7 @@ Binding names:
228
236
  | `key.KeyZ` | A physical key, by `KeyboardEvent.code`. `KeyA` is the same physical key on every keyboard layout. |
229
237
  | `pointer.primary` | A left click or a tap. (`pointer.secondary`: a right click.) |
230
238
  | `mouse.left` | A left click only, not a tap. Also `mouse.middle`, `mouse.right`. |
239
+ | `mouse.wheelup`, `mouse.wheeldown` | A step of the mouse wheel: a press and a release for each notch, and for every stretch of a trackpad swipe. |
231
240
  | `touch.tap` | A tap only, not a click. |
232
241
  | `pad.south` | The bottom face button of either cluster, on any gamepad or on-screen pad. |
233
242
  | `pad.right.south` | The bottom right face button (A on Xbox, cross on PlayStation). |
@@ -295,6 +304,31 @@ Each channel gets its own labelled volume slider in the host's sound controls.
295
304
  `title` is required: it is the slider's label. `master` is reserved, since
296
305
  every host already has a master volume. At most 8 channels.
297
306
 
307
+ ### genres
308
+
309
+ ```json
310
+ "genres": ["puzzle", "shooter", "online-multiplayer"]
311
+ ```
312
+
313
+ What kind of game this is, the main one first: at least one, and at most 3
314
+ beside the two multiplayer words, none twice. A game with none declared is
315
+ filed by what its title and description say, and most often less well.
316
+
317
+ The words are `racing`, `shooter`, `platformer`, `puzzle`, `arcade`, `physics`,
318
+ `roguelike`, `strategy`, `building`, `idle`, `sports`, `horror`, `stealth`,
319
+ `fighting`, `casual`, `simulation`, `adventure`, `rhythm` and `rpg`, and two
320
+ that say how it is played together:
321
+
322
+ - **`local-multiplayer`**: several people on one device. Needs `players.local`
323
+ above 1.
324
+ - **`online-multiplayer`**: over the network. Needs `previews.network`.
325
+
326
+ Either of those without what it needs is not refused: the game is published,
327
+ that word is ignored, and `hopple check`, `hopple upload` and `hopple publish`
328
+ warn about it. They warn the other way round too, for a game that declares
329
+ `genres` and leaves out a way of playing together its manifest shows, since it
330
+ then is not listed as one. Clients group the two under one shelf.
331
+
298
332
  ### previews
299
333
 
300
334
  ```json
@@ -327,17 +361,27 @@ approval and links, and an upload declaring it is refused.
327
361
 
328
362
  ## Changing the manifest in a new version
329
363
 
330
- The server refuses a new version whose manifest would corrupt data players
331
- already have:
332
-
333
- - **A score board that has scores can't be removed or change direction.**
334
- Removing it would orphan the scores (and renaming a board looks like removing
335
- it); flipping `direction` would turn the whole board upside down. An admin
336
- can clear the board first if that is really what you want.
337
- - **A published achievement can't be changed.** Its title, detail, icon, secret
338
- flag, kind, target, direction, format and tiers are fixed, because players already
339
- hold it. You can add new achievements, and you can stop declaring old ones
340
- (what players earned is kept).
364
+ Every board and every achievement has a `version`, 1 when left out. The
365
+ server refuses a new version of the game whose manifest would change what data
366
+ players already have means, and the way out is always to raise that board's or
367
+ achievement's `version`: it starts again from nobody under the same id, and
368
+ what was stored under the version before is kept but no longer counted.
369
+
370
+ - **Boards and achievements can be added and dropped freely.** What players
371
+ stored under one that is dropped is kept, and comes back if it is declared
372
+ again.
373
+ - **A board's title can change, and so can its format between `integer` and
374
+ `decimal`.** Once it has scores, flipping its `direction` or moving its
375
+ format to or from `duration-ms` needs a new `version`.
376
+ - **An achievement's title, detail, icon and secret flag can change, and so can
377
+ its target; tiers can be added after the last one.** Progress is kept as the
378
+ number reported, so a player at 12 of 12 is at 12 of 25 when the target is
379
+ raised, and keeps the unlock; when it is lowered past them, they have it.
380
+ Turning a flag into a measured achievement or back, flipping `direction`,
381
+ moving its format to or from `duration-ms`, or moving or dropping a tier it
382
+ already has needs a new `version`, whether or not anybody holds it yet.
383
+ - **A version declared again is held to what it was**, since what was stored
384
+ under it comes back.
341
385
 
342
386
  Input action ids and audio channel ids may change freely. A player's remapped
343
387
  keys or volume settings for an id that no longer exists are simply dropped.
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@hopple/sdk",
3
- "version": "1.0.0-next.1",
3
+ "version": "1.0.0-next.12",
4
4
  "description": "The Hopple SDK: what a game imports to talk to the host that runs it, forwarded to the runtime its page loads.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://hopple.app",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/pamarit/hopple.git"
10
+ },
7
11
  "keywords": [
8
12
  "hopple",
9
13
  "game",
@@ -25,7 +29,6 @@
25
29
  "types": "./manifest.d.ts",
26
30
  "default": "./manifest.js"
27
31
  },
28
- "./config.schema.json": "./config.schema.json",
29
32
  "./package.json": "./package.json"
30
33
  },
31
34
  "main": "./index.js",