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

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-SHySdg/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,53 @@ 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';
897
+ /**
898
+ * A few seconds of real play, which Explore shows in place of the still cover,
899
+ * played muted, over and over. See `GameManifest.clip`.
900
+ *
901
+ * @group Manifest
902
+ * @category Useful
903
+ */
904
+ type ClipDeclaration = {
905
+ /**
906
+ * The clip: the path of an MP4 file beside the config. H.264 with no sound
907
+ * track, 2 to 15 seconds long, its longest side at most 1280 and its
908
+ * shortest at most 720, saved with "fast start", and at most
909
+ * `CLIP_MAX_BYTES`. `clipProblems` says what is wrong with one.
910
+ *
911
+ * @pattern ^(?!/)(?!.*\.\.)[A-Za-z0-9._/-]+\.mp4$
912
+ * @maxLength 200
913
+ * @errorMessage must be the path of an .mp4 file beside the config, like "clip.mp4"
914
+ */
915
+ video: string;
916
+ /**
917
+ * The clip's first frame, as a PNG, JPEG or WebP file beside the config,
918
+ * shown until the clip plays and wherever it cannot. At most
919
+ * `POSTER_MAX_BYTES`.
920
+ *
921
+ * @pattern ^(?!/)(?!.*\.\.)[A-Za-z0-9._/-]+\.(?:png|jpe?g|webp)$
922
+ * @maxLength 200
923
+ * @errorMessage must be the path of a .png, .jpg or .webp file beside the config, like "clip.png"
924
+ */
925
+ poster: string;
926
+ };
824
927
  /**
825
928
  * Publishing refuses a key the manifest's API does not know; a host reading a
826
929
  * published one ignores it (see `validateManifest`).
@@ -860,8 +963,19 @@ type GameManifest = {
860
963
  * @errorMessage must be a handle: lowercase letters, digits and hyphens
861
964
  */
862
965
  owner?: string;
966
+ /**
967
+ * `false` for a game that is never to be uploaded: an example, a test, a
968
+ * game kept to one machine. `hopple dev` and `hopple check` still run it;
969
+ * `hopple upload` and `hopple publish` refuse it, and so does a server it
970
+ * is sent to another way. Absent, it may be published.
971
+ *
972
+ * @category Useful
973
+ */
974
+ publishable?: boolean;
863
975
  /**
864
976
  * The game's own version, `MAJOR.MINOR.PATCH`. Ours to read, never to parse for meaning.
977
+ * People see it beside the build the server numbers each upload with, and
978
+ * each published version needs one no other published version of the game has.
865
979
  *
866
980
  * @category Important
867
981
  * @pattern ^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$
@@ -885,20 +999,46 @@ type GameManifest = {
885
999
  */
886
1000
  description?: string;
887
1001
  /**
888
- * A picture of the game, shown wherever a link to it is pasted — a chat, a
889
- * post, a message.
1002
+ * What kind of game it is, the main one first. Read by whoever files it in a
1003
+ * catalogue, which never runs it. At most `GENRE_MAX`, not counting
1004
+ * `local-multiplayer` and `online-multiplayer`: a game that is every kind
1005
+ * tells a player nothing. The publisher's claim, taken at its word, except
1006
+ * for those two. See `GameGenre`.
1007
+ *
1008
+ * @category Useful
1009
+ * @minItems 1
1010
+ * @maxItems 5
1011
+ */
1012
+ genres?: GameGenre[];
1013
+ /**
1014
+ * A picture of the game: its cover on Explore's shelves and in a player's
1015
+ * inbox, and what a link to it shows wherever it is pasted — a chat, a post,
1016
+ * a message.
890
1017
  *
891
1018
  * The path of a PNG, JPEG or WebP file, relative to the config the manifest
892
1019
  * is in, and at most `COVER_MAX_BYTES`. `hopple publish` uploads it beside
893
- * the game. 1200 × 630 is the shape every platform shows whole; without one,
894
- * a link shows Hopple's own card.
1020
+ * the game, and a server takes no upload without one: only a game that is
1021
+ * never uploaded leaves it out. 1200 × 630 is the shape every platform shows
1022
+ * whole; `hopple smoke --cover` makes one from the game itself.
895
1023
  *
896
- * @category Useful
1024
+ * @category Important
897
1025
  * @pattern ^(?!/)(?!.*\.\.)[A-Za-z0-9._/-]+\.(?:png|jpe?g|webp)$
898
1026
  * @maxLength 200
899
1027
  * @errorMessage must be the path of a .png, .jpg or .webp file beside the config, like "cover.png"
900
1028
  */
901
1029
  cover?: string;
1030
+ /**
1031
+ * A few seconds of real play, which Explore shows in place of the still
1032
+ * `cover`, played muted. `poster` is the clip's first frame, shown until it
1033
+ * plays. The clip is an H.264 MP4 with no sound track, 2 to 15 seconds long,
1034
+ * no larger than 1280 × 720 either way up, saved with "fast start", and at
1035
+ * most `CLIP_MAX_BYTES`; the poster at most `POSTER_MAX_BYTES`. Both are
1036
+ * paths beside the config, and `hopple publish` uploads them with the game.
1037
+ * See `ClipDeclaration`.
1038
+ *
1039
+ * @category Useful
1040
+ */
1041
+ clip?: ClipDeclaration;
902
1042
  /** @category Important */
903
1043
  orientation: Orientation;
904
1044
  /**
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.13 | 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,12 +34,19 @@ 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)$",
39
46
  "maxLength": 200,
40
47
  "errorMessage": "must be the path of a .png, .jpg or .webp file beside the config, like \"cover.png\""
41
48
  },
49
+ "clip": { "$ref": "#/definitions/ClipDeclaration" },
42
50
  "orientation": { "$ref": "#/definitions/Orientation" },
43
51
  "layout": { "$ref": "#/definitions/LayoutDeclaration" },
44
52
  "analytics": { "$ref": "#/definitions/AnalyticsDeclaration" },
@@ -65,6 +73,51 @@ const ApiVersion = {
65
73
  "pattern": "^(?:(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)|next)$",
66
74
  "errorMessage": "must be the API the game is written against, like \"1.0\", or \"next\""
67
75
  };
76
+ const GameGenre = {
77
+ "type": "string",
78
+ "enum": [
79
+ "racing",
80
+ "shooter",
81
+ "platformer",
82
+ "puzzle",
83
+ "arcade",
84
+ "physics",
85
+ "roguelike",
86
+ "strategy",
87
+ "building",
88
+ "idle",
89
+ "sports",
90
+ "horror",
91
+ "stealth",
92
+ "fighting",
93
+ "casual",
94
+ "simulation",
95
+ "adventure",
96
+ "rhythm",
97
+ "rpg",
98
+ "local-multiplayer",
99
+ "online-multiplayer"
100
+ ]
101
+ };
102
+ const ClipDeclaration = {
103
+ "type": "object",
104
+ "properties": {
105
+ "video": {
106
+ "type": "string",
107
+ "pattern": "^(?!/)(?!.*\\.\\.)[A-Za-z0-9._/-]+\\.mp4$",
108
+ "maxLength": 200,
109
+ "errorMessage": "must be the path of an .mp4 file beside the config, like \"clip.mp4\""
110
+ },
111
+ "poster": {
112
+ "type": "string",
113
+ "pattern": "^(?!/)(?!.*\\.\\.)[A-Za-z0-9._/-]+\\.(?:png|jpe?g|webp)$",
114
+ "maxLength": 200,
115
+ "errorMessage": "must be the path of a .png, .jpg or .webp file beside the config, like \"clip.png\""
116
+ }
117
+ },
118
+ "required": ["video", "poster"],
119
+ "additionalProperties": false
120
+ };
68
121
  const Orientation = {
69
122
  "type": "string",
70
123
  "enum": [
@@ -153,7 +206,12 @@ const ScoreBoard = {
153
206
  "errorMessage": "must not be blank"
154
207
  },
155
208
  "direction": { "$ref": "#/definitions/ScoreDirection" },
156
- "format": { "$ref": "#/definitions/ScoreFormat" }
209
+ "format": { "$ref": "#/definitions/ScoreFormat" },
210
+ "version": {
211
+ "type": "integer",
212
+ "minimum": 1,
213
+ "maximum": 1e3
214
+ }
157
215
  },
158
216
  "required": ["title", "direction"],
159
217
  "additionalProperties": false
@@ -208,7 +266,12 @@ const Achievement = {
208
266
  "errorMessage": "must not be blank"
209
267
  },
210
268
  "secret": { "type": "boolean" },
211
- "progress": { "$ref": "#/definitions/AchievementProgress" }
269
+ "progress": { "$ref": "#/definitions/AchievementProgress" },
270
+ "version": {
271
+ "type": "integer",
272
+ "minimum": 1,
273
+ "maximum": 1e3
274
+ }
212
275
  },
213
276
  "required": ["title"],
214
277
  "additionalProperties": false
@@ -332,7 +395,7 @@ const InputBindings = {
332
395
  };
333
396
  const InputBinding = {
334
397
  "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))$",
398
+ "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
399
  "errorMessage": "is not a binding this SDK knows (a thumbstick is read rather than bound, and Escape is the host's key)"
337
400
  };
338
401
  const ControlLayout = {
@@ -372,7 +435,12 @@ const ControlSlot = {
372
435
  "type": "object",
373
436
  "additionalProperties": { "$ref": "#/definitions/InputBinding" }
374
437
  }] },
375
- "axis": { "$ref": "#/definitions/ControlAxis" }
438
+ "axis": { "$ref": "#/definitions/ControlAxis" },
439
+ "scale": {
440
+ "type": "number",
441
+ "minimum": .75,
442
+ "maximum": 1.5
443
+ }
376
444
  },
377
445
  "required": ["type"],
378
446
  "additionalProperties": false,
@@ -412,6 +480,8 @@ const InputStick = {
412
480
  const definitions = {
413
481
  GameManifest,
414
482
  ApiVersion,
483
+ GameGenre,
484
+ ClipDeclaration,
415
485
  Orientation,
416
486
  LayoutDeclaration,
417
487
  InsetMode,
@@ -523,7 +593,7 @@ const definitions = {
523
593
  },
524
594
  KeyboardBinding: {
525
595
  "type": "string",
526
- "pattern": "^(?:key\\.(?!Escape$)(?=.{1,48}$)[A-Za-z][A-Za-z0-9]*|mouse\\.(?:left|middle|right))$",
596
+ "pattern": "^(?:key\\.(?!Escape$)(?=.{1,48}$)[A-Za-z][A-Za-z0-9]*|mouse\\.(?:left|middle|right|wheelup|wheeldown))$",
527
597
  "errorMessage": "must be a key or a mouse button (and Escape is the host's key)"
528
598
  },
529
599
  PlayersDeclaration: {
@@ -573,6 +643,7 @@ const definitions = {
573
643
  };
574
644
  AchievementProgress.properties.target.maximum;
575
645
  GameManifest.required;
646
+ GameGenre.enum;
576
647
  /**
577
648
  * The zones a layout places controls in, in the order they are drawn.
578
649
  *
@@ -750,6 +821,9 @@ function walkObject(node, value, path, fail, lenient) {
750
821
  }
751
822
  }
752
823
  function checkReferences(manifest, fail) {
824
+ const genres = manifest.genres ?? [];
825
+ for (const genre of new Set(genres.filter((one, index) => genres.indexOf(one) !== index))) fail("genres", `names "${genre}" twice`);
826
+ 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
827
  const players = manifest.input?.player;
754
828
  const own = new Set(Object.keys(players?.actions ?? {}));
755
829
  const actions = /* @__PURE__ */ new Set([...Object.keys(manifest.input?.actions ?? {}), ...own]);
@@ -787,6 +861,8 @@ function checkReferences(manifest, fail) {
787
861
  }
788
862
  }
789
863
  }
864
+ /** The genres that say how a game is played together rather than what it is. */
865
+ const MULTIPLAYER_GENRES = ["local-multiplayer", "online-multiplayer"];
790
866
  /**
791
867
  * Whether `a` beats `b` on a board sorted this way. Ties are not improvements.
792
868
  *
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.13",
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",