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

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/README.md CHANGED
@@ -673,8 +673,44 @@ sdk.input.controls.showing; // whether controls are actually visible
673
673
  - Hiding them only stops drawing them; keys and gamepads keep working.
674
674
  - While shown, they appear in `sdk.layout.obstructions` as reserved
675
675
  rectangles, so code that avoids the host's buttons avoids them too.
676
- - They are drawn in a shadow root inside your frame, so your CSS can't affect
677
- them and they don't appear in your DOM.
676
+ - They are drawn in a shadow root inside your frame, so they don't appear in
677
+ your DOM and your CSS only reaches them where it is meant to: through
678
+ `hopple-controls::part(…)` and the `--hopple-pad-*` properties (see
679
+ [Styling them](#styling-the-on-screen-controls)).
680
+
681
+ ### Styling the on-screen controls
682
+
683
+ ```css
684
+ hopple-controls {
685
+ --hopple-pad-tint: #2b1d0ecc; /* a button's fill */
686
+ --hopple-pad-edge: #f5c542; /* rims, the knob */
687
+ --hopple-pad-ink: #fff8e1; /* labels and icons */
688
+ --hopple-pad-radius: 14px;
689
+ --hopple-pad-font: 700 16px/1 'My Font', sans-serif, hopple-keys;
690
+ }
691
+ hopple-controls::part(face pressed) { translate: 0 2px; }
692
+ hopple-controls::part(face action-jump) { background: url(ui/jump.png) center / 70% no-repeat; }
693
+ ```
694
+
695
+ ```ts
696
+ sdk.input.icon('jump', 'M12 3 4 13h5v8h6v-8h5z'); // a filled path in a 24×24 box, or null
697
+ ```
698
+
699
+ - Parts: `face`, `label` and `icon` on a button; `group` behind a group of
700
+ buttons; `well`, `knob`, `groove` and `arrow` on a stick. Each also carries
701
+ `action-<id>` and `control-<id>`, and the states `pressed`, `held`,
702
+ `floating` and `resting`.
703
+ - `<hopple-controls>` has `layout`, `sticks` and `look` attributes, and a
704
+ stick has `--hopple-stick-x` and `--hopple-stick-y` (−1 to 1) for a game to
705
+ read.
706
+ - Sizes (`--hopple-pad-size`, `-stick`, `-track`, `-gap`, `-inset`) are held
707
+ between a size a thumb can press and one that leaves the game room. How big
708
+ one control is next to the others is its `scale` in the manifest. What a
709
+ part draws never changes where a touch lands.
710
+ - A player can turn a game's look off, for every game or for one, and gets the
711
+ plain controls back: test that they still read well over the game.
712
+ - Under `hopple dev`, a rule that names a part or a property the controls don't
713
+ have is reported to `onError`.
678
714
 
679
715
  ### Several players on one device
680
716
 
@@ -63,6 +63,11 @@
63
63
  "maxLength": 64,
64
64
  "errorMessage": "must be a handle: lowercase letters, digits and hyphens"
65
65
  },
66
+ "publishable": {
67
+ "type": "boolean",
68
+ "description": "`false` for a game that is never to be uploaded: an example, a test, a game kept to one machine. `hopple dev` and `hopple check` still run it; `hopple upload` and `hopple publish` refuse it, and so does a server it is sent to another way. Absent, it may be published.",
69
+ "markdownDescription": "`false` for a game that is never to be uploaded: an example, a test, a\ngame kept to one machine. `hopple dev` and `hopple check` still run it;\n`hopple upload` and `hopple publish` refuse it, and so does a server it\nis sent to another way. Absent, it may be published."
70
+ },
66
71
  "version": {
67
72
  "type": "string",
68
73
  "description": "The game's own version, `MAJOR.MINOR.PATCH`. Ours to read, never to parse for meaning.",
@@ -84,6 +89,16 @@
84
89
  "maxLength": 500,
85
90
  "errorMessage": "must not be blank"
86
91
  },
92
+ "genres": {
93
+ "type": "array",
94
+ "items": {
95
+ "$ref": "#/definitions/GameGenre"
96
+ },
97
+ "description": "What kind of game it is, the main one first. Read by whoever files it in a catalogue, which never runs it. At most `GENRE_MAX`, not counting `local-multiplayer` and `online-multiplayer`: a game that is every kind tells a player nothing. The publisher's claim, taken at its word, except for those two. See `GameGenre`.",
98
+ "markdownDescription": "What kind of game it is, the main one first. Read by whoever files it in a\ncatalogue, which never runs it. At most `GENRE_MAX`, not counting\n`local-multiplayer` and `online-multiplayer`: a game that is every kind\ntells a player nothing. The publisher's claim, taken at its word, except\nfor those two. See `GameGenre`.",
99
+ "minItems": 1,
100
+ "maxItems": 5
101
+ },
87
102
  "cover": {
88
103
  "type": "string",
89
104
  "description": "A picture of the game, shown wherever a link to it is pasted — a chat, a post, a message.\n\nThe path of a PNG, JPEG or WebP file, relative to the config the manifest is in, and at most `COVER_MAX_BYTES`. `hopple publish` uploads it beside the game. 1200 × 630 is the shape every platform shows whole; without one, a link shows Hopple's own card.",
@@ -165,6 +180,34 @@
165
180
  "pattern": "^(?:(?:0|[1-9][0-9]*)\\.(?:0|[1-9][0-9]*)|next)$",
166
181
  "errorMessage": "must be the API the game is written against, like \"1.0\", or \"next\""
167
182
  },
183
+ "GameGenre": {
184
+ "type": "string",
185
+ "enum": [
186
+ "racing",
187
+ "shooter",
188
+ "platformer",
189
+ "puzzle",
190
+ "arcade",
191
+ "physics",
192
+ "roguelike",
193
+ "strategy",
194
+ "building",
195
+ "idle",
196
+ "sports",
197
+ "horror",
198
+ "stealth",
199
+ "fighting",
200
+ "casual",
201
+ "simulation",
202
+ "adventure",
203
+ "rhythm",
204
+ "rpg",
205
+ "local-multiplayer",
206
+ "online-multiplayer"
207
+ ],
208
+ "description": "What kind of game this is, as one word from a list every game shares.\n\nA closed list, because a client files games under these and has to name each one in front of a player: left to free text, one kind of game becomes three spellings of it with a game each. The list only grows, since a published manifest has to stay valid. Whoever reads one may be older than the game, and ignores a word it does not know.\n\n`local-multiplayer` and `online-multiplayer` say how a game is played rather than what it is, and only count where the manifest bears them out: see `declaredGenres`.",
209
+ "markdownDescription": "What kind of game this is, as one word from a list every game shares.\n\nA closed list, because a client files games under these and has to name each\none in front of a player: left to free text, one kind of game becomes three\nspellings of it with a game each. The list only grows, since a published\nmanifest has to stay valid. Whoever reads one may be older than the game, and\nignores a word it does not know.\n\n`local-multiplayer` and `online-multiplayer` say how a game is played rather\nthan what it is, and only count where the manifest bears them out: see\n`declaredGenres`."
210
+ },
168
211
  "Orientation": {
169
212
  "type": "string",
170
213
  "enum": [
@@ -678,6 +721,13 @@
678
721
  "$ref": "#/definitions/ControlAxis",
679
722
  "description": "`thumbstick` only. See `ControlAxis`. Absent, the stick moves both ways.",
680
723
  "markdownDescription": "`thumbstick` only. See `ControlAxis`. Absent, the stick moves both ways."
724
+ },
725
+ "scale": {
726
+ "type": "number",
727
+ "description": "How big this control is next to the layout's others: `1.25` for a jump button a thumb lands on without looking, `0.8` for one pressed now and then. Absent, 1.\n\nA size in the layout rather than in the game's stylesheet, because whether the layout fits on a small screen is worked out from it before the controls are drawn. A game's stylesheet changes how they look, never how much of the screen they answer in.",
728
+ "markdownDescription": "How big this control is next to the layout's others: `1.25` for a jump\nbutton a thumb lands on without looking, `0.8` for one pressed now and then.\nAbsent, 1.\n\nA size in the layout rather than in the game's stylesheet, because whether\nthe layout fits on a small screen is worked out from it before the\ncontrols are drawn. A game's stylesheet changes how they look, never how\nmuch of the screen they answer in.",
729
+ "minimum": 0.75,
730
+ "maximum": 1.5
681
731
  }
682
732
  },
683
733
  "required": [
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/analytics.d.ts
1
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/protocol/src/analytics.d.ts
2
2
  /**
3
3
  * What a game says happened.
4
4
  *
@@ -29,7 +29,7 @@ type AnalyticsValue = string | number | boolean;
29
29
  /** @group Analytics */
30
30
  type AnalyticsProps = Record<string, AnalyticsValue>;
31
31
  //#endregion
32
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/errors.d.ts
32
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/protocol/src/errors.d.ts
33
33
  /**
34
34
  * How a call fails.
35
35
  *
@@ -115,7 +115,7 @@ declare class BridgeError extends Error {
115
115
  constructor(error: ProtocolError);
116
116
  }
117
117
  //#endregion
118
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/input.d.ts
118
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/protocol/src/input.d.ts
119
119
  /**
120
120
  * What the host says about how this game is being played.
121
121
  *
@@ -175,7 +175,7 @@ type InputBinding = {
175
175
  label?: string;
176
176
  };
177
177
  //#endregion
178
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/pause.d.ts
178
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/protocol/src/pause.d.ts
179
179
  /**
180
180
  * Whether the game is holding still, which the host may ask for and only the
181
181
  * game can answer.
@@ -233,7 +233,7 @@ type PauseState =
233
233
  */
234
234
  'unpausable';
235
235
  //#endregion
236
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/save.d.ts
236
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/protocol/src/save.d.ts
237
237
  /**
238
238
  * The one thing a game may keep.
239
239
  *
@@ -284,7 +284,7 @@ type SaveStored = {
284
284
  bytes: number;
285
285
  };
286
286
  //#endregion
287
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/score.d.ts
287
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/protocol/src/score.d.ts
288
288
  /**
289
289
  * What the host says came of it.
290
290
  *
@@ -322,7 +322,7 @@ type ScoreStanding = {
322
322
  of?: number;
323
323
  };
324
324
  //#endregion
325
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/share.d.ts
325
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/protocol/src/share.d.ts
326
326
  /**
327
327
  * A link to this game, offered to the player by the host.
328
328
  *
@@ -421,7 +421,7 @@ type ShareMeta = {
421
421
  [key: string]: ShareMetaValue;
422
422
  };
423
423
  //#endregion
424
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/methods.d.ts
424
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/protocol/src/methods.d.ts
425
425
  /**
426
426
  * How far a game has got with its own preparation.
427
427
  *
@@ -601,7 +601,7 @@ type DeviceInfo = {
601
601
  tier: DeviceTier;
602
602
  };
603
603
  //#endregion
604
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/protocol/src/listeners.d.ts
604
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/protocol/src/listeners.d.ts
605
605
  /**
606
606
  * The one way anything here is listened to: `on(listener)` hands back the
607
607
  * function that stops it.
@@ -630,7 +630,7 @@ type DeviceInfo = {
630
630
  */
631
631
  type Unsubscribe = () => void;
632
632
  //#endregion
633
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/manifest/src/manifest.d.ts
633
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/manifest/src/manifest.d.ts
634
634
  /**
635
635
  * The API a game is written against: `M.m`, a major and the least minor it
636
636
  * needs, or `next`, the pre-releases of the next stable release.
@@ -1309,6 +1309,20 @@ type ControlSlot = {
1309
1309
  as?: InputStick | Record<string, InputBinding$1>;
1310
1310
  /** `thumbstick` only. See `ControlAxis`. Absent, the stick moves both ways. */
1311
1311
  axis?: ControlAxis;
1312
+ /**
1313
+ * How big this control is next to the layout's others: `1.25` for a jump
1314
+ * button a thumb lands on without looking, `0.8` for one pressed now and then.
1315
+ * Absent, 1.
1316
+ *
1317
+ * A size in the layout rather than in the game's stylesheet, because whether
1318
+ * the layout fits on a small screen is worked out from it before the
1319
+ * controls are drawn. A game's stylesheet changes how they look, never how
1320
+ * much of the screen they answer in.
1321
+ *
1322
+ * @minimum 0.75
1323
+ * @maximum 1.5
1324
+ */
1325
+ scale?: number;
1312
1326
  };
1313
1327
  /**
1314
1328
  * The one direction an on-screen thumbstick moves in: `x` across, `y` up and
@@ -1459,6 +1473,23 @@ type PreviewsDeclaration = {
1459
1473
  */
1460
1474
  network?: PreviewRevision;
1461
1475
  };
1476
+ /**
1477
+ * What kind of game this is, as one word from a list every game shares.
1478
+ *
1479
+ * A closed list, because a client files games under these and has to name each
1480
+ * one in front of a player: left to free text, one kind of game becomes three
1481
+ * spellings of it with a game each. The list only grows, since a published
1482
+ * manifest has to stay valid. Whoever reads one may be older than the game, and
1483
+ * ignores a word it does not know.
1484
+ *
1485
+ * `local-multiplayer` and `online-multiplayer` say how a game is played rather
1486
+ * than what it is, and only count where the manifest bears them out: see
1487
+ * `declaredGenres`.
1488
+ *
1489
+ * @group Manifest
1490
+ * @category Useful
1491
+ */
1492
+ 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';
1462
1493
  /**
1463
1494
  * Publishing refuses a key the manifest's API does not know; a host reading a
1464
1495
  * published one ignores it (see `validateManifest`).
@@ -1498,6 +1529,15 @@ type GameManifest = {
1498
1529
  * @errorMessage must be a handle: lowercase letters, digits and hyphens
1499
1530
  */
1500
1531
  owner?: string;
1532
+ /**
1533
+ * `false` for a game that is never to be uploaded: an example, a test, a
1534
+ * game kept to one machine. `hopple dev` and `hopple check` still run it;
1535
+ * `hopple upload` and `hopple publish` refuse it, and so does a server it
1536
+ * is sent to another way. Absent, it may be published.
1537
+ *
1538
+ * @category Useful
1539
+ */
1540
+ publishable?: boolean;
1501
1541
  /**
1502
1542
  * The game's own version, `MAJOR.MINOR.PATCH`. Ours to read, never to parse for meaning.
1503
1543
  *
@@ -1522,6 +1562,18 @@ type GameManifest = {
1522
1562
  * @errorMessage must not be blank
1523
1563
  */
1524
1564
  description?: string;
1565
+ /**
1566
+ * What kind of game it is, the main one first. Read by whoever files it in a
1567
+ * catalogue, which never runs it. At most `GENRE_MAX`, not counting
1568
+ * `local-multiplayer` and `online-multiplayer`: a game that is every kind
1569
+ * tells a player nothing. The publisher's claim, taken at its word, except
1570
+ * for those two. See `GameGenre`.
1571
+ *
1572
+ * @category Useful
1573
+ * @minItems 1
1574
+ * @maxItems 5
1575
+ */
1576
+ genres?: GameGenre[];
1525
1577
  /**
1526
1578
  * A picture of the game, shown wherever a link to it is pasted — a chat, a
1527
1579
  * post, a message.
@@ -1654,7 +1706,7 @@ type ProjectManifest = Omit<GameManifest, 'api'> & {
1654
1706
  api?: ApiVersion;
1655
1707
  };
1656
1708
  //#endregion
1657
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/achievements.d.ts
1709
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/achievements.d.ts
1658
1710
  /**
1659
1711
  * Where this player stands on one achievement.
1660
1712
  *
@@ -1752,7 +1804,7 @@ type Achievements<Id extends string = string, Measured extends string = Id> = {
1752
1804
  progress(id: Measured, value: number): void;
1753
1805
  };
1754
1806
  //#endregion
1755
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/analytics.d.ts
1807
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/analytics.d.ts
1756
1808
  /**
1757
1809
  * @group Analytics
1758
1810
  * @category Important
@@ -1769,7 +1821,7 @@ type Analytics<Events extends object = Record<string, AnalyticsProps>> = {
1769
1821
  track<Event extends keyof Events & string>(name: Event, props?: Events[Event]): void;
1770
1822
  };
1771
1823
  //#endregion
1772
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/audio.d.ts
1824
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/audio.d.ts
1773
1825
  /**
1774
1826
  * Something about the game's sound changed: the host's order, the player's
1775
1827
  * mute, a fader. Read what you need — `volume(channel)`, `allowed` — and apply
@@ -1860,7 +1912,7 @@ type Audio<Channel extends string = string> = {
1860
1912
  media(element: HTMLMediaElement, channel?: Channel): Unsubscribe;
1861
1913
  };
1862
1914
  //#endregion
1863
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/info.d.ts
1915
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/info.d.ts
1864
1916
  /**
1865
1917
  * What the host said, frozen.
1866
1918
  *
@@ -1878,7 +1930,7 @@ type Info = Readonly<{
1878
1930
  device: Readonly<DeviceInfo>;
1879
1931
  }>;
1880
1932
  //#endregion
1881
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/layout.d.ts
1933
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/layout.d.ts
1882
1934
  /**
1883
1935
  * How much of each edge belongs to the host. CSS pixels.
1884
1936
  *
@@ -1988,7 +2040,7 @@ type InsetPolicy = {
1988
2040
  */
1989
2041
  declare function insetsFrom(obstructions: readonly Obstruction[], frame: Size, policy?: InsetPolicy): Insets;
1990
2042
  //#endregion
1991
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/names.d.ts
2043
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/names.d.ts
1992
2044
  /**
1993
2045
  * The keys of a declared map, or `string` where the map is not a literal one —
1994
2046
  * which is what a manifest typed as the wide `GameManifest` gives — and
@@ -2108,7 +2160,7 @@ type EventProps<M> = M extends {
2108
2160
  */
2109
2161
  type StickName = 'pad.left.stick' | 'pad.right.stick' | 'pad.stick' | `controls.${string}`;
2110
2162
  //#endregion
2111
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/input/state.d.ts
2163
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/input/state.d.ts
2112
2164
  /**
2113
2165
  * Where the pointer is, in the frame's own coordinates.
2114
2166
  *
@@ -2169,7 +2221,7 @@ type InputSnapshot<Action extends string = string> = {
2169
2221
  pointer: PointerState | null;
2170
2222
  };
2171
2223
  //#endregion
2172
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/input/players.d.ts
2224
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/input/players.d.ts
2173
2225
  /**
2174
2226
  * What one player holds: a pad, one part of the keyboard, or the on-screen
2175
2227
  * controls.
@@ -2191,7 +2243,7 @@ type Controller =
2191
2243
  kind: 'touch';
2192
2244
  };
2193
2245
  //#endregion
2194
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/input/index.d.ts
2246
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/input/index.d.ts
2195
2247
  /**
2196
2248
  * A binding a game can name on its own, for a prompt about a button that is
2197
2249
  * not one of its actions: `pad.start`, `key.Space`.
@@ -2301,6 +2353,22 @@ type Input<Action extends string = string, Layout extends string = string, Playe
2301
2353
  describe(name: Action | ButtonName): string;
2302
2354
  /** The font family `glyph` draws in: name it, last, wherever a glyph is printed. */
2303
2355
  readonly font: string;
2356
+ /**
2357
+ * Draw an icon on an action's on-screen button instead of its label, or
2358
+ * `null` to go back to the label.
2359
+ *
2360
+ * hopple.input.icon('jump', 'M12 3 4 13h5v8h6v-8h5z');
2361
+ *
2362
+ * One SVG path, filled in the buttons' ink, in a 24 by 24 box: what icon
2363
+ * sets draw filled icons in. The label stays the button's name for a screen
2364
+ * reader. `glyph(action)` on a touch screen draws the same icon, so a prompt
2365
+ * matches the button. A path that cannot be read, or that leaves the box, is
2366
+ * reported to `onError` and the label is drawn instead.
2367
+ *
2368
+ * How the controls look otherwise is the game's stylesheet's: see the
2369
+ * skill's input guide.
2370
+ */
2371
+ icon(action: Action, path: string | null): void;
2304
2372
  /**
2305
2373
  * Stop reading input and let go of everything held.
2306
2374
  *
@@ -2412,7 +2480,7 @@ type Controls<Layout extends string = string> = {
2412
2480
  readonly available: boolean;
2413
2481
  };
2414
2482
  //#endregion
2415
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/lifecycle.d.ts
2483
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/lifecycle.d.ts
2416
2484
  /** @group Lifecycle */
2417
2485
  type LifecycleListener = (phase: Phase, previous: Phase) => void | Promise<void>;
2418
2486
  /**
@@ -2431,7 +2499,7 @@ type Lifecycle = {
2431
2499
  on(listener: LifecycleListener): Unsubscribe;
2432
2500
  };
2433
2501
  //#endregion
2434
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/loop.d.ts
2502
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/loop.d.ts
2435
2503
  /**
2436
2504
  * The host has let the game run its loop, or stopped it. Return a promise to
2437
2505
  * make the host wait.
@@ -2485,7 +2553,7 @@ type Loop = {
2485
2553
  tick(frame: FrameCallback): Unsubscribe;
2486
2554
  };
2487
2555
  //#endregion
2488
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/panel.d.ts
2556
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/panel.d.ts
2489
2557
  /**
2490
2558
  * The pages of the host's panel a game has a name for, as the keys of this
2491
2559
  * interface.
@@ -2623,7 +2691,7 @@ type Panel = {
2623
2691
  open(page: PageId): void;
2624
2692
  };
2625
2693
  //#endregion
2626
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/pause.d.ts
2694
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/pause.d.ts
2627
2695
  /**
2628
2696
  * The host has asked for a pause, or for the game to carry on.
2629
2697
  *
@@ -2684,7 +2752,7 @@ type Pause = {
2684
2752
  on(listener: PauseListener): Unsubscribe;
2685
2753
  };
2686
2754
  //#endregion
2687
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/resources.d.ts
2755
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/resources.d.ts
2688
2756
  /**
2689
2757
  * The host has let the game hold its resources again, or asked for them back.
2690
2758
  * Return a promise to make the host wait until they have gone, or come back.
@@ -2714,7 +2782,7 @@ type Resources = {
2714
2782
  on(listener: ResourcesListener): Unsubscribe;
2715
2783
  };
2716
2784
  //#endregion
2717
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/save.d.ts
2785
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/save.d.ts
2718
2786
  /**
2719
2787
  * What a game gets back from a read: its own schema number, and its state.
2720
2788
  *
@@ -2851,7 +2919,7 @@ type Save = {
2851
2919
  readonly maxBytes: number;
2852
2920
  };
2853
2921
  //#endregion
2854
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/score.d.ts
2922
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/score.d.ts
2855
2923
  /**
2856
2924
  * Where this player stands on every board, as far as anybody has said.
2857
2925
  *
@@ -2928,7 +2996,7 @@ type Score<Board extends string = string> = {
2928
2996
  }): Promise<ScoreResult>;
2929
2997
  };
2930
2998
  //#endregion
2931
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/share.d.ts
2999
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/share.d.ts
2932
3000
  /**
2933
3001
  * @group Share
2934
3002
  * @category Important
@@ -3000,7 +3068,7 @@ type Share = {
3000
3068
  }): void;
3001
3069
  };
3002
3070
  //#endregion
3003
- //#region ../../../../../../../tmp/hopple-sdk-types-SLrqDf/game/src/init.d.ts
3071
+ //#region ../../../../../../../tmp/hopple-sdk-types-A6KFyv/game/src/init.d.ts
3004
3072
  /**
3005
3073
  * What `init` hands back: every namespace a game has, on one object.
3006
3074
  *
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 1.0.0-next.4 | 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-A6KFyv/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.
@@ -671,6 +671,20 @@ type ControlSlot = {
671
671
  as?: InputStick | Record<string, InputBinding>;
672
672
  /** `thumbstick` only. See `ControlAxis`. Absent, the stick moves both ways. */
673
673
  axis?: ControlAxis;
674
+ /**
675
+ * How big this control is next to the layout's others: `1.25` for a jump
676
+ * button a thumb lands on without looking, `0.8` for one pressed now and then.
677
+ * Absent, 1.
678
+ *
679
+ * A size in the layout rather than in the game's stylesheet, because whether
680
+ * the layout fits on a small screen is worked out from it before the
681
+ * controls are drawn. A game's stylesheet changes how they look, never how
682
+ * much of the screen they answer in.
683
+ *
684
+ * @minimum 0.75
685
+ * @maximum 1.5
686
+ */
687
+ scale?: number;
674
688
  };
675
689
  /**
676
690
  * The one direction an on-screen thumbstick moves in: `x` across, `y` up and
@@ -821,6 +835,23 @@ type PreviewsDeclaration = {
821
835
  */
822
836
  network?: PreviewRevision;
823
837
  };
838
+ /**
839
+ * What kind of game this is, as one word from a list every game shares.
840
+ *
841
+ * A closed list, because a client files games under these and has to name each
842
+ * one in front of a player: left to free text, one kind of game becomes three
843
+ * spellings of it with a game each. The list only grows, since a published
844
+ * manifest has to stay valid. Whoever reads one may be older than the game, and
845
+ * ignores a word it does not know.
846
+ *
847
+ * `local-multiplayer` and `online-multiplayer` say how a game is played rather
848
+ * than what it is, and only count where the manifest bears them out: see
849
+ * `declaredGenres`.
850
+ *
851
+ * @group Manifest
852
+ * @category Useful
853
+ */
854
+ 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
855
  /**
825
856
  * Publishing refuses a key the manifest's API does not know; a host reading a
826
857
  * published one ignores it (see `validateManifest`).
@@ -860,6 +891,15 @@ type GameManifest = {
860
891
  * @errorMessage must be a handle: lowercase letters, digits and hyphens
861
892
  */
862
893
  owner?: string;
894
+ /**
895
+ * `false` for a game that is never to be uploaded: an example, a test, a
896
+ * game kept to one machine. `hopple dev` and `hopple check` still run it;
897
+ * `hopple upload` and `hopple publish` refuse it, and so does a server it
898
+ * is sent to another way. Absent, it may be published.
899
+ *
900
+ * @category Useful
901
+ */
902
+ publishable?: boolean;
863
903
  /**
864
904
  * The game's own version, `MAJOR.MINOR.PATCH`. Ours to read, never to parse for meaning.
865
905
  *
@@ -884,6 +924,18 @@ type GameManifest = {
884
924
  * @errorMessage must not be blank
885
925
  */
886
926
  description?: string;
927
+ /**
928
+ * What kind of game it is, the main one first. Read by whoever files it in a
929
+ * catalogue, which never runs it. At most `GENRE_MAX`, not counting
930
+ * `local-multiplayer` and `online-multiplayer`: a game that is every kind
931
+ * tells a player nothing. The publisher's claim, taken at its word, except
932
+ * for those two. See `GameGenre`.
933
+ *
934
+ * @category Useful
935
+ * @minItems 1
936
+ * @maxItems 5
937
+ */
938
+ genres?: GameGenre[];
887
939
  /**
888
940
  * A picture of the game, shown wherever a link to it is pasted — a chat, a
889
941
  * 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.4 | 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": [
@@ -372,7 +405,12 @@ const ControlSlot = {
372
405
  "type": "object",
373
406
  "additionalProperties": { "$ref": "#/definitions/InputBinding" }
374
407
  }] },
375
- "axis": { "$ref": "#/definitions/ControlAxis" }
408
+ "axis": { "$ref": "#/definitions/ControlAxis" },
409
+ "scale": {
410
+ "type": "number",
411
+ "minimum": .75,
412
+ "maximum": 1.5
413
+ }
376
414
  },
377
415
  "required": ["type"],
378
416
  "additionalProperties": false,
@@ -412,6 +450,7 @@ const InputStick = {
412
450
  const definitions = {
413
451
  GameManifest,
414
452
  ApiVersion,
453
+ GameGenre,
415
454
  Orientation,
416
455
  LayoutDeclaration,
417
456
  InsetMode,
@@ -573,6 +612,7 @@ const definitions = {
573
612
  };
574
613
  AchievementProgress.properties.target.maximum;
575
614
  GameManifest.required;
615
+ GameGenre.enum;
576
616
  /**
577
617
  * The zones a layout places controls in, in the order they are drawn.
578
618
  *
@@ -750,6 +790,9 @@ function walkObject(node, value, path, fail, lenient) {
750
790
  }
751
791
  }
752
792
  function checkReferences(manifest, fail) {
793
+ const genres = manifest.genres ?? [];
794
+ for (const genre of new Set(genres.filter((one, index) => genres.indexOf(one) !== index))) fail("genres", `names "${genre}" twice`);
795
+ 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
796
  const players = manifest.input?.player;
754
797
  const own = new Set(Object.keys(players?.actions ?? {}));
755
798
  const actions = /* @__PURE__ */ new Set([...Object.keys(manifest.input?.actions ?? {}), ...own]);
@@ -787,6 +830,8 @@ function checkReferences(manifest, fail) {
787
830
  }
788
831
  }
789
832
  }
833
+ /** The genres that say how a game is played together rather than what it is. */
834
+ const MULTIPLAYER_GENRES = ["local-multiplayer", "online-multiplayer"];
790
835
  /**
791
836
  * Whether `a` beats `b` on a board sorted this way. Ties are not improvements.
792
837
  *
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
 
@@ -219,7 +220,8 @@ up to 8 characters, 8 tiers.
219
220
  `buttons` (naming actions) arranged as a `row`, `column`, `diamond` or
220
221
  `cluster`. An unknown shape is drawn as a column. A stick held to an axis is
221
222
  drawn as a track and always reads 0 on the other axis; a real pad's stick
222
- still reports both.
223
+ still reports both. Any control may have a `scale` from 0.75 to 1.5, its
224
+ size next to the layout's others (1 when left out).
223
225
 
224
226
  Binding names:
225
227
 
@@ -295,6 +297,31 @@ Each channel gets its own labelled volume slider in the host's sound controls.
295
297
  `title` is required: it is the slider's label. `master` is reserved, since
296
298
  every host already has a master volume. At most 8 channels.
297
299
 
300
+ ### genres
301
+
302
+ ```json
303
+ "genres": ["puzzle", "shooter", "online-multiplayer"]
304
+ ```
305
+
306
+ What kind of game this is, the main one first: at least one, and at most 3
307
+ beside the two multiplayer words, none twice. A game with none declared is
308
+ filed by what its title and description say, and most often less well.
309
+
310
+ The words are `racing`, `shooter`, `platformer`, `puzzle`, `arcade`, `physics`,
311
+ `roguelike`, `strategy`, `building`, `idle`, `sports`, `horror`, `stealth`,
312
+ `fighting`, `casual`, `simulation`, `adventure`, `rhythm` and `rpg`, and two
313
+ that say how it is played together:
314
+
315
+ - **`local-multiplayer`**: several people on one device. Needs `players.local`
316
+ above 1.
317
+ - **`online-multiplayer`**: over the network. Needs `previews.network`.
318
+
319
+ Either of those without what it needs is not refused: the game is published,
320
+ that word is ignored, and `hopple check`, `hopple upload` and `hopple publish`
321
+ warn about it. They warn the other way round too, for a game that declares
322
+ `genres` and leaves out a way of playing together its manifest shows, since it
323
+ then is not listed as one. Clients group the two under one shelf.
324
+
298
325
  ### previews
299
326
 
300
327
  ```json
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.4",
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",