gamesteel 0.6.0 → 0.6.1

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.
@@ -24846,7 +24846,7 @@ function shouldInterruptAfterDeadline(deadline) {
24846
24846
  var import_sucrase3 = __toESM(require_dist2(), 1);
24847
24847
 
24848
24848
  // ../core/dist/version.js
24849
- var ENGINE_VERSION = "0.10.0";
24849
+ var ENGINE_VERSION = "0.10.1";
24850
24850
 
24851
24851
  // ../core/dist/sim/trig-table.js
24852
24852
  var QUARTER_WAVE = [
@@ -26111,6 +26111,10 @@ function ticksOf(d, line = 1, column = 1) {
26111
26111
  return ticks;
26112
26112
  }
26113
26113
 
26114
+ // ../core/dist/script/scan.js
26115
+ var import_parser = __toESM(require_parser(), 1);
26116
+ var import_types = __toESM(require_types(), 1);
26117
+
26114
26118
  // ../core/dist/home.js
26115
26119
  import { existsSync, renameSync, symlinkSync } from "node:fs";
26116
26120
  import { homedir } from "node:os";
@@ -27968,7 +27972,7 @@ var Parser = class {
27968
27972
  return clauses;
27969
27973
  }
27970
27974
  };
27971
- function parse(source) {
27975
+ function parse2(source) {
27972
27976
  return new Parser(tokenize(source)).parse();
27973
27977
  }
27974
27978
 
@@ -29274,7 +29278,7 @@ function checkStrings(ast, out) {
29274
29278
  function checkControls(ast, out) {
29275
29279
  const lines = ast.controls?.bindings ?? [];
29276
29280
  const players = playerButtonsOf(ast);
29277
- const mouse = ["click", "right_click", "middle_click"];
29281
+ const mouse = ["click", "right_click", "middle_click", "menu"];
29278
29282
  const builtIn = /* @__PURE__ */ new Set([...BUTTONS, ...mouse, ...players]);
29279
29283
  const buttons = [...BUTTONS, ...mouse, ...players, ...lines.map((l) => l.button).filter((b) => !builtIn.has(b))];
29280
29284
  const own2 = lines.filter((l) => !builtIn.has(l.button));
@@ -29535,6 +29539,27 @@ function numericProps(props) {
29535
29539
  return out;
29536
29540
  }
29537
29541
 
29542
+ // ../core/dist/script/prefs.js
29543
+ var NO_PREFS_YET = { sound: true, music: true, fullscreen: false, canFullscreen: false, touch: false };
29544
+ var usesPrefs = (sources) => sources.some((source) => /\.\s*prefs\b/.test(source));
29545
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29546
+ function isPrefs(value) {
29547
+ return isObject(value) && Object.keys(value).length === 5 && typeof value["sound"] === "boolean" && typeof value["music"] === "boolean" && typeof value["fullscreen"] === "boolean" && typeof value["canFullscreen"] === "boolean" && typeof value["touch"] === "boolean";
29548
+ }
29549
+ var CHANGEABLE = ["sound", "music", "fullscreen"];
29550
+ function prefsChangeOf(raw) {
29551
+ const said2 = "prefs.set takes sound, music and fullscreen, each true or false, like prefs.set({ music: false })";
29552
+ if (!isObject(raw))
29553
+ throw new Error(said2);
29554
+ const out = {};
29555
+ for (const [key, value] of Object.entries(raw)) {
29556
+ if (!CHANGEABLE.includes(key) || typeof value !== "boolean")
29557
+ throw new Error(`${said2}, not ${JSON.stringify({ [key]: value })}`);
29558
+ out[key] = value;
29559
+ }
29560
+ return out;
29561
+ }
29562
+
29538
29563
  // ../core/dist/script/ads.js
29539
29564
  var AD_KINDS = ["break", "reward"];
29540
29565
  var NO_ADS_YET = { pending: null, lastBreak: null, lastPress: null, lastPlay: null, played: false, scene: null, ready: AD_KINDS, banner: null };
@@ -29591,7 +29616,7 @@ var ONLINE_LIMITS = {
29591
29616
  downloadBytes: 1e6
29592
29617
  };
29593
29618
  var TOO_MANY_ASKS = `a tick asks the game's server at most ${String(ONLINE_LIMITS.asksPerTick)} times: spread the calls over ticks`;
29594
- var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29619
+ var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29595
29620
  var jsonBytes = (data) => {
29596
29621
  try {
29597
29622
  const text2 = JSON.stringify(data);
@@ -29620,7 +29645,7 @@ var uploadProblem = (ask) => {
29620
29645
  };
29621
29646
  function askProblem(ask) {
29622
29647
  const kinds = "an online ask is a call, join, leave, send, upload, download or askPush";
29623
- if (!isObject(ask))
29648
+ if (!isObject2(ask))
29624
29649
  return kinds;
29625
29650
  switch (ask["kind"]) {
29626
29651
  case "call":
@@ -29644,7 +29669,7 @@ function askProblem(ask) {
29644
29669
  }
29645
29670
  var usesOnline = (sources) => sources.some((source) => /\bonline\s*\.\s*(call|join|leave|send|state|upload|uploadScreen|download|askPush)\b/.test(source));
29646
29671
  function isOnlineHeard(value) {
29647
- if (!isObject(value))
29672
+ if (!isObject2(value))
29648
29673
  return false;
29649
29674
  switch (value["kind"]) {
29650
29675
  case "answer":
@@ -29671,9 +29696,9 @@ function isOnlineHeard(value) {
29671
29696
  // ../core/dist/script/purchases.js
29672
29697
  var PURCHASE_RESULTS = ["granted", "pending", "cancelled", "unavailable", "refused"];
29673
29698
  var PRODUCT_ID = /^[a-z0-9][a-z0-9_.]{0,39}$/;
29674
- var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29699
+ var isObject3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29675
29700
  function purchaseAskProblem(ask) {
29676
- if (!isObject2(ask))
29701
+ if (!isObject3(ask))
29677
29702
  return "a purchase ask is a buy of a product or a restore";
29678
29703
  if (ask["kind"] === "restore")
29679
29704
  return null;
@@ -29682,9 +29707,9 @@ function purchaseAskProblem(ask) {
29682
29707
  const id = ask["id"];
29683
29708
  return typeof id === "string" && PRODUCT_ID.test(id) ? null : `a product id is 1 to 40 lowercase letters, digits, _ and ., starting with a letter or digit, like coins_100, not ${JSON.stringify(id) ?? String(id)}`;
29684
29709
  }
29685
- var isProduct = (value) => isObject2(value) && typeof value["id"] === "string" && typeof value["price"] === "string" && typeof value["name"] === "string";
29710
+ var isProduct = (value) => isObject3(value) && typeof value["id"] === "string" && typeof value["price"] === "string" && typeof value["name"] === "string";
29686
29711
  function isPurchaseHeard(value) {
29687
- if (!isObject2(value))
29712
+ if (!isObject3(value))
29688
29713
  return false;
29689
29714
  switch (value["kind"]) {
29690
29715
  case "products":
@@ -30721,6 +30746,16 @@ const __online = Object.freeze({
30721
30746
  askPush() { __call('onlineAskPush', {}) },
30722
30747
  get state() { return __call('onlineState', {}) },
30723
30748
  })
30749
+ /** The player's settings (a game's own pause menu): read as the page last said them; a change asked of the page, which
30750
+ * makes it after the tick and says it on a later one. */
30751
+ const __prefs = Object.freeze({
30752
+ get sound() { return __call('prefs', {}).sound === true },
30753
+ get music() { return __call('prefs', {}).music === true },
30754
+ get fullscreen() { return __call('prefs', {}).fullscreen === true },
30755
+ get canFullscreen() { return __call('prefs', {}).canFullscreen === true },
30756
+ get touch() { return __call('prefs', {}).touch === true },
30757
+ set(change) { __call('prefsSet', { change: change === undefined ? null : change }) },
30758
+ })
30724
30759
  /** Purchases (the online services spec, part 3): asked of the page, which asks the store; answered on a later tick as a
30725
30760
  * 'purchase' event. */
30726
30761
  const __purchases = Object.freeze({
@@ -31006,6 +31041,7 @@ const __game = {
31006
31041
  get ui() { return __ui },
31007
31042
  get ads() { return __ads },
31008
31043
  get online() { return __online },
31044
+ get prefs() { return __prefs },
31009
31045
  get purchases() { return __purchases },
31010
31046
  /** The game script's own fields, read only; null without one. */
31011
31047
  get script() { return __frozen(__revived(__call('gameScript', null))) },
@@ -31440,19 +31476,19 @@ globalThis.noise = noise
31440
31476
  `;
31441
31477
 
31442
31478
  // ../core/dist/script/top-level.js
31443
- var import_parser = __toESM(require_parser(), 1);
31479
+ var import_parser2 = __toESM(require_parser(), 1);
31444
31480
  var import_tokenizer = __toESM(require_tokenizer(), 1);
31445
- var import_types = __toESM(require_types(), 1);
31481
+ var import_types2 = __toESM(require_types(), 1);
31446
31482
  var import_keywords = __toESM(require_keywords(), 1);
31447
31483
  var CHANGEABLE_TOP_LEVEL = "a script keeps its state in its fields: a top-level let or var lives outside the game, where a snapshot cannot hold it and a second play does not start from it; make it a field, or const if it never changes";
31448
31484
  var PRIVATE_STATIC = "a private static field cannot be frozen, so it would keep state outside the game, shared by every object: make it an instance field, or a const at the top of the script";
31449
31485
  var PRIVATE_MEMBER = "a private field or method (#name) cannot be kept between ticks: a script's state is its plain fields, and an object made again from them, after a restore or a failed call, has no private ones; drop the #, and keep it as a field or a method";
31450
- var DECLARING = /* @__PURE__ */ new Set([import_types.TokenType._const, import_types.TokenType._let, import_types.TokenType._var, import_types.TokenType._function, import_types.TokenType._class, import_types.TokenType._enum]);
31451
- var OPENING = /* @__PURE__ */ new Set([import_types.TokenType.parenL, import_types.TokenType.bracketL, import_types.TokenType.braceL, import_types.TokenType.dollarBraceL]);
31452
- var CLOSING = /* @__PURE__ */ new Set([import_types.TokenType.parenR, import_types.TokenType.bracketR, import_types.TokenType.braceR]);
31486
+ var DECLARING = /* @__PURE__ */ new Set([import_types2.TokenType._const, import_types2.TokenType._let, import_types2.TokenType._var, import_types2.TokenType._function, import_types2.TokenType._class, import_types2.TokenType._enum]);
31487
+ var OPENING = /* @__PURE__ */ new Set([import_types2.TokenType.parenL, import_types2.TokenType.bracketL, import_types2.TokenType.braceL, import_types2.TokenType.dollarBraceL]);
31488
+ var CLOSING = /* @__PURE__ */ new Set([import_types2.TokenType.parenR, import_types2.TokenType.bracketR, import_types2.TokenType.braceR]);
31453
31489
  var lineAt = (source, offset) => source.slice(0, offset).split("\n").length;
31454
31490
  function topLevelOf(source) {
31455
- const { tokens, scopes } = (0, import_parser.parse)(source, false, true, false);
31491
+ const { tokens, scopes } = (0, import_parser2.parse)(source, false, true, false);
31456
31492
  const at2 = (i) => {
31457
31493
  const token = tokens[i];
31458
31494
  return { name: token === void 0 ? "" : source.slice(token.start, token.end), line: lineAt(source, token?.start ?? 0) };
@@ -31464,21 +31500,21 @@ function topLevelOf(source) {
31464
31500
  const functions = scopes.filter((scope) => scope.isFunctionScope && !(scope.startTokenIndex === 0 && scope.endTokenIndex >= tokens.length));
31465
31501
  const inFunction = (i) => functions.some((scope) => scope.startTokenIndex <= i && i < scope.endTokenIndex);
31466
31502
  for (const [i, token] of tokens.entries()) {
31467
- if (token.type === import_types.TokenType._var && !inFunction(i))
31503
+ if (token.type === import_types2.TokenType._var && !inFunction(i))
31468
31504
  changeable.push({ ...at2(i + 1), line: at2(i).line });
31469
31505
  if (token.contextualKeyword === import_keywords.ContextualKeyword._static) {
31470
31506
  let next = i + 1;
31471
31507
  while (tokens[next]?.contextualKeyword === import_keywords.ContextualKeyword._readonly)
31472
31508
  next += 1;
31473
- if (tokens[next]?.type === import_types.TokenType.hash && tokens[next + 2]?.type !== import_types.TokenType.parenL)
31509
+ if (tokens[next]?.type === import_types2.TokenType.hash && tokens[next + 2]?.type !== import_types2.TokenType.parenL)
31474
31510
  privateStatics.push({ ...at2(next + 1), name: `#${at2(next + 1).name}` });
31475
31511
  }
31476
- if (token.type === import_types.TokenType.hash) {
31512
+ if (token.type === import_types2.TokenType.hash) {
31477
31513
  let back2 = i - 1;
31478
31514
  while (tokens[back2]?.contextualKeyword === import_keywords.ContextualKeyword._readonly)
31479
31515
  back2 -= 1;
31480
31516
  const before2 = tokens[back2];
31481
- const used = before2 !== void 0 && (before2.type === import_types.TokenType.dot || before2.type === import_types.TokenType.questionDot);
31517
+ const used = before2 !== void 0 && (before2.type === import_types2.TokenType.dot || before2.type === import_types2.TokenType.questionDot);
31482
31518
  if (!used && before2?.contextualKeyword !== import_keywords.ContextualKeyword._static)
31483
31519
  privateMembers.push({ ...at2(i + 1), name: `#${at2(i + 1).name}` });
31484
31520
  }
@@ -31498,7 +31534,7 @@ function topLevelOf(source) {
31498
31534
  else if (depth <= 0 && DECLARING.has(type))
31499
31535
  break;
31500
31536
  }
31501
- if (tokens[back]?.type === import_types.TokenType._let)
31537
+ if (tokens[back]?.type === import_types2.TokenType._let)
31502
31538
  changeable.push(declared);
31503
31539
  }
31504
31540
  changeable.sort((a, b) => a.line - b.line);
@@ -32369,6 +32405,8 @@ function initialScripts(given, kinds, seed = 1, saved = {}, audio, fonts, adsOff
32369
32405
  ...adsOff === void 0 ? {} : { adsOff },
32370
32406
  // A game whose scripts mention online keeps its state and call count from the first tick; any other never has them.
32371
32407
  ...usesOnline([...Object.values(sources.kinds), sources.game ?? "", ...Object.values(sources.modules ?? {})]) ? { onlineState: "off", nextCall: 1 } : {},
32408
+ // A game whose scripts mention the player's settings keeps them from the first tick; any other never has them.
32409
+ ...usesPrefs([...Object.values(sources.kinds), sources.game ?? "", ...Object.values(sources.modules ?? {})]) ? { prefs: NO_PREFS_YET } : {},
32372
32410
  // A game whose scripts mention purchases keeps its products from the first tick; any other never has them.
32373
32411
  ...usesPurchases([...Object.values(sources.kinds), sources.game ?? "", ...Object.values(sources.modules ?? {})]) ? { purchases: NO_PURCHASES_YET } : {}
32374
32412
  };
@@ -33981,7 +34019,7 @@ type Side = 'above' | 'below' | 'side' | 'overlap'
33981
34019
  type Colour = string | number
33982
34020
  /* KindName, PropName and Button are each game's own (the whole-engine review, I11): its
33983
34021
  * kinds; the properties it declares, or a rule or a script sets; and the buttons every game
33984
- * has (left, right, up, down, jump, action, and click, the left mouse button) with those its
34022
+ * has (left, right, up, down, jump, action, click, the left mouse button, and menu: see Prefs) with those its
33985
34023
  * controls name. A name written
33986
34024
  * in quotes that the game does not have is an error where it is written; a name held in a
33987
34025
  * string (a field, a table) is checked as the game plays. */
@@ -34293,87 +34331,6 @@ interface Turns {
34293
34331
  readonly round: number
34294
34332
  }
34295
34333
 
34296
- /** Where a UI element sits in what holds it (the screen, or the element its in names): one of nine points. */
34297
- type UiAnchor = 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right'
34298
- /** A UI element, in design units (480 by 270 unless ui.design says), scaled to the real screen: the design's long side to
34299
- * its long side, so a phone held either way shows it at the same size, and what is wider than a narrow screen is the
34300
- * game's to lay out (a playtest look with ui: true says what runs off). */
34301
- interface UiSpec {
34302
- /** 'panel' when left out. Only a button is hovered, pressed, clicked and focused. A row or column places the elements
34303
- * whose in names it, in the order they were first set, gap apart, and is as big as them unless given a size. */
34304
- kind?: 'panel' | 'button' | 'label' | 'meter' | 'icon' | 'row' | 'column'
34305
- /** The point of what holds it that it sits at; 'top-left' when left out. */
34306
- anchor?: UiAnchor
34307
- /** Design units in from the anchored edges: { anchor: 'top-right', at: [8, 8] } sits 8 in from the top and the
34308
- * right; from a middle, x moves right and y down. [0, 0] when left out. A row or column places its own. */
34309
- at?: [number, number]
34310
- /** [width, height] in design units: needed, but for a row or column. */
34311
- size?: [number, number]
34312
- /** The element it sits inside, by name: anchored in its box, or placed by it when it is a row or column. */
34313
- in?: string
34314
- /** Design units between what a row or column holds. */
34315
- gap?: number
34316
- /** Design units inside a row's or column's edge. */
34317
- pad?: number
34318
- /** Where what a row or column holds sits across it; 'center' when left out. */
34319
- align?: 'start' | 'center' | 'end'
34320
- /** Never hovered, pressed, focused or clicked; its state reads 'disabled', to draw it greyed. */
34321
- disabled?: boolean
34322
- /** false starts it hidden, for ui.show to bring in. */
34323
- shown?: boolean
34324
- }
34325
- /** An element as this screen shows it, in pixels as g.screen draws. */
34326
- interface UiBox {
34327
- readonly x: number
34328
- readonly y: number
34329
- readonly w: number
34330
- readonly h: number
34331
- /** 0 hidden to 1 shown, eased, as ui.show moves it: draw by it for a slide, a fade or a pop. */
34332
- readonly shown: number
34333
- readonly state: 'disabled' | 'pressed' | 'hover' | 'focused' | 'idle'
34334
- /** Screen pixels in one design unit: size what is drawn in the box by it, as 14 * b.scale. */
34335
- readonly scale: number
34336
- }
34337
- /** Text fitted to a box: the size it fits at, its lines, and whether what did not fit was cut. */
34338
- interface UiFit { readonly size: number; readonly lines: string[]; readonly width: number; readonly height: number; readonly cut: boolean }
34339
- /** game.ui: the game's UI elements. The engine places them on any screen, kept clear of a phone's notch and corners,
34340
- * and tracks the pointer, a finger, presses, clicks and focus. It draws nothing: each element looks as the game's draw
34341
- * draws it at ui.box, so make it look like this game and no other. Declare in start (or in update: the same again
34342
- * changes nothing), read clicks in update or whilePaused, draw in draw. */
34343
- interface Ui {
34344
- /** Declares an element, or changes one; 64 at most. Never in draw. */
34345
- set(name: string, spec: UiSpec): void
34346
- /** Takes an element away, with those inside it. */
34347
- remove(name: string): void
34348
- /** Eases it in (true) or out (false) over the seconds (0.2 when left out), on the real clock, so a pause menu slides
34349
- * in while the world holds. A hidden one is never clicked. */
34350
- show(name: string, shown: boolean, seconds?: number): void
34351
- /** Gives the keys to a menu: up, down, left and right move focus to the nearest button that way, and jump or action
34352
- * clicks it. The game still sees those buttons, so hold its own controls while ui.focused is not null; null gives
34353
- * them back. */
34354
- focus(name: string | null): void
34355
- /** The size the elements are written in, 480 by 270 when never set. */
34356
- design(width: number, height: number): void
34357
- /** Where it is on this screen and how it stands; null for a name never set. update reads it at the screen played,
34358
- * draw at the screen drawn. */
34359
- box(name: string): UiBox | null
34360
- /** How it stands: idle, hover, pressed, focused or disabled; null for a name not set, as box gives. */
34361
- state(name: string): UiBox['state'] | null
34362
- /** Clicked since the last update: pressed and let go on it, by the pointer or a finger, or jump or action on its
34363
- * focus. Heard once. A press on a button is the UI's: input.pressed('click') never sees it. False for a name not
34364
- * set, so a game reading one it removed runs on. */
34365
- clicked(name: string): boolean
34366
- /** The largest size, from size (16 when left out) down to min (8), at which the text wrapped to the box fits it; at
34367
- * min, what still does not fit is cut, ending in an ellipsis. text's fit option does this as it draws. */
34368
- fit(text: string | number, box: { w: number; h: number }, options?: { size?: number; min?: number; font?: FontName; lineHeight?: number }): UiFit
34369
- /** The focused button, or null. */
34370
- readonly focused: string | null
34371
- /** Screen pixels in one design unit on the screen played. */
34372
- readonly scale: number
34373
- /** The names set, in the order they were first set. */
34374
- readonly names: readonly string[]
34375
- }
34376
-
34377
34334
  /** A level's back layer, drawn behind the objects and darker, or its front layer, drawn over them; neither collides. */
34378
34335
  interface TileLayerOption { layer?: 'back' | 'front' }
34379
34336
 
@@ -34683,6 +34640,8 @@ interface Game {
34683
34640
  readonly ads: Ads
34684
34641
  /** The game's own server: calls and live rooms. */
34685
34642
  readonly online: Online
34643
+ /** The player's sound, music and fullscreen, for the game's own pause menu. */
34644
+ readonly prefs: Prefs
34686
34645
  /** Real-money purchases in the game's store app. */
34687
34646
  readonly purchases: Purchases
34688
34647
  /** Opens the game's privacy policy, as a game on the app stores must from inside itself: call it when the player
@@ -34742,6 +34701,116 @@ interface Game {
34742
34701
  burst(x: In3D<number, 'burst(x, y, z)'>, y: number, z: number, options?: BurstOptions): void
34743
34702
  }
34744
34703
 
34704
+ /** The player's own settings, which the page keeps for every game they play: what a game's own pause menu shows and
34705
+ * changes. A game whose scripts read the button 'menu' draws its own pause menu: the page presses menu where it would
34706
+ * open its own (P, Escape, a pad's Start, the touch pause button, an app's back button), and the game pauses itself
34707
+ * with game.pause() and resumes with game.resume(). Read as the page last said; a change is asked of the page, which
34708
+ * makes it after the tick and says it on a later one. */
34709
+ interface Prefs {
34710
+ /** Sound effects on. */
34711
+ readonly sound: boolean
34712
+ /** Music on. */
34713
+ readonly music: boolean
34714
+ /** The page fills the screen. */
34715
+ readonly fullscreen: boolean
34716
+ /** The page may fill the screen: false where it cannot (a frame that forbids it, most phones' browsers, an app), so a
34717
+ * menu leaves its Fullscreen row out. */
34718
+ readonly canFullscreen: boolean
34719
+ /** The player is on a touch screen (a finger has touched it): show the game's own touch controls, the UI elements
34720
+ * with touch, and its sticks. */
34721
+ readonly touch: boolean
34722
+ /** Asks the page to change them: prefs.set({ music: !this.game.prefs.music }). Fullscreen only on a press or a click,
34723
+ * as browsers allow it. Never from draw. */
34724
+ set(change: { sound?: boolean; music?: boolean; fullscreen?: boolean }): void
34725
+ }
34726
+
34727
+ /** Where a UI element sits in what holds it (the screen, or the element its in names): one of nine points. */
34728
+ type UiAnchor = 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right'
34729
+ /** A UI element, in design units (480 by 270 unless ui.design says), scaled to the real screen: the design's long side to
34730
+ * its long side, so a phone held either way shows it at the same size, and what is wider than a narrow screen is the
34731
+ * game's to lay out (a playtest look with ui: true says what runs off). */
34732
+ interface UiSpec {
34733
+ /** 'panel' when left out. Only a button is hovered, pressed, clicked and focused. A row or column places the elements
34734
+ * whose in names it, in the order they were first set, gap apart, and is as big as them unless given a size. A stick
34735
+ * is a touch control: a finger on it (or that drifts off its edge) is player 1's left stick, -1 to 1 each way from its
34736
+ * middle, as input.stick('left') reads it. */
34737
+ kind?: 'panel' | 'button' | 'label' | 'meter' | 'icon' | 'row' | 'column' | 'stick'
34738
+ /** The point of what holds it that it sits at; 'top-left' when left out. */
34739
+ anchor?: UiAnchor
34740
+ /** Design units in from the anchored edges: { anchor: 'top-right', at: [8, 8] } sits 8 in from the top and the
34741
+ * right; from a middle, x moves right and y down. [0, 0] when left out. A row or column places its own. */
34742
+ at?: [number, number]
34743
+ /** [width, height] in design units: needed, but for a row or column. */
34744
+ size?: [number, number]
34745
+ /** The element it sits inside, by name: anchored in its box, or placed by it when it is a row or column. */
34746
+ in?: string
34747
+ /** Design units between what a row or column holds. */
34748
+ gap?: number
34749
+ /** Design units inside a row's or column's edge. */
34750
+ pad?: number
34751
+ /** Where what a row or column holds sits across it; 'center' when left out. */
34752
+ align?: 'start' | 'center' | 'end'
34753
+ /** Never hovered, pressed, focused or clicked; its state reads 'disabled', to draw it greyed. */
34754
+ disabled?: boolean
34755
+ /** false starts it hidden, for ui.show to bring in. */
34756
+ shown?: boolean
34757
+ /** A touch control (the game's own, which the page then draws none of): the button a finger on it holds, as a key
34758
+ * would, read by input.held and input.pressed; a finger on it, or on a stick, is never a click. Declare touch
34759
+ * controls shown: false and ui.show them while game.prefs.touch: a hidden one takes no finger. */
34760
+ touch?: Buttons
34761
+ }
34762
+ /** An element as this screen shows it, in pixels as g.screen draws. */
34763
+ interface UiBox {
34764
+ readonly x: number
34765
+ readonly y: number
34766
+ readonly w: number
34767
+ readonly h: number
34768
+ /** 0 hidden to 1 shown, eased, as ui.show moves it: draw by it for a slide, a fade or a pop. */
34769
+ readonly shown: number
34770
+ readonly state: 'disabled' | 'pressed' | 'hover' | 'focused' | 'idle'
34771
+ /** Screen pixels in one design unit: size what is drawn in the box by it, as 14 * b.scale. */
34772
+ readonly scale: number
34773
+ }
34774
+ /** Text fitted to a box: the size it fits at, its lines, and whether what did not fit was cut. */
34775
+ interface UiFit { readonly size: number; readonly lines: string[]; readonly width: number; readonly height: number; readonly cut: boolean }
34776
+ /** game.ui: the game's UI elements. The engine places them on any screen, kept clear of a phone's notch and corners,
34777
+ * and tracks the pointer, a finger, presses, clicks and focus. It draws nothing: each element looks as the game's draw
34778
+ * draws it at ui.box, so make it look like this game and no other. Declare in start (or in update: the same again
34779
+ * changes nothing), read clicks in update or whilePaused, draw in draw. */
34780
+ interface Ui {
34781
+ /** Declares an element, or changes one; 64 at most. Never in draw. */
34782
+ set(name: string, spec: UiSpec): void
34783
+ /** Takes an element away, with those inside it. */
34784
+ remove(name: string): void
34785
+ /** Eases it in (true) or out (false) over the seconds (0.2 when left out), on the real clock, so a pause menu slides
34786
+ * in while the world holds. A hidden one is never clicked. */
34787
+ show(name: string, shown: boolean, seconds?: number): void
34788
+ /** Gives the keys to a menu: up, down, left and right move focus to the nearest button that way, and jump or action
34789
+ * clicks it. The game still sees those buttons, so hold its own controls while ui.focused is not null; null gives
34790
+ * them back. */
34791
+ focus(name: string | null): void
34792
+ /** The size the elements are written in, 480 by 270 when never set. */
34793
+ design(width: number, height: number): void
34794
+ /** Where it is on this screen and how it stands; null for a name never set. update reads it at the screen played,
34795
+ * draw at the screen drawn. */
34796
+ box(name: string): UiBox | null
34797
+ /** How it stands: idle, hover, pressed, focused or disabled; null for a name not set, as box gives. */
34798
+ state(name: string): UiBox['state'] | null
34799
+ /** Clicked since the last update: pressed and let go on it, by the pointer or a finger, or jump or action on its
34800
+ * focus. Heard once. A press on a button is the UI's: input.pressed('click') never sees it, nor the game's own button
34801
+ * bound to Click. False for a name not set, so a game reading one it removed runs on. */
34802
+ clicked(name: string): boolean
34803
+ /** The largest size, from size (16 when left out) down to min (8), at which the text wrapped to the box fits it; at
34804
+ * min, what still does not fit is cut, ending in an ellipsis. text's fit option does this as it draws. */
34805
+ fit(text: string | number, box: { w: number; h: number }, options?: { size?: number; min?: number; font?: FontName; lineHeight?: number }): UiFit
34806
+ /** The focused button, or null. */
34807
+ readonly focused: string | null
34808
+ /** Screen pixels in one design unit on the screen played. */
34809
+ readonly scale: number
34810
+ /** The names set, in the order they were first set. */
34811
+ readonly names: readonly string[]
34812
+ }
34813
+
34745
34814
  /** On g.screen, a drawing placed from a point of the world, a Thing's middle or a point in tiles: its x and y are then
34746
34815
  * pixels from where that point is on the screen this frame (a health bar over an enemy, a name over a character).
34747
34816
  * edge keeps it inside the screen by that many pixels, as an arrow to something off it does. The world layers take
@@ -34905,10 +34974,6 @@ declare const console: {
34905
34974
  }
34906
34975
  `;
34907
34976
 
34908
- // ../core/dist/script/scan.js
34909
- var import_parser2 = __toESM(require_parser(), 1);
34910
- var import_types2 = __toESM(require_types(), 1);
34911
-
34912
34977
  // ../core/dist/script/shader-wrap.js
34913
34978
  var UNIFORM_NUMBERS = {
34914
34979
  float: 1,
@@ -49517,12 +49582,13 @@ function rayOf(text2) {
49517
49582
  var LOOK_WORDS = "which must be <dx,dy in whole pixels from -4000 to 4000, like <12,-3";
49518
49583
  var RAY_WORDS = "which must be ?ox,oy,oz,dx,dy,dz: its origin in 256ths of a block and its direction in 10,000ths, whole numbers, like ?1280,512,0,0,-7071,7071";
49519
49584
  function onlineKey(input) {
49520
- if (input.online === void 0 && input.onlineState === void 0 && input.purchases === void 0)
49585
+ if (input.online === void 0 && input.onlineState === void 0 && input.purchases === void 0 && input.prefs === void 0)
49521
49586
  return "";
49522
49587
  const json = JSON.stringify({
49523
49588
  ...input.online === void 0 ? {} : { h: input.online },
49524
49589
  ...input.onlineState === void 0 ? {} : { s: input.onlineState },
49525
- ...input.purchases === void 0 ? {} : { p: input.purchases }
49590
+ ...input.purchases === void 0 ? {} : { p: input.purchases },
49591
+ ...input.prefs === void 0 ? {} : { f: input.prefs }
49526
49592
  });
49527
49593
  let text2 = "";
49528
49594
  for (const byte of new TextEncoder().encode(json))
@@ -49538,19 +49604,22 @@ function onlineOf(text2) {
49538
49604
  const read2 = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
49539
49605
  if (typeof read2 !== "object" || read2 === null || Array.isArray(read2))
49540
49606
  return null;
49541
- const { h, s, p } = read2;
49607
+ const { h, s, p, f: f2 } = read2;
49542
49608
  if (h !== void 0 && (!Array.isArray(h) || !h.every(isOnlineHeard)))
49543
49609
  return null;
49544
49610
  if (s !== void 0 && !ONLINE_STATES.includes(s))
49545
49611
  return null;
49546
49612
  if (p !== void 0 && (!Array.isArray(p) || !p.every(isPurchaseHeard)))
49547
49613
  return null;
49548
- if (h === void 0 && s === void 0 && p === void 0)
49614
+ if (f2 !== void 0 && !isPrefs(f2))
49615
+ return null;
49616
+ if (h === void 0 && s === void 0 && p === void 0 && f2 === void 0)
49549
49617
  return null;
49550
49618
  return {
49551
49619
  ...h === void 0 ? {} : { online: h },
49552
49620
  ...s === void 0 ? {} : { onlineState: s },
49553
- ...p === void 0 ? {} : { purchases: p }
49621
+ ...p === void 0 ? {} : { purchases: p },
49622
+ ...f2 === void 0 ? {} : { prefs: f2 }
49554
49623
  };
49555
49624
  } catch {
49556
49625
  return null;
@@ -51209,7 +51278,7 @@ function nextStepOf(tiles, field2, from, size2, diagonal) {
51209
51278
  }
51210
51279
 
51211
51280
  // ../core/dist/script/ui.js
51212
- var UI_KINDS = ["panel", "button", "label", "meter", "icon", "row", "column"];
51281
+ var UI_KINDS = ["panel", "button", "label", "meter", "icon", "row", "column", "stick"];
51213
51282
  var UI_ANCHORS = ["top-left", "top", "top-right", "left", "center", "right", "bottom-left", "bottom", "bottom-right"];
51214
51283
  var MAX_UI_ELEMENTS = 64;
51215
51284
  var MOST_CLICKED = 16;
@@ -51227,7 +51296,7 @@ var NO_UI = {
51227
51296
  clicked: []
51228
51297
  };
51229
51298
  var NAME2 = /^[a-z][a-z0-9_-]{0,39}$/;
51230
- var KEYS = ["kind", "anchor", "at", "size", "in", "gap", "pad", "align", "disabled", "shown"];
51299
+ var KEYS = ["kind", "anchor", "at", "size", "in", "gap", "pad", "align", "disabled", "shown", "touch"];
51231
51300
  var words = (list2) => `${list2.slice(0, -1).map((w) => `'${w}'`).join(", ")} or '${list2[list2.length - 1] ?? ""}'`;
51232
51301
  function uiName(raw, what) {
51233
51302
  if (typeof raw === "string" && NAME2.test(raw))
@@ -51276,6 +51345,14 @@ function uiSpecOf(raw, name) {
51276
51345
  throw new Error(`${label}: disabled is true or false`);
51277
51346
  if (o.shown !== void 0 && typeof o.shown !== "boolean")
51278
51347
  throw new Error(`${label}: shown is true or false`);
51348
+ const touch = o.touch === void 0 || o.touch === null ? null : o.touch;
51349
+ if (touch !== null && (typeof touch !== "string" || !/^[a-z][a-z0-9_]{0,39}$/.test(touch))) {
51350
+ throw new Error(`${label}: touch is the name of a button a finger on it holds, like 'jump' or 'fire', not ${JSON.stringify(touch)}`);
51351
+ }
51352
+ if (touch !== null && kind === "stick")
51353
+ throw new Error(`${label}: a stick moves the stick, player 1's left; it holds no button, so it takes no touch`);
51354
+ if (touch !== null && container)
51355
+ throw new Error(`${label}: a row or a column holds its elements, not a button; give touch to an element inside it`);
51279
51356
  return {
51280
51357
  kind,
51281
51358
  anchor,
@@ -51285,7 +51362,8 @@ function uiSpecOf(raw, name) {
51285
51362
  gap: units(o.gap, `${label}: gap`, 0),
51286
51363
  pad: units(o.pad, `${label}: pad`, 0),
51287
51364
  align,
51288
- disabled: o.disabled === true
51365
+ disabled: o.disabled === true,
51366
+ ...touch === null ? {} : { touch }
51289
51367
  };
51290
51368
  }
51291
51369
  function safeInsets(screen) {
@@ -51427,24 +51505,55 @@ function uiStateOf(ui, name) {
51427
51505
  var sameList = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
51428
51506
  var FOCUS_KEYS = ["left", "right", "up", "down", "jump", "action"];
51429
51507
  var uiFrom = (input) => ({ ...NO_UI, down: input.buttons?.includes("click") === true, keys: FOCUS_KEYS.filter((key) => input[key]) });
51430
- function stepUi(ui, input, real) {
51508
+ function pressable(ui, name, real) {
51509
+ const spec = ui.elements[name];
51510
+ return spec !== void 0 && spec.kind === "button" && !spec.disabled && shownOf2(ui, name, real) > 0;
51511
+ }
51512
+ function boxHolds(boxes, name, p) {
51513
+ const b = boxes.get(name);
51514
+ return b !== void 0 && p.x >= b.x && p.x < b.x + b.w && p.y >= b.y && p.y < b.y + b.h;
51515
+ }
51516
+ function uiButtonAt(ui, point, screen, real, boxes = layoutUi(ui, screen)) {
51517
+ let found = null;
51518
+ for (const name of ui.order)
51519
+ if (pressable(ui, name, real) && boxHolds(boxes, name, point))
51520
+ found = name;
51521
+ return found;
51522
+ }
51523
+ function stickHolds(box, p) {
51524
+ return p.x >= box.x - box.w / 2 && p.x <= box.x + box.w * 1.5 && p.y >= box.y - box.h / 2 && p.y <= box.y + box.h * 1.5;
51525
+ }
51526
+ function touchedOf(ui, input, real) {
51527
+ const fingers = input.touches ?? [];
51528
+ if (fingers.length === 0)
51529
+ return { held: [], stick: null };
51431
51530
  const screen = { width: input.screenSize?.w ?? SCREEN_DEFAULT.w, height: input.screenSize?.h ?? SCREEN_DEFAULT.h };
51432
51531
  const boxes = layoutUi(ui, screen);
51433
- const eligible = (name) => {
51532
+ const held = [];
51533
+ let stick = null;
51534
+ const clamp4 = (n) => Math.max(-1, Math.min(1, Math.round(n * 100) / 100)) + 0;
51535
+ for (const name of ui.order) {
51434
51536
  const spec = ui.elements[name];
51435
- return spec !== void 0 && spec.kind === "button" && !spec.disabled && shownOf2(ui, name, real) > 0;
51436
- };
51437
- const holds2 = (name, p) => {
51438
- const b = boxes.get(name);
51439
- return b !== void 0 && p.x >= b.x && p.x < b.x + b.w && p.y >= b.y && p.y < b.y + b.h;
51440
- };
51441
- const pointer = input.screenPointer ?? input.touches?.[0] ?? null;
51442
- let hover = null;
51443
- if (pointer !== null) {
51444
- for (const name of ui.order)
51445
- if (eligible(name) && holds2(name, pointer))
51446
- hover = name;
51537
+ const box = boxes.get(name);
51538
+ if (spec === void 0 || box === void 0 || spec.disabled || shownOf2(ui, name, real) <= 0)
51539
+ continue;
51540
+ if (spec.touch !== void 0 && !held.includes(spec.touch) && fingers.some((f2) => boxHolds(boxes, name, f2)))
51541
+ held.push(spec.touch);
51542
+ if (spec.kind === "stick" && stick === null) {
51543
+ const finger = fingers.find((f2) => stickHolds(box, f2));
51544
+ if (finger !== void 0)
51545
+ stick = { x: clamp4((finger.x - (box.x + box.w / 2)) / (box.w / 2)), y: clamp4((finger.y - (box.y + box.h / 2)) / (box.h / 2)) };
51546
+ }
51447
51547
  }
51548
+ return { held, stick };
51549
+ }
51550
+ function stepUi(ui, input, real) {
51551
+ const screen = { width: input.screenSize?.w ?? SCREEN_DEFAULT.w, height: input.screenSize?.h ?? SCREEN_DEFAULT.h };
51552
+ const boxes = layoutUi(ui, screen);
51553
+ const eligible = (name) => pressable(ui, name, real);
51554
+ const holds2 = (name, p) => boxHolds(boxes, name, p);
51555
+ const pointer = input.screenPointer ?? input.touches?.[0] ?? null;
51556
+ const hover = pointer === null ? null : uiButtonAt(ui, pointer, screen, real, boxes);
51448
51557
  const held = input.buttons?.includes("click") === true;
51449
51558
  const clicked = [...ui.clicked];
51450
51559
  const click = (name) => {
@@ -51845,7 +51954,7 @@ var OWN_AUDIO = /^own:[a-z0-9][a-z0-9-]{0,39}$/;
51845
51954
  var LIB_AUDIO = /^lib:[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
51846
51955
  var MOST_BURSTS = 64;
51847
51956
  var MOST_DEFERRED = 1024;
51848
- var DRAW_OPS = /* @__PURE__ */ new Set(["thing", "data", "fields", "shown", "find", "findAll", "findIn", "touching", "tileAt", "tileIn", "tileUnder", "raycast", "path", "setting", "load", "random", "log", "drawn", "clock", "running", "measure", "bodyOf", "nextStep", "vars", "shared", "gameScript", "parentOf", "levelSize", "moving", "turns", "mixer", "typing", "t", "language", "languages", "ui", "uiFit", "onlineState", "purchaseProducts", "overlap3", "blockAt", "toScreen"]);
51957
+ var DRAW_OPS = /* @__PURE__ */ new Set(["thing", "data", "fields", "shown", "find", "findAll", "findIn", "touching", "tileAt", "tileIn", "tileUnder", "raycast", "path", "setting", "load", "random", "log", "drawn", "clock", "running", "measure", "bodyOf", "nextStep", "vars", "shared", "gameScript", "parentOf", "levelSize", "moving", "turns", "mixer", "typing", "t", "language", "languages", "ui", "uiFit", "onlineState", "prefs", "purchaseProducts", "overlap3", "blockAt", "toScreen"]);
51849
51958
  function screenOf2(input) {
51850
51959
  const size2 = input.screenSize ?? SCREEN_DEFAULT;
51851
51960
  return { screen: input.screenPointer ?? null, size: { width: size2.w, height: size2.h } };
@@ -51914,7 +52023,7 @@ function fromUnit(raw, unit, what) {
51914
52023
  return fromInt(Math.round(value));
51915
52024
  }
51916
52025
  }
51917
- var BOT_OPS = /* @__PURE__ */ new Set(["thing", "data", "fields", "shown", "find", "findAll", "findIn", "touching", "tileAt", "tileIn", "tileUnder", "raycast", "path", "setting", "load", "random", "log", "clock", "running", "measure", "bodyOf", "nextStep", "vars", "shared", "gameScript", "parentOf", "levelSize", "moving", "turns", "mixer", "typing", "t", "language", "languages", "ui", "uiFit", "onlineState", "purchaseProducts", "overlap3", "blockAt"]);
52026
+ var BOT_OPS = /* @__PURE__ */ new Set(["thing", "data", "fields", "shown", "find", "findAll", "findIn", "touching", "tileAt", "tileIn", "tileUnder", "raycast", "path", "setting", "load", "random", "log", "clock", "running", "measure", "bodyOf", "nextStep", "vars", "shared", "gameScript", "parentOf", "levelSize", "moving", "turns", "mixer", "typing", "t", "language", "languages", "ui", "uiFit", "onlineState", "prefs", "purchaseProducts", "overlap3", "blockAt"]);
51918
52027
  function sidesOf(normal) {
51919
52028
  switch (normal) {
51920
52029
  case "a_above_b":
@@ -52035,6 +52144,9 @@ var Turn = class {
52035
52144
  * game that never mentions online (the online services spec). */
52036
52145
  online;
52037
52146
  onlineState;
52147
+ /** The player's settings as the input last said, and the change of them asked this tick (a game's own pause menu). */
52148
+ prefs;
52149
+ prefsAsk;
52038
52150
  nextCall;
52039
52151
  /** What the scripts asked of the store this tick, and what the game keeps of its purchases, absent for a game that
52040
52152
  * never mentions them (the online services spec, part 3). */
@@ -52106,6 +52218,8 @@ var Turn = class {
52106
52218
  this.online = fresh || scripts.online === void 0 ? void 0 : [...scripts.online];
52107
52219
  this.onlineState = scripts.onlineState;
52108
52220
  this.nextCall = scripts.nextCall;
52221
+ this.prefs = scripts.prefs;
52222
+ this.prefsAsk = fresh ? void 0 : scripts.prefsAsk;
52109
52223
  this.purchase = fresh ? void 0 : scripts.purchase;
52110
52224
  this.purchases = scripts.purchases;
52111
52225
  this.music = scripts.music;
@@ -52261,6 +52375,8 @@ var Turn = class {
52261
52375
  nextCall: this.nextCall,
52262
52376
  purchase: this.purchase,
52263
52377
  purchases: this.purchases,
52378
+ prefs: this.prefs,
52379
+ prefsAsk: this.prefsAsk,
52264
52380
  bursts: this.bursts.length === 0 ? [] : [...this.bursts],
52265
52381
  screen: this.screen,
52266
52382
  waiting: this.waiting,
@@ -52320,6 +52436,8 @@ var Turn = class {
52320
52436
  this.online = was.online;
52321
52437
  this.onlineState = was.onlineState;
52322
52438
  this.nextCall = was.nextCall;
52439
+ this.prefs = was.prefs;
52440
+ this.prefsAsk = was.prefsAsk;
52323
52441
  this.purchase = was.purchase;
52324
52442
  this.purchases = was.purchases;
52325
52443
  this.bursts = was.bursts;
@@ -53661,6 +53779,14 @@ var Turn = class {
53661
53779
  return null;
53662
53780
  case "onlineState":
53663
53781
  return this.onlineState ?? "off";
53782
+ // The player's settings (a game's own pause menu): read as the input last said; a change asked of the page, which
53783
+ // makes it after the tick and says it on a later one.
53784
+ case "prefs":
53785
+ return this.prefs ?? NO_PREFS_YET;
53786
+ case "prefsSet": {
53787
+ this.prefsAsk = { ...this.prefsAsk ?? {}, ...prefsChangeOf(args.change) };
53788
+ return null;
53789
+ }
53664
53790
  // Purchases (the online services spec, part 3): asked of the page, which asks the store; the answer is heard on a
53665
53791
  // later tick, as input.
53666
53792
  case "purchaseBuy":
@@ -54352,6 +54478,17 @@ var Turn = class {
54352
54478
  throw new Error(TOO_MANY_ASKS);
54353
54479
  this.online = [...asked, ask];
54354
54480
  }
54481
+ /** The player's settings as the page said them this tick, for a game that keeps them. A tick with a finger on the
54482
+ * screen that says nothing of them says a touch screen, as the page does on that tick: a playtest's track, which no
54483
+ * page plays, shows the game's touch controls to its look. */
54484
+ takePrefsInput() {
54485
+ if (this.prefs === void 0)
54486
+ return;
54487
+ if (this.input.prefs !== void 0)
54488
+ this.prefs = this.input.prefs;
54489
+ else if (!this.prefs.touch && (this.input.touches?.length ?? 0) > 0)
54490
+ this.prefs = { ...this.prefs, touch: true };
54491
+ }
54355
54492
  /** What the page brought back from the game's server, and whether it is online, for a game that keeps online: each
54356
54493
  * heard as an 'online' event. A game that never mentions online ignores it. */
54357
54494
  takeOnlineInput() {
@@ -54409,7 +54546,7 @@ var Turn = class {
54409
54546
  this.events.push(name);
54410
54547
  }
54411
54548
  scriptsState() {
54412
- const { starting: _was, loops: _loops, mixer: _mixer, typing: _typing, ui: _ui, privacy: _privacy, ad: _ad, ads: _ads, online: _online, onlineState: _onlineState, nextCall: _nextCall, purchase: _purchase, purchases: _purchases, ...before2 } = this.scripts;
54549
+ const { starting: _was, loops: _loops, mixer: _mixer, typing: _typing, ui: _ui, privacy: _privacy, ad: _ad, ads: _ads, online: _online, onlineState: _onlineState, nextCall: _nextCall, purchase: _purchase, purchases: _purchases, prefs: _prefs, prefsAsk: _prefsAsk, ...before2 } = this.scripts;
54413
54550
  return {
54414
54551
  ...before2,
54415
54552
  fields: this.fields,
@@ -54441,6 +54578,8 @@ var Turn = class {
54441
54578
  ...this.online === void 0 || this.online.length === 0 ? {} : { online: this.online },
54442
54579
  ...this.onlineState === void 0 ? {} : { onlineState: this.onlineState },
54443
54580
  ...this.nextCall === void 0 ? {} : { nextCall: this.nextCall },
54581
+ ...this.prefs === void 0 ? {} : { prefs: this.prefs },
54582
+ ...this.prefsAsk === void 0 ? {} : { prefsAsk: this.prefsAsk },
54444
54583
  ...this.purchase === void 0 ? {} : { purchase: this.purchase },
54445
54584
  ...this.purchases === void 0 ? {} : { purchases: this.purchases },
54446
54585
  music: this.music,
@@ -54483,12 +54622,33 @@ function uiTick(state, input) {
54483
54622
  return { state, input };
54484
54623
  const stepped = stepUi(ui, input, scripts.clock.real);
54485
54624
  const next = stepped.ui === ui ? state : { ...state, scripts: { ...scripts, ui: stepped.ui } };
54486
- if (!stepped.swallow || input.buttons?.includes("click") !== true)
54487
- return { state: next, input };
54488
- const { buttons, ...rest } = input;
54625
+ const touched = withTouched(input, touchedOf(stepped.ui, input, scripts.clock.real));
54626
+ if (!stepped.swallow || touched.buttons?.includes("click") !== true)
54627
+ return { state: next, input: touched };
54628
+ const { buttons, ...rest } = touched;
54489
54629
  const kept = (buttons ?? []).filter((button) => button !== "click");
54490
54630
  return { state: next, input: kept.length === 0 ? rest : { ...rest, buttons: kept } };
54491
54631
  }
54632
+ var SIX2 = /* @__PURE__ */ new Set(["left", "right", "up", "down", "jump", "action"]);
54633
+ function withTouched(input, touched) {
54634
+ if (touched.held.length === 0 && touched.stick === null)
54635
+ return input;
54636
+ let out = input;
54637
+ const own2 = new Set(input.buttons ?? []);
54638
+ for (const button of touched.held) {
54639
+ if (SIX2.has(button))
54640
+ out = { ...out, [button]: true };
54641
+ else
54642
+ own2.add(button);
54643
+ }
54644
+ if (own2.size !== (input.buttons ?? []).length)
54645
+ out = { ...out, buttons: [...own2].sort() };
54646
+ if (touched.stick !== null) {
54647
+ const [first, ...rest] = input.sticks ?? [];
54648
+ out = { ...out, sticks: [{ rx: first?.rx ?? 0, ry: first?.ry ?? 0, lx: Math.round(touched.stick.x * 100) + 0, ly: Math.round(touched.stick.y * 100) + 0 }, ...rest] };
54649
+ }
54650
+ return out;
54651
+ }
54492
54652
  function heardClicks(turn) {
54493
54653
  if (turn.ui !== void 0 && turn.ui.clicked.length > 0)
54494
54654
  turn.ui = { ...turn.ui, clicked: [] };
@@ -54514,6 +54674,7 @@ function beginScripts(state, input) {
54514
54674
  function beginTurn(turn, scripts) {
54515
54675
  turn.takeAdInput();
54516
54676
  turn.takeOnlineInput();
54677
+ turn.takePrefsInput();
54517
54678
  turn.takePurchaseInput();
54518
54679
  turn.bursts = turn.bursts.filter((b) => turn.now - b.step < b.life * 60);
54519
54680
  dueChange(turn);
@@ -54625,6 +54786,7 @@ function pausedScripts(state, input) {
54625
54786
  function pausedTurn(turn, scripts) {
54626
54787
  turn.takeAdInput();
54627
54788
  turn.takeOnlineInput();
54789
+ turn.takePrefsInput();
54628
54790
  turn.takePurchaseInput();
54629
54791
  dueChange(turn);
54630
54792
  turn.call(null, "whilePaused", [DT]);
@@ -57901,7 +58063,7 @@ function listGames(args = {}, fallback = LIBRARY_ROOT) {
57901
58063
  // ../core/dist/surface/tools/inspect.js
57902
58064
  function compile(args) {
57903
58065
  try {
57904
- const compiled = load(parse(args.source));
58066
+ const compiled = load(parse2(args.source));
57905
58067
  const first = compiled.levels[0];
57906
58068
  if (first === void 0)
57907
58069
  return fail("this spec declares no levels");
@@ -57914,7 +58076,7 @@ var isResult = (v) => typeof v === "object" && v !== null && "content" in v;
57914
58076
  function validate(args) {
57915
58077
  let ast;
57916
58078
  try {
57917
- ast = parse(args.source);
58079
+ ast = parse2(args.source);
57918
58080
  } catch (e) {
57919
58081
  const err = e;
57920
58082
  return ok({