gamesteel 0.4.1 → 0.5.2

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.9.1";
24849
+ var ENGINE_VERSION = "0.9.2";
24850
24850
 
24851
24851
  // ../core/dist/home.js
24852
24852
  import { existsSync, renameSync, symlinkSync } from "node:fs";
@@ -28475,6 +28475,170 @@ function impulseFor(height, gravity, terminalFall) {
28475
28475
  return impulseUpTo(height, gravity, terminalFall);
28476
28476
  }
28477
28477
 
28478
+ // ../core/dist/script/ads.js
28479
+ var AD_KINDS = ["break", "reward"];
28480
+ var NO_ADS_YET = { pending: null, lastBreak: null, lastPress: null, lastPlay: null, played: false, scene: null, ready: AD_KINDS, banner: null };
28481
+ var PLACEMENT = /^[a-z0-9][a-z0-9-]{0,39}$/;
28482
+ var PLACEMENT_WORDS = "a placement is named with 1 to 40 lowercase letters, digits and hyphens, like level-end or extra-life";
28483
+ var adsOf = (kept) => kept ?? NO_ADS_YET;
28484
+ var usesAds = (sources) => sources.some((source) => /\bads\s*\.\s*(break|reward|ready|banner)\b/.test(source));
28485
+ var BREAK_GAP_TICKS = 3600;
28486
+ var REWARD_PRESS_TICKS = 60;
28487
+ var PLAY_PRESS_TICKS = 30;
28488
+ var SCENE_SETTLE_TICKS = 120;
28489
+ var BANNER_EDGE_PX = 8;
28490
+ var AD_WORDS = {
28491
+ launch: "a break before the player's first input is refused: AdMob forbids ads at launch",
28492
+ backToBack: "a break less than 60 seconds after the last one is refused: AdMob forbids ads back to back",
28493
+ unchosen: "a reward the player did not choose is refused: AdMob rewards only an ad the player asks for, with a press just before",
28494
+ showing: "an ad is already showing",
28495
+ children: "this game is made for children: it shows no ads",
28496
+ midPlay: "a break while the player is in the middle of play: AdMob and Google Play forbid surprise ads during play; put it at a natural pause",
28497
+ banner: (name) => `a banner beside the game's button '${name}': AdMob forbids ads where players tap; keep buttons away from the banner's edge`
28498
+ };
28499
+ function adRefusal(ads, kind, now, off) {
28500
+ if (off !== null)
28501
+ return off;
28502
+ if (ads.pending !== null)
28503
+ return AD_WORDS.showing;
28504
+ if (kind === "break") {
28505
+ if (!ads.played)
28506
+ return AD_WORDS.launch;
28507
+ if (ads.lastBreak !== null && now - ads.lastBreak < BREAK_GAP_TICKS)
28508
+ return AD_WORDS.backToBack;
28509
+ return null;
28510
+ }
28511
+ return ads.lastPress !== null && now - ads.lastPress <= REWARD_PRESS_TICKS ? null : AD_WORDS.unchosen;
28512
+ }
28513
+ var SIX = ["left", "right", "up", "down", "jump", "action"];
28514
+ var playButtons = (input) => (input.buttons ?? []).filter((name) => name !== "click" && !name.startsWith("click:"));
28515
+ var playHeld = (input) => SIX.some((button) => input[button]) || playButtons(input).length > 0;
28516
+ var playPressed = (input, before2) => SIX.some((button) => input[button] && !before2[button]) || playButtons(input).some((name) => !playButtons(before2).includes(name));
28517
+ var anyPressed = (input, before2) => playPressed(input, before2) || (input.buttons ?? []).some((name) => !(before2.buttons ?? []).includes(name)) || (input.touches?.length ?? 0) > (before2.touches?.length ?? 0) || input.typed !== void 0 && input.typed !== "";
28518
+
28519
+ // ../core/dist/script/online.js
28520
+ var ONLINE_STATES = ["online", "resting", "off"];
28521
+ var PUSH_RESULTS = ["granted", "denied", "unavailable"];
28522
+ var CALL_RESULTS = ["ok", "error", "busy", "resting", "refused"];
28523
+ var ONLINE_LIMITS = {
28524
+ asksPerTick: 32,
28525
+ dataBytes: 8192,
28526
+ callName: /^[a-zA-Z][a-zA-Z0-9_]{0,39}$/,
28527
+ room: /^[a-z0-9][a-z0-9-]{0,39}$/,
28528
+ /** A file a game's scripts upload, as JSON; the screen's picture is the page's, capped by the server's size. */
28529
+ fileBytes: 1e6,
28530
+ /** What a download gives a game, as JSON. */
28531
+ downloadBytes: 1e6
28532
+ };
28533
+ var TOO_MANY_ASKS = `a tick asks the game's server at most ${String(ONLINE_LIMITS.asksPerTick)} times: spread the calls over ticks`;
28534
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
28535
+ var jsonBytes = (data) => {
28536
+ try {
28537
+ const text2 = JSON.stringify(data);
28538
+ return text2 === void 0 ? null : new TextEncoder().encode(text2).length;
28539
+ } catch {
28540
+ return null;
28541
+ }
28542
+ };
28543
+ var dataProblem = (data) => {
28544
+ const bytes = jsonBytes(data === void 0 ? null : data);
28545
+ if (bytes === null)
28546
+ return "online data is plain data: numbers, strings, booleans, null, lists and objects of them";
28547
+ return bytes > ONLINE_LIMITS.dataBytes ? `online data is at most 8,192 bytes as JSON, and this is ${String(bytes)}` : null;
28548
+ };
28549
+ var nameProblem = (name) => typeof name === "string" && ONLINE_LIMITS.callName.test(name) ? null : `a call's name is a letter, then up to 39 letters, digits and _, like postScore, not ${JSON.stringify(name)}`;
28550
+ var uploadProblem = (ask) => {
28551
+ const file = "file" in ask && ask["file"] !== void 0;
28552
+ if (file === (ask["screen"] === true))
28553
+ return "an upload is a file or the screen: give file data, or screen: true, not both";
28554
+ if (!file)
28555
+ return null;
28556
+ const bytes = jsonBytes(ask["file"]);
28557
+ if (bytes === null)
28558
+ return "a file is plain data: numbers, strings, booleans, null, lists and objects of them";
28559
+ return bytes > ONLINE_LIMITS.fileBytes ? `a file from a game's scripts is at most 1,000,000 bytes as JSON, and this is ${String(bytes)}` : null;
28560
+ };
28561
+ function askProblem(ask) {
28562
+ const kinds = "an online ask is a call, join, leave, send, upload, download or askPush";
28563
+ if (!isObject(ask))
28564
+ return kinds;
28565
+ switch (ask["kind"]) {
28566
+ case "call":
28567
+ return nameProblem(ask["name"]) ?? dataProblem(ask["data"]);
28568
+ case "upload":
28569
+ return nameProblem(ask["name"]) ?? dataProblem(ask["data"]) ?? uploadProblem(ask);
28570
+ case "download":
28571
+ return typeof ask["url"] === "string" && ask["url"].length <= 4096 && /^(https:\/\/|local:)/.test(ask["url"]) ? null : "a download's address is one the game's server gave, starting https://";
28572
+ case "askPush":
28573
+ return null;
28574
+ case "join":
28575
+ case "leave":
28576
+ case "send":
28577
+ if (typeof ask["room"] !== "string" || !ONLINE_LIMITS.room.test(ask["room"])) {
28578
+ return `a room's name is 1 to 40 lowercase letters, digits and hyphens, like lobby-1, not ${JSON.stringify(ask["room"])}`;
28579
+ }
28580
+ return ask["kind"] === "send" ? dataProblem(ask["data"]) : null;
28581
+ default:
28582
+ return kinds;
28583
+ }
28584
+ }
28585
+ var usesOnline = (sources) => sources.some((source) => /\bonline\s*\.\s*(call|join|leave|send|state|upload|uploadScreen|download|askPush)\b/.test(source));
28586
+ function isOnlineHeard(value) {
28587
+ if (!isObject(value))
28588
+ return false;
28589
+ switch (value["kind"]) {
28590
+ case "answer":
28591
+ return typeof value["id"] === "number" && Number.isInteger(value["id"]) && typeof value["name"] === "string" && CALL_RESULTS.includes(value["result"]) && (value["reason"] === void 0 || typeof value["reason"] === "string");
28592
+ case "message":
28593
+ return typeof value["room"] === "string" && value["data"] !== void 0;
28594
+ case "joined":
28595
+ case "left":
28596
+ case "full":
28597
+ return typeof value["room"] === "string";
28598
+ case "upload":
28599
+ return typeof value["id"] === "number" && Number.isInteger(value["id"]) && typeof value["name"] === "string" && CALL_RESULTS.includes(value["result"]) && (value["reason"] === void 0 || typeof value["reason"] === "string");
28600
+ case "download":
28601
+ return typeof value["id"] === "number" && Number.isInteger(value["id"]) && CALL_RESULTS.includes(value["result"]) && (value["reason"] === void 0 || typeof value["reason"] === "string");
28602
+ case "push":
28603
+ return PUSH_RESULTS.includes(value["result"]);
28604
+ case "pushOpened":
28605
+ return value["data"] !== void 0;
28606
+ default:
28607
+ return false;
28608
+ }
28609
+ }
28610
+
28611
+ // ../core/dist/script/purchases.js
28612
+ var PURCHASE_RESULTS = ["granted", "pending", "cancelled", "unavailable", "refused"];
28613
+ var PRODUCT_ID = /^[a-z0-9][a-z0-9_.]{0,39}$/;
28614
+ var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
28615
+ function purchaseAskProblem(ask) {
28616
+ if (!isObject2(ask))
28617
+ return "a purchase ask is a buy of a product or a restore";
28618
+ if (ask["kind"] === "restore")
28619
+ return null;
28620
+ if (ask["kind"] !== "buy")
28621
+ return "a purchase ask is a buy of a product or a restore";
28622
+ const id = ask["id"];
28623
+ 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)}`;
28624
+ }
28625
+ var isProduct = (value) => isObject2(value) && typeof value["id"] === "string" && typeof value["price"] === "string" && typeof value["name"] === "string";
28626
+ function isPurchaseHeard(value) {
28627
+ if (!isObject2(value))
28628
+ return false;
28629
+ switch (value["kind"]) {
28630
+ case "products":
28631
+ return Array.isArray(value["list"]) && value["list"].every(isProduct);
28632
+ case "bought":
28633
+ return typeof value["id"] === "string" && PURCHASE_RESULTS.includes(value["result"]) && (value["reason"] === void 0 || typeof value["reason"] === "string") && (value["test"] === void 0 || typeof value["test"] === "boolean");
28634
+ case "restored":
28635
+ return Array.isArray(value["ids"]) && value["ids"].every((id) => typeof id === "string");
28636
+ default:
28637
+ return false;
28638
+ }
28639
+ }
28640
+ var usesPurchases = (sources) => sources.some((source) => /\bpurchases\s*\.\s*(buy|restore|products)\b/.test(source));
28641
+
28478
28642
  // ../core/dist/script/runtime.js
28479
28643
  var import_sucrase = __toESM(require_dist2(), 1);
28480
28644
 
@@ -29378,6 +29542,33 @@ const __soundHow = (options, label) => {
29378
29542
  }
29379
29543
  return out
29380
29544
  }
29545
+ /** Online (the online services spec): asked of the game's server through the page; what comes back is heard on a later
29546
+ * tick as an 'online' event. */
29547
+ const __online = Object.freeze({
29548
+ call(name, data) { return __call('onlineCall', { name, data: data === undefined ? null : data }) },
29549
+ join(room) { __call('onlineJoin', { room }) },
29550
+ leave(room) { __call('onlineLeave', { room }) },
29551
+ send(room, data) { __call('onlineSend', { room, data: data === undefined ? null : data }) },
29552
+ upload(name, file, data) { return __call('onlineUpload', { name, file: file === undefined ? null : file, data: data === undefined ? null : data }) },
29553
+ uploadScreen(name, data) { return __call('onlineUpload', { name, screen: true, data: data === undefined ? null : data }) },
29554
+ download(url) { return __call('onlineDownload', { url }) },
29555
+ askPush() { __call('onlineAskPush', {}) },
29556
+ get state() { return __call('onlineState', {}) },
29557
+ })
29558
+ /** Purchases (the online services spec, part 3): asked of the page, which asks the store; answered on a later tick as a
29559
+ * 'purchase' event. */
29560
+ const __purchases = Object.freeze({
29561
+ get products() { return __frozen(__revived(__call('purchaseProducts', {}))) },
29562
+ buy(id) { __call('purchaseBuy', { id }) },
29563
+ restore() { __call('purchaseRestore', {}) },
29564
+ })
29565
+ /** Ads (the ads spec): asked of the page, answered on a later tick as an 'ad' event. */
29566
+ const __ads = Object.freeze({
29567
+ break(placement) { __call('adBreak', { placement }) },
29568
+ reward(placement) { __call('adReward', { placement }) },
29569
+ ready(kind) { return __call('adReady', { kind }) === true },
29570
+ banner(position) { __call('adBanner', { position: position === undefined ? null : position }) },
29571
+ })
29381
29572
  const __sound = {
29382
29573
  play(name, options) { __call('sound', Object.assign({ name }, __soundHow(options, 'sound.play'))) },
29383
29574
  loop(name, options) { return __call('soundLoop', Object.assign({ name }, __soundHow(options, 'sound.loop'))) },
@@ -29630,6 +29821,9 @@ const __game = {
29630
29821
  get vars() { return __vars },
29631
29822
  get turns() { return __turns },
29632
29823
  get ui() { return __ui },
29824
+ get ads() { return __ads },
29825
+ get online() { return __online },
29826
+ get purchases() { return __purchases },
29633
29827
  /** The game script's own fields, read only; null without one. */
29634
29828
  get script() { return __frozen(__revived(__call('gameScript', null))) },
29635
29829
  get input() { return __input },
@@ -29691,6 +29885,7 @@ const __game = {
29691
29885
  restart(options) { __call('restart', { fade: __num(__opt(options).fade, null) }) },
29692
29886
  flash(colour, seconds) { __call('screen', { flash: { color: __color(colour, 0xffffff), seconds: seconds === undefined ? 0.2 : seconds } }) },
29693
29887
  fade(alpha, seconds, colour) { __call('screen', { fade: { alpha, seconds, color: __color(colour, 0x000000) } }) },
29888
+ openPrivacy() { __call('openPrivacy', null) },
29694
29889
  save(key, value) { __call('save', { key, value: value === undefined ? null : value }) },
29695
29890
  load(key) { return __call('load', { key }) },
29696
29891
  get camera() { return __camera },
@@ -30130,7 +30325,7 @@ var FAILURES_BEFORE_PAUSE = 10;
30130
30325
  var MEMORY_LIMIT = 32 * 1024 * 1024;
30131
30326
  var INTERRUPTS_PER_CALL = 1e3;
30132
30327
  var INTERRUPTS_PER_PHASE = 2e3;
30133
- var STACK_BYTES = 256 * 1024;
30328
+ var STACK_BYTES = 128 * 1024;
30134
30329
  var GAME_KEY = "__game";
30135
30330
  var PRELUDE = `'use strict'
30136
30331
  ;(function () {
@@ -30821,7 +31016,8 @@ function build(sources, skipped) {
30821
31016
  return { fields: fieldsOf(text2), error: null, ...wantReturn ? { returned } : {} };
30822
31017
  } catch (error) {
30823
31018
  healthy = false;
30824
- return { ...failed(`the script broke the interpreter and was stopped: ${error instanceof Error ? error.message : String(error)}`), runaway: true };
31019
+ const hostStack = error instanceof RangeError && /call stack/i.test(error.message);
31020
+ return { ...failed(hostStack ? messageOf({ message: "stack overflow" }) : `the script broke the interpreter and was stopped: ${error instanceof Error ? error.message : String(error)}`), runaway: true };
30825
31021
  } finally {
30826
31022
  for (const handle of handles)
30827
31023
  handle.dispose();
@@ -30893,6 +31089,7 @@ function mixAt(level, real) {
30893
31089
  return level.from;
30894
31090
  return level.from + (level.to - level.from) * (real - level.start) / level.ticks;
30895
31091
  }
31092
+ var NO_PURCHASES_YET = { products: [], pending: null };
30896
31093
  var START_CLOCK = { real: 0, rate: 60, owed: 0, paused: false, stopUntil: 0, latched: [] };
30897
31094
  var ticking = /* @__PURE__ */ new Map();
30898
31095
  var drawing = /* @__PURE__ */ new Map();
@@ -30921,7 +31118,7 @@ function runtimeOf(sources, draw = false) {
30921
31118
  function hasScripts(scripts) {
30922
31119
  return scripts.sources.game !== null || Object.keys(scripts.sources.kinds).length > 0;
30923
31120
  }
30924
- function initialScripts(given, kinds, seed = 1, saved = {}, audio, fonts) {
31121
+ function initialScripts(given, kinds, seed = 1, saved = {}, audio, fonts, adsOff) {
30925
31122
  const sources = {
30926
31123
  kinds: { ...given.kinds },
30927
31124
  game: given.game,
@@ -30967,7 +31164,14 @@ function initialScripts(given, kinds, seed = 1, saved = {}, audio, fonts) {
30967
31164
  sequences: [],
30968
31165
  clock: START_CLOCK,
30969
31166
  ...audio === void 0 ? {} : { audio },
30970
- ...fonts === void 0 ? {} : { fonts }
31167
+ ...fonts === void 0 ? {} : { fonts },
31168
+ // A game whose scripts mention ads keeps them from the first tick, its presses known before it first asks.
31169
+ ...usesAds([...Object.values(sources.kinds), sources.game ?? "", ...Object.values(sources.modules ?? {})]) ? { ads: NO_ADS_YET } : {},
31170
+ ...adsOff === void 0 ? {} : { adsOff },
31171
+ // A game whose scripts mention online keeps its state and call count from the first tick; any other never has them.
31172
+ ...usesOnline([...Object.values(sources.kinds), sources.game ?? "", ...Object.values(sources.modules ?? {})]) ? { onlineState: "off", nextCall: 1 } : {},
31173
+ // A game whose scripts mention purchases keeps its products from the first tick; any other never has them.
31174
+ ...usesPurchases([...Object.values(sources.kinds), sources.game ?? "", ...Object.values(sources.modules ?? {})]) ? { purchases: NO_PURCHASES_YET } : {}
30971
31175
  };
30972
31176
  }
30973
31177
 
@@ -31276,7 +31480,7 @@ function numericProps(props) {
31276
31480
  }
31277
31481
  function load(ast, options = {}) {
31278
31482
  const palette2 = paletteOf(ast);
31279
- const scripts = options.scripts === void 0 ? null : initialScripts(options.scripts, new Set(ast.entities.map((e) => e.name)), options.seed, options.saved, options.audio, options.fonts);
31483
+ const scripts = options.scripts === void 0 ? null : initialScripts(options.scripts, new Set(ast.entities.map((e) => e.name)), options.seed, options.saved, options.audio, options.fonts, options.adsOff);
31280
31484
  const defs = [];
31281
31485
  const defIndexOf = /* @__PURE__ */ new Map();
31282
31486
  const kindDefs = /* @__PURE__ */ new Map();
@@ -31780,6 +31984,71 @@ interface Mixer {
31780
31984
  }
31781
31985
 
31782
31986
  /** game.turns: whose turn it is, in an order of objects. */
31987
+ /** Ads, for a game in Google Play and the App Store, from its maker's own AdMob account (the guide's publish topic).
31988
+ * The page answers a break or a reward on a later tick, and every script hears it as onEvent('ad', { kind, placement,
31989
+ * result, reason? }): result is 'shown', 'rewarded', 'closed', 'unavailable' or 'refused'. Give a reward only on
31990
+ * 'rewarded'. In the editor an ad is a placeholder card; in browser play every ad is 'unavailable'. */
31991
+ interface Ads {
31992
+ /** A full-screen break at a natural pause, like a level's end: placement names it, like 'level-end'. Refused before
31993
+ * the player's first input, and less than 60 seconds after the last break. */
31994
+ break(placement: string): void
31995
+ /** An ad the player chose to watch for a reward, like 'extra-life': ask it right after the player's press on the
31996
+ * game's own offer. Refused with no press in the last second. */
31997
+ reward(placement: string): void
31998
+ /** Whether an ad of this kind can play now: show a reward's offer only while it is. Never in a game made for children. */
31999
+ ready(kind: 'break' | 'reward'): boolean
32000
+ /** A banner at the top or bottom of the screen, which the game's view makes room for; null hides it. */
32001
+ banner(position: 'top' | 'bottom' | null): void
32002
+ }
32003
+
32004
+ /** Real-money purchases in the game's store app (the guide's purchases topic): the products purchases.json declares,
32005
+ * bought through Apple's and Google's own in-app purchases. Each answer reaches every script on a later tick as
32006
+ * onEvent('purchase', heard), where heard is { kind: 'products', list }, { kind: 'bought', id, result, data?, reason?,
32007
+ * test? } or { kind: 'restored', ids }. Give what was bought only on result 'granted' (data is what the game's server
32008
+ * answered from onPurchase); 'pending' is not paid yet, and its answer comes again once it is. In a browser and a web
32009
+ * export, every buy is 'unavailable'. */
32010
+ interface Purchases {
32011
+ /** The products as the store shows them to this player: the price in their money, the name in their language; empty
32012
+ * until the store answers, and in a browser. */
32013
+ readonly products: readonly { readonly id: string; readonly price: string; readonly name: string }[]
32014
+ /** Buys a product by its id, like 'coins_100': the store's own sheet asks the player. One at a time: a second buy
32015
+ * while one waits for its answer is heard 'refused'. */
32016
+ buy(id: string): void
32017
+ /** Asks the store for the unlocks and subscriptions this player owns, heard as { kind: 'restored', ids }: Apple asks for
32018
+ * a Restore purchases button wherever unlocks are sold. */
32019
+ restore(): void
32020
+ }
32021
+
32022
+ /** The game's own server, for a game whose Online tab turned it on (the guide's online topic). Each ask goes to the
32023
+ * server through the page; what comes back reaches every script on a later tick as onEvent('online', heard), where
32024
+ * heard is { kind: 'answer', id, name, result, data?, reason? } (result 'ok', 'error', 'busy', 'resting' or 'refused'),
32025
+ * { kind: 'message', room, data }, or { kind: 'joined' | 'left' | 'full', room }. A tick never waits on the network. */
32026
+ interface Online {
32027
+ /** Asks the server's call by name, like 'postScore', with plain data (at most 8,192 bytes as JSON); answers the call's
32028
+ * number, which its answer carries. */
32029
+ call(name: string, data?: Plain): number
32030
+ /** Joins a live room by name (lowercase letters, digits and hyphens, like 'lobby-1'); heard as 'joined', or 'full'. */
32031
+ join(room: string): void
32032
+ leave(room: string): void
32033
+ /** Sends plain data to a room's code, which decides what everyone hears. */
32034
+ send(room: string, data: Plain): void
32035
+ /** Uploads a file (plain data, at most 1,000,000 bytes as JSON) to the game's storage through its server's call by name,
32036
+ * which gets { size, type, data } and answers what ctx.files.uploadUrl gives; heard as { kind: 'upload', id, name,
32037
+ * result, reason? }. Answers the upload's number. */
32038
+ upload(call: string, file: Plain, data?: Plain): number
32039
+ /** Uploads a picture of the game's screen (a PNG) the same way. */
32040
+ uploadScreen(call: string, data?: Plain): number
32041
+ /** Reads a file at an address the game's server gave (ctx.files.downloadUrl): heard as { kind: 'download', id, result,
32042
+ * data?, reason? }, its data the file's JSON (at most 1,000,000 bytes). Answers the download's number. */
32043
+ download(url: string): number
32044
+ /** Asks the player whether the game may send them notifications, where it can (a phone app with push on): heard as
32045
+ * { kind: 'push', result: 'granted' | 'denied' | 'unavailable' }. Never required to play, never rewarded. A
32046
+ * notification the player tapped to open the game is heard as { kind: 'pushOpened', data }. */
32047
+ askPush(): void
32048
+ /** 'online', 'resting' (the server is off or paused, or its wallet is empty) or 'off' (no connection yet). */
32049
+ readonly state: 'online' | 'resting' | 'off'
32050
+ }
32051
+
31783
32052
  interface Turns {
31784
32053
  /** Begins an order of these objects: the first's turn, round 1. */
31785
32054
  begin(things: Thing[]): void
@@ -32153,6 +32422,15 @@ interface Game {
32153
32422
  /** The game's UI elements: placed, hovered, pressed and clicked by the engine, drawn by the game. Kept through a goTo,
32154
32423
  * gone with a restart. */
32155
32424
  readonly ui: Ui
32425
+ /** Breaks, rewards and banners from the game's maker's own AdMob account. */
32426
+ readonly ads: Ads
32427
+ /** The game's own server: calls and live rooms. */
32428
+ readonly online: Online
32429
+ /** Real-money purchases in the game's store app. */
32430
+ readonly purchases: Purchases
32431
+ /** Opens the game's privacy policy, as a game on the app stores must from inside itself: call it when the player
32432
+ * taps the game's own Privacy button. The page knows where the policy is; with none yet, nothing opens. */
32433
+ openPrivacy(): void
32156
32434
  /** Kept across restarts and levels, and between plays in the Game tab (in that
32157
32435
  * browser). A playtest starts with nothing saved. */
32158
32436
  save(key: string, value: Plain): void
@@ -46900,9 +47178,68 @@ function keyOf2(input) {
46900
47178
  const wheel = input.wheel === void 0 || input.wheel === 0 ? "" : `~${String(input.wheel)}`;
46901
47179
  const sticks = input.sticks === void 0 || input.sticks.length === 0 ? "" : `&${input.sticks.map((s) => `${String(s.lx)},${String(s.ly)},${String(s.rx)},${String(s.ry)}`).join("/")}`;
46902
47180
  const touches = input.touches === void 0 || input.touches.length === 0 ? "" : `!${input.touches.map((t) => `${String(t.x)},${String(t.y)}`).join(";")}`;
47181
+ const ready = readyKey(input.adsReady);
47182
+ const answer = input.adAnswer === void 0 ? "" : `$${ANSWER_LETTERS[input.adAnswer]}`;
47183
+ const online = onlineKey(input);
46903
47184
  const language = input.language === void 0 ? "" : `=${input.language}`;
46904
47185
  const typed = input.typed === void 0 || input.typed === "" ? "" : `"${escapeTyped(input.typed)}"`;
46905
- return `${letters}${own2}${aim}${screen}${size2}${wheel}${sticks}${touches}${language}${typed}`;
47186
+ return `${letters}${own2}${aim}${screen}${size2}${wheel}${sticks}${touches}${ready}${answer}${online}${language}${typed}`;
47187
+ }
47188
+ function onlineKey(input) {
47189
+ if (input.online === void 0 && input.onlineState === void 0 && input.purchases === void 0)
47190
+ return "";
47191
+ const json = JSON.stringify({
47192
+ ...input.online === void 0 ? {} : { h: input.online },
47193
+ ...input.onlineState === void 0 ? {} : { s: input.onlineState },
47194
+ ...input.purchases === void 0 ? {} : { p: input.purchases }
47195
+ });
47196
+ let text2 = "";
47197
+ for (const byte of new TextEncoder().encode(json))
47198
+ text2 += String.fromCharCode(byte);
47199
+ return `>${btoa(text2).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
47200
+ }
47201
+ function onlineOf(text2) {
47202
+ if (!/^[A-Za-z0-9_-]+$/.test(text2))
47203
+ return null;
47204
+ try {
47205
+ const padded = text2.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - text2.length % 4) % 4);
47206
+ const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
47207
+ const read2 = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
47208
+ if (typeof read2 !== "object" || read2 === null || Array.isArray(read2))
47209
+ return null;
47210
+ const { h, s, p } = read2;
47211
+ if (h !== void 0 && (!Array.isArray(h) || !h.every(isOnlineHeard)))
47212
+ return null;
47213
+ if (s !== void 0 && !ONLINE_STATES.includes(s))
47214
+ return null;
47215
+ if (p !== void 0 && (!Array.isArray(p) || !p.every(isPurchaseHeard)))
47216
+ return null;
47217
+ if (h === void 0 && s === void 0 && p === void 0)
47218
+ return null;
47219
+ return {
47220
+ ...h === void 0 ? {} : { online: h },
47221
+ ...s === void 0 ? {} : { onlineState: s },
47222
+ ...p === void 0 ? {} : { purchases: p }
47223
+ };
47224
+ } catch {
47225
+ return null;
47226
+ }
47227
+ }
47228
+ var ANSWER_LETTERS = { shown: "s", rewarded: "r", closed: "c", unavailable: "u" };
47229
+ var ANSWER_OF = { s: "shown", r: "rewarded", c: "closed", u: "unavailable" };
47230
+ var READY_LETTERS = [["b", "break"], ["w", "reward"]];
47231
+ function readyKey(ready) {
47232
+ if (ready === void 0)
47233
+ return "";
47234
+ return `#${READY_LETTERS.filter(([, kind]) => ready.includes(kind)).map(([letter]) => letter).join("")}`;
47235
+ }
47236
+ function answerOf(text2) {
47237
+ return text2.length === 1 ? ANSWER_OF[text2] ?? null : null;
47238
+ }
47239
+ function readyOf(text2) {
47240
+ if (![...text2].every((ch) => READY_LETTERS.some(([letter]) => letter === ch)))
47241
+ return null;
47242
+ return READY_LETTERS.filter(([letter]) => text2.includes(letter)).map(([, kind]) => kind);
46906
47243
  }
46907
47244
  var TYPED_ESCAPES = { "\\": "\\\\", '"': '\\"', " ": "\\s", "\b": "\\b", "\n": "\\n" };
46908
47245
  var UNESCAPED = { "\\": "\\", '"': '"', s: " ", b: "\b", n: "\n" };
@@ -46961,8 +47298,17 @@ function inputOf(run2) {
46961
47298
  const spoken = quote === -1 ? run2 : run2.slice(0, quote);
46962
47299
  const typed = quote === -1 ? null : typedOf(run2.slice(quote + 1, -1));
46963
47300
  const languageAt = spoken.indexOf("=");
46964
- const fingered = languageAt === -1 ? spoken : spoken.slice(0, languageAt);
47301
+ const heard = languageAt === -1 ? spoken : spoken.slice(0, languageAt);
46965
47302
  const language = languageAt === -1 ? null : spoken.slice(languageAt + 1);
47303
+ const onlineAt = heard.indexOf(">");
47304
+ const answered = onlineAt === -1 ? heard : heard.slice(0, onlineAt);
47305
+ const online = onlineAt === -1 ? null : onlineOf(heard.slice(onlineAt + 1));
47306
+ const answerAt = answered.indexOf("$");
47307
+ const readied = answerAt === -1 ? answered : answered.slice(0, answerAt);
47308
+ const answer = answerAt === -1 ? null : answerOf(answered.slice(answerAt + 1));
47309
+ const readyAt = readied.indexOf("#");
47310
+ const fingered = readyAt === -1 ? readied : readied.slice(0, readyAt);
47311
+ const ready = readyAt === -1 ? null : readyOf(readied.slice(readyAt + 1));
46966
47312
  const touchesAt = fingered.indexOf("!");
46967
47313
  const full = touchesAt === -1 ? fingered : fingered.slice(0, touchesAt);
46968
47314
  const touches = touchesAt === -1 ? null : touchesOf(fingered.slice(touchesAt + 1));
@@ -46986,6 +47332,9 @@ function inputOf(run2) {
46986
47332
  ...wheel === null ? {} : { wheel },
46987
47333
  ...sticks === null ? {} : { sticks },
46988
47334
  ...touches === null ? {} : { touches },
47335
+ ...ready === null ? {} : { adsReady: ready },
47336
+ ...answer === null ? {} : { adAnswer: answer },
47337
+ ...online ?? {},
46989
47338
  ...language === null ? {} : { language },
46990
47339
  ...typed === null ? {} : { typed }
46991
47340
  };
@@ -47216,13 +47565,43 @@ function scan(text2, chars) {
47216
47565
  }
47217
47566
  languageKey = `=${language}`;
47218
47567
  }
47219
- const touchesSign = rawFingered.indexOf("!");
47220
- const rawFull = touchesSign === -1 ? rawFingered : rawFingered.slice(0, touchesSign);
47568
+ const onlineSign = rawFingered.indexOf(">");
47569
+ const rawAnswered = onlineSign === -1 ? rawFingered : rawFingered.slice(0, onlineSign);
47570
+ let onlineKeyed = "";
47571
+ if (onlineSign !== -1) {
47572
+ const online = onlineOf(rawFingered.slice(onlineSign + 1));
47573
+ if (online === null) {
47574
+ throw new TrackError(`run ${index2} has '${rawFingered.slice(onlineSign, onlineSign + 24)}': what a game heard online rides as > and the base64url of its JSON, as the page wrote it`, index2, at2);
47575
+ }
47576
+ onlineKeyed = onlineKey(online);
47577
+ }
47578
+ const answerSign = rawAnswered.indexOf("$");
47579
+ const rawReadied = answerSign === -1 ? rawAnswered : rawAnswered.slice(0, answerSign);
47580
+ let answerKey = "";
47581
+ if (answerSign !== -1) {
47582
+ const answer = answerOf(rawAnswered.slice(answerSign + 1));
47583
+ if (answer === null) {
47584
+ throw new TrackError(`run ${index2} has '${rawAnswered.slice(answerSign)}': an ad's answer is $s (shown), $r (rewarded), $c (closed) or $u (unavailable)`, index2, at2);
47585
+ }
47586
+ answerKey = `$${ANSWER_LETTERS[answer]}`;
47587
+ }
47588
+ const readySign = rawReadied.indexOf("#");
47589
+ const rawTouched = readySign === -1 ? rawReadied : rawReadied.slice(0, readySign);
47590
+ let readyKeyed = "";
47591
+ if (readySign !== -1) {
47592
+ const ready = readyOf(rawReadied.slice(readySign + 1));
47593
+ if (ready === null) {
47594
+ throw new TrackError(`run ${index2} has '${rawReadied.slice(readySign)}': ads ready are # with b (break) and w (reward), or # alone for none, like #bw`, index2, at2);
47595
+ }
47596
+ readyKeyed = readyKey(ready);
47597
+ }
47598
+ const touchesSign = rawTouched.indexOf("!");
47599
+ const rawFull = touchesSign === -1 ? rawTouched : rawTouched.slice(0, touchesSign);
47221
47600
  let touchesKey = "";
47222
47601
  if (touchesSign !== -1) {
47223
- const touches = touchesOf(rawFingered.slice(touchesSign + 1));
47602
+ const touches = touchesOf(rawTouched.slice(touchesSign + 1));
47224
47603
  if (touches === null) {
47225
- throw new TrackError(`run ${index2} has the touches '${rawFingered.slice(touchesSign)}', which must be !x,y in whole screen pixels, a semicolon between fingers, one to four, like !120,300;400,310`, index2, at2);
47604
+ throw new TrackError(`run ${index2} has the touches '${rawTouched.slice(touchesSign)}', which must be !x,y in whole screen pixels, a semicolon between fingers, one to four, like !120,300;400,310`, index2, at2);
47226
47605
  }
47227
47606
  touchesKey = `!${touches.map((t) => `${String(t.x)},${String(t.y)}`).join(";")}`;
47228
47607
  }
@@ -47308,7 +47687,7 @@ function scan(text2, chars) {
47308
47687
  aimKey = `@${String(aim.x / AIM_PER_TILE)},${String(aim.y / AIM_PER_TILE)}`;
47309
47688
  }
47310
47689
  const lettered = rawButtons.toLowerCase().replace(/[+|]/g, "");
47311
- const buttons = lettered === "" && (ownKey !== "" || aimKey !== "" || pointKey !== "" || sizeKey !== "" || wheelKey !== "" || sticksKey !== "" || touchesKey !== "" || languageKey !== "" || typedKey !== "") ? "-" : lettered;
47690
+ const buttons = lettered === "" && (ownKey !== "" || aimKey !== "" || pointKey !== "" || sizeKey !== "" || wheelKey !== "" || sticksKey !== "" || touchesKey !== "" || readyKeyed !== "" || answerKey !== "" || onlineKeyed !== "" || languageKey !== "" || typedKey !== "") ? "-" : lettered;
47312
47691
  if (buttons === "") {
47313
47692
  throw new TrackError(`run ${index2} has a count but no buttons: '${token}'`, index2, at2);
47314
47693
  }
@@ -47323,7 +47702,7 @@ function scan(text2, chars) {
47323
47702
  }
47324
47703
  key = keyOf2(inputOf(buttons));
47325
47704
  }
47326
- key += ownKey + aimKey + pointKey + sizeKey + wheelKey + sticksKey + touchesKey + languageKey + typedKey;
47705
+ key += ownKey + aimKey + pointKey + sizeKey + wheelKey + sticksKey + touchesKey + readyKeyed + answerKey + onlineKeyed + languageKey + typedKey;
47327
47706
  if (!/^[1-9][0-9]*$/.test(rawCount)) {
47328
47707
  throw new TrackError(`run ${index2} has the count '${rawCount}', which must be a whole number from 1 to ${MAX_RUN_COUNT} with no leading zero`, index2, at2);
47329
47708
  }
@@ -48848,9 +49227,10 @@ function pressedMeanwhile(input, latched) {
48848
49227
  function quiet(scripts) {
48849
49228
  if (scripts === null)
48850
49229
  return null;
48851
- if (scripts.errors.length === 0 && scripts.warnings.length === 0 && scripts.logs.length === 0 && scripts.sounds.length === 0)
49230
+ if (scripts.errors.length === 0 && scripts.warnings.length === 0 && scripts.logs.length === 0 && scripts.sounds.length === 0 && scripts.privacy !== true && scripts.ad === void 0 && scripts.online === void 0 && scripts.purchase === void 0)
48852
49231
  return scripts;
48853
- return { ...scripts, errors: [], warnings: [], logs: [], sounds: [] };
49232
+ const { privacy: _privacy, ad: _ad, online: _online, purchase: _purchase, ...rest } = scripts;
49233
+ return { ...rest, errors: [], warnings: [], logs: [], sounds: [] };
48854
49234
  }
48855
49235
  function heldTick(state, input) {
48856
49236
  const scripts = quiet(state.scripts);
@@ -49086,6 +49466,7 @@ function ownBodyWith(was, args) {
49086
49466
  var LOGS_PER_TICK = 50;
49087
49467
  var LOG_CHARS = 500;
49088
49468
  var EVENTS_PER_TICK = 256;
49469
+ var AD_EVENTS_PER_TICK = 16;
49089
49470
  var TIMERS_PER_OWNER = 64;
49090
49471
  var TWEENS_PER_OWNER = 64;
49091
49472
  var SEQUENCES_PER_OWNER = 16;
@@ -49100,7 +49481,7 @@ var OWN_AUDIO = /^own:[a-z0-9][a-z0-9-]{0,39}$/;
49100
49481
  var LIB_AUDIO = /^lib:[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
49101
49482
  var MOST_BURSTS = 64;
49102
49483
  var MOST_DEFERRED = 1024;
49103
- 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"]);
49484
+ 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"]);
49104
49485
  function screenOf2(input) {
49105
49486
  const size2 = input.screenSize ?? SCREEN_DEFAULT;
49106
49487
  return { screen: input.screenPointer ?? null, size: { width: size2.w, height: size2.h } };
@@ -49169,7 +49550,7 @@ function fromUnit(raw, unit, what) {
49169
49550
  return fromInt(Math.round(value));
49170
49551
  }
49171
49552
  }
49172
- 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"]);
49553
+ 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"]);
49173
49554
  function sidesOf(normal) {
49174
49555
  switch (normal) {
49175
49556
  case "a_above_b":
@@ -49281,6 +49662,20 @@ var Turn = class {
49281
49662
  language;
49282
49663
  camera;
49283
49664
  sounds;
49665
+ /** game.openPrivacy() was called this tick (the store publishing spec). */
49666
+ privacy;
49667
+ /** The ad asked this tick, and what the game keeps of its ads, absent until it has any (the ads spec). */
49668
+ ad;
49669
+ ads;
49670
+ /** What the scripts asked of the game's server this tick; whether it is online; the next call's number, absent for a
49671
+ * game that never mentions online (the online services spec). */
49672
+ online;
49673
+ onlineState;
49674
+ nextCall;
49675
+ /** What the scripts asked of the store this tick, and what the game keeps of its purchases, absent for a game that
49676
+ * never mentions them (the online services spec, part 3). */
49677
+ purchase;
49678
+ purchases;
49284
49679
  music;
49285
49680
  /** Sounds playing round, and the mixer, replaced whole when changed
49286
49681
  * (sub-project 9). */
@@ -49341,6 +49736,14 @@ var Turn = class {
49341
49736
  this.saved = scripts.saved;
49342
49737
  this.camera = scripts.camera;
49343
49738
  this.sounds = fresh ? [] : [...scripts.sounds];
49739
+ this.privacy = !fresh && scripts.privacy === true;
49740
+ this.ad = fresh ? void 0 : scripts.ad;
49741
+ this.ads = scripts.ads;
49742
+ this.online = fresh || scripts.online === void 0 ? void 0 : [...scripts.online];
49743
+ this.onlineState = scripts.onlineState;
49744
+ this.nextCall = scripts.nextCall;
49745
+ this.purchase = fresh ? void 0 : scripts.purchase;
49746
+ this.purchases = scripts.purchases;
49344
49747
  this.music = scripts.music;
49345
49748
  this.loops = scripts.loops;
49346
49749
  this.mixer = scripts.mixer;
@@ -49486,6 +49889,14 @@ var Turn = class {
49486
49889
  logs: this.logs.length === 0 ? [] : [...this.logs],
49487
49890
  warnings: this.warnings.length,
49488
49891
  sounds: this.sounds.length,
49892
+ privacy: this.privacy,
49893
+ ad: this.ad,
49894
+ ads: this.ads,
49895
+ online: this.online === void 0 ? void 0 : [...this.online],
49896
+ onlineState: this.onlineState,
49897
+ nextCall: this.nextCall,
49898
+ purchase: this.purchase,
49899
+ purchases: this.purchases,
49489
49900
  bursts: this.bursts.length === 0 ? [] : [...this.bursts],
49490
49901
  screen: this.screen,
49491
49902
  waiting: this.waiting,
@@ -49539,6 +49950,14 @@ var Turn = class {
49539
49950
  this.logs.splice(0, this.logs.length, ...was.logs);
49540
49951
  this.warnings.length = was.warnings;
49541
49952
  this.sounds.length = was.sounds;
49953
+ this.privacy = was.privacy;
49954
+ this.ad = was.ad;
49955
+ this.ads = was.ads;
49956
+ this.online = was.online;
49957
+ this.onlineState = was.onlineState;
49958
+ this.nextCall = was.nextCall;
49959
+ this.purchase = was.purchase;
49960
+ this.purchases = was.purchases;
49542
49961
  this.bursts = was.bursts;
49543
49962
  this.screen = was.screen;
49544
49963
  this.waiting = was.waiting;
@@ -50810,6 +51229,76 @@ var Turn = class {
50810
51229
  // kind's script to read; null for a game without one.
50811
51230
  case "gameScript":
50812
51231
  return this.scripts.sources.game === null ? null : this.game ?? {};
51232
+ // The game's privacy policy (the store publishing spec): asked of the page, which knows where it is.
51233
+ // Ads (the ads spec): asked of the page, which answers on a later tick, as input.
51234
+ // Online (the online services spec): asked of the page, which carries it to the game's server; what comes back
51235
+ // is heard on a later tick, as input.
51236
+ case "onlineCall": {
51237
+ const id = this.nextCall ?? 1;
51238
+ this.askOnline({ kind: "call", id, name: args.name, data: args.data ?? null });
51239
+ this.nextCall = id + 1;
51240
+ return id;
51241
+ }
51242
+ case "onlineJoin":
51243
+ case "onlineLeave":
51244
+ this.askOnline({ kind: op === "onlineJoin" ? "join" : "leave", room: args.room });
51245
+ return null;
51246
+ case "onlineSend":
51247
+ this.askOnline({ kind: "send", room: args.room, data: args.data ?? null });
51248
+ return null;
51249
+ // Part 2: an upload through one of the game's calls, a download, and asking for push; numbered with the calls.
51250
+ case "onlineUpload":
51251
+ case "onlineDownload": {
51252
+ const id = this.nextCall ?? 1;
51253
+ this.askOnline(op === "onlineDownload" ? { kind: "download", id, url: args.url } : { kind: "upload", id, name: args.name, data: args.data ?? null, ...args.screen === true ? { screen: true } : { file: args.file ?? null } });
51254
+ this.nextCall = id + 1;
51255
+ return id;
51256
+ }
51257
+ case "onlineAskPush":
51258
+ this.askOnline({ kind: "askPush" });
51259
+ return null;
51260
+ case "onlineState":
51261
+ return this.onlineState ?? "off";
51262
+ // Purchases (the online services spec, part 3): asked of the page, which asks the store; the answer is heard on a
51263
+ // later tick, as input.
51264
+ case "purchaseBuy":
51265
+ this.askPurchase({ kind: "buy", id: args.id });
51266
+ return null;
51267
+ case "purchaseRestore":
51268
+ this.askPurchase({ kind: "restore" });
51269
+ return null;
51270
+ case "purchaseProducts":
51271
+ return (this.purchases ?? NO_PURCHASES_YET).products;
51272
+ case "adBreak":
51273
+ case "adReward": {
51274
+ const placement = args.placement;
51275
+ if (typeof placement !== "string" || !PLACEMENT.test(placement))
51276
+ throw new Error(`${op === "adBreak" ? "ads.break" : "ads.reward"} was given ${JSON.stringify(placement)}: ${PLACEMENT_WORDS}`);
51277
+ this.askAd({ kind: op === "adBreak" ? "break" : "reward", placement });
51278
+ return null;
51279
+ }
51280
+ case "adReady": {
51281
+ const kind = args.kind;
51282
+ if (!AD_KINDS.includes(kind))
51283
+ throw new Error(`ads.ready takes 'break' or 'reward', not ${JSON.stringify(kind)}`);
51284
+ return this.scripts.adsOff === void 0 && adsOf(this.ads).ready.includes(kind);
51285
+ }
51286
+ case "adBanner": {
51287
+ const position = args.position;
51288
+ if (position !== null && position !== "top" && position !== "bottom")
51289
+ throw new Error(`ads.banner takes 'top', 'bottom' or null, not ${JSON.stringify(position)}`);
51290
+ if (position !== null && this.scripts.adsOff !== void 0) {
51291
+ this.warn(this.scripts.adsOff);
51292
+ return null;
51293
+ }
51294
+ this.ads = { ...adsOf(this.ads), banner: position };
51295
+ if (position !== null && this.ui !== void 0)
51296
+ this.bannerBeside(position, this.ui);
51297
+ return null;
51298
+ }
51299
+ case "openPrivacy":
51300
+ this.privacy = true;
51301
+ return null;
50813
51302
  case "save": {
50814
51303
  this.saved = { ...this.saved, [text(args.key, "save: key")]: args.value ?? null };
50815
51304
  return null;
@@ -51390,8 +51879,132 @@ var Turn = class {
51390
51879
  this.setDriven(id, { ...this.driven.get(id) ?? { x: false, y: false }, facing: true });
51391
51880
  return this.row(e);
51392
51881
  }
51882
+ /** An ad asked of the page for this tick, and kept until its answer comes (the ads spec); refused at once, in the
51883
+ * words of the rule it breaks, when it breaks one the engine can tell exactly, and warned of when a break comes in
51884
+ * the middle of play. */
51885
+ askAd(ask) {
51886
+ const ads = adsOf(this.ads);
51887
+ const refused = adRefusal(ads, ask.kind, this.real, this.scripts.adsOff ?? null);
51888
+ if (refused !== null) {
51889
+ this.warn(refused);
51890
+ this.hearAd({ kind: ask.kind, placement: ask.placement, result: "refused", reason: refused });
51891
+ return;
51892
+ }
51893
+ if (ask.kind === "break" && !this.clock.paused) {
51894
+ const playing = playHeld(this.input) || ads.lastPlay !== null && this.real - ads.lastPlay <= PLAY_PRESS_TICKS;
51895
+ const settled = ads.scene === null || this.real - ads.scene.at > SCENE_SETTLE_TICKS;
51896
+ if (playing && settled)
51897
+ this.warn(AD_WORDS.midPlay);
51898
+ }
51899
+ this.ad = ask;
51900
+ this.ads = { ...ads, pending: ask };
51901
+ }
51902
+ /** A banner beside one of the game's own buttons, warned of: a button within a few pixels of the banner's edge. */
51903
+ bannerBeside(position, ui) {
51904
+ const screen = screenOf2(this.input).size;
51905
+ for (const [name, box] of layoutUi(ui, screen)) {
51906
+ if (ui.elements[name]?.kind !== "button")
51907
+ continue;
51908
+ const beside = position === "top" ? box.y <= BANNER_EDGE_PX : box.y + box.h >= screen.height - BANNER_EDGE_PX;
51909
+ if (beside)
51910
+ this.warn(AD_WORDS.banner(name));
51911
+ }
51912
+ }
51913
+ /** What the tick's input says of ads: the kinds ready, and the page's answer to the ad pending, heard as an 'ad'
51914
+ * event; an answer with none pending is let go (the ads spec). */
51915
+ takeAdInput() {
51916
+ const { adAnswer, adsReady } = this.input;
51917
+ if (this.ads !== void 0) {
51918
+ const before2 = this.state.prevInput;
51919
+ const pressed = anyPressed(this.input, before2) || (this.ui?.clicked.length ?? 0) > 0;
51920
+ const played = playPressed(this.input, before2);
51921
+ const key = `${String(this.state.level)}:${String(this.scripts.loads)}`;
51922
+ const scene = this.ads.scene?.key === key ? this.ads.scene : { key, at: this.real };
51923
+ if (pressed || played || scene !== this.ads.scene) {
51924
+ this.ads = {
51925
+ ...this.ads,
51926
+ scene,
51927
+ ...pressed ? { played: true, lastPress: this.real } : {},
51928
+ ...played ? { lastPlay: this.real } : {}
51929
+ };
51930
+ }
51931
+ }
51932
+ if (adsReady !== void 0)
51933
+ this.ads = { ...adsOf(this.ads), ready: AD_KINDS.filter((kind) => adsReady.includes(kind)) };
51934
+ const pending = this.ads?.pending ?? null;
51935
+ if (adAnswer === void 0 || pending === null)
51936
+ return;
51937
+ this.ads = { ...adsOf(this.ads), pending: null, ...pending.kind === "break" && adAnswer === "shown" ? { lastBreak: this.real } : {} };
51938
+ this.hearAd({ kind: pending.kind, placement: pending.placement, result: adAnswer });
51939
+ }
51940
+ /** An ask of the game's server, refused in words when it is not one, or past a tick's asks. */
51941
+ askOnline(ask) {
51942
+ const problem = askProblem(ask);
51943
+ if (problem !== null)
51944
+ throw new Error(problem);
51945
+ const asked = this.online ?? [];
51946
+ if (asked.length >= ONLINE_LIMITS.asksPerTick)
51947
+ throw new Error(TOO_MANY_ASKS);
51948
+ this.online = [...asked, ask];
51949
+ }
51950
+ /** What the page brought back from the game's server, and whether it is online, for a game that keeps online: each
51951
+ * heard as an 'online' event. A game that never mentions online ignores it. */
51952
+ takeOnlineInput() {
51953
+ if (this.nextCall === void 0)
51954
+ return;
51955
+ const { online, onlineState } = this.input;
51956
+ if (onlineState !== void 0)
51957
+ this.onlineState = onlineState;
51958
+ for (const heard of online ?? [])
51959
+ this.hearHost("online", heard);
51960
+ }
51961
+ /** A purchase asked of the page for this tick: a buy of an id not one, or a second ask in a tick, refused in words; a
51962
+ * buy while another waits for its answer heard at once as refused. */
51963
+ askPurchase(ask) {
51964
+ const problem = purchaseAskProblem(ask);
51965
+ if (problem !== null)
51966
+ throw new Error(problem);
51967
+ if (this.purchase !== void 0)
51968
+ throw new Error("game.purchases asks the store once a tick: ask again on a later tick");
51969
+ const purchases = this.purchases ?? NO_PURCHASES_YET;
51970
+ if (ask.kind === "buy" && purchases.pending !== null) {
51971
+ this.hearHost("purchase", { kind: "bought", id: ask.id, result: "refused", reason: "a purchase is already in progress: wait for its answer" });
51972
+ return;
51973
+ }
51974
+ this.purchase = ask;
51975
+ if (ask.kind === "buy")
51976
+ this.purchases = { ...purchases, pending: ask.id };
51977
+ }
51978
+ /** What the store and the game's server answered of purchases, for a game that keeps them: the products kept, and each
51979
+ * heard as a 'purchase' event; the buy pending let go once its answer comes. A game that never mentions purchases
51980
+ * ignores it. */
51981
+ takePurchaseInput() {
51982
+ if (this.purchases === void 0)
51983
+ return;
51984
+ for (const heard of this.input.purchases ?? []) {
51985
+ if (heard.kind === "products")
51986
+ this.purchases = { ...this.purchases, products: heard.list };
51987
+ if (heard.kind === "bought" && heard.id === this.purchases.pending)
51988
+ this.purchases = { ...this.purchases, pending: null };
51989
+ this.hearHost("purchase", heard);
51990
+ }
51991
+ }
51992
+ /** An 'ad' event every onEvent hears on the next tick, as an emitted one is. */
51993
+ hearAd(data) {
51994
+ this.hearHost("ad", data);
51995
+ }
51996
+ /** An event from the page (an ad's answer, what the game heard online), heard past a tick's emitted events, up to its
51997
+ * own few (the final review of ads). */
51998
+ hearHost(name, data) {
51999
+ if (this.emitted.length >= EVENTS_PER_TICK + AD_EVENTS_PER_TICK)
52000
+ return;
52001
+ if (this.emitted.length >= EVENTS_PER_TICK && this.emitted.filter((one2) => one2.name === name).length >= AD_EVENTS_PER_TICK)
52002
+ return;
52003
+ this.emitted.push({ name, data: { ...data }, at: this.events.length });
52004
+ this.events.push(name);
52005
+ }
51393
52006
  scriptsState() {
51394
- const { starting: _was, loops: _loops, mixer: _mixer, typing: _typing, ui: _ui, ...before2 } = this.scripts;
52007
+ 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;
51395
52008
  return {
51396
52009
  ...before2,
51397
52010
  fields: this.fields,
@@ -51417,6 +52030,14 @@ var Turn = class {
51417
52030
  ...this.mixer === void 0 ? {} : { mixer: this.mixer },
51418
52031
  camera: this.camera,
51419
52032
  sounds: this.sounds,
52033
+ ...this.privacy ? { privacy: true } : {},
52034
+ ...this.ad === void 0 ? {} : { ad: this.ad },
52035
+ ...this.ads === void 0 ? {} : { ads: this.ads },
52036
+ ...this.online === void 0 || this.online.length === 0 ? {} : { online: this.online },
52037
+ ...this.onlineState === void 0 ? {} : { onlineState: this.onlineState },
52038
+ ...this.nextCall === void 0 ? {} : { nextCall: this.nextCall },
52039
+ ...this.purchase === void 0 ? {} : { purchase: this.purchase },
52040
+ ...this.purchases === void 0 ? {} : { purchases: this.purchases },
51420
52041
  music: this.music,
51421
52042
  bursts: this.bursts,
51422
52043
  deferred: this.deferred,
@@ -51484,6 +52105,9 @@ function beginScripts(state, input) {
51484
52105
  if (scripts === null || !hasScripts(scripts))
51485
52106
  return null;
51486
52107
  const turn = new Turn({ ...state, events: [] }, scripts, input, state.contacts, state.step, true, scripts.driven);
52108
+ turn.takeAdInput();
52109
+ turn.takeOnlineInput();
52110
+ turn.takePurchaseInput();
51487
52111
  turn.bursts = turn.bursts.filter((b) => turn.now - b.step < b.life * 60);
51488
52112
  dueChange(turn);
51489
52113
  for (const late of scripts.deferred) {
@@ -51588,6 +52212,9 @@ function pausedScripts(state, input) {
51588
52212
  if (scripts === null)
51589
52213
  throw new Error("a game without scripts cannot be paused");
51590
52214
  const turn = new Turn({ ...state, events: [] }, scripts, input, state.contacts, state.step, true, scripts.driven);
52215
+ turn.takeAdInput();
52216
+ turn.takeOnlineInput();
52217
+ turn.takePurchaseInput();
51591
52218
  dueChange(turn);
51592
52219
  turn.call(null, "whilePaused", [DT]);
51593
52220
  if (turn.clock.paused && (turn.paused.includes(0) || !(turn.runtime.has(null) && turn.runtime.hasHook(null, "whilePaused")))) {
@@ -51596,7 +52223,8 @@ function pausedScripts(state, input) {
51596
52223
  }
51597
52224
  heardClicks(turn);
51598
52225
  const heard = closeScripts(turn.scriptsState(), turn.events).inbox;
51599
- const result = turn.result([...scripts.inbox, ...heard].slice(0, EVENTS_PER_TICK));
52226
+ const all = [...scripts.inbox, ...heard];
52227
+ const result = turn.result([...all.slice(0, EVENTS_PER_TICK), ...all.slice(EVENTS_PER_TICK).filter((one2) => one2.name === "ad" || one2.name === "online").slice(0, AD_EVENTS_PER_TICK)]);
51600
52228
  const held = result.state.scripts;
51601
52229
  if (held === null)
51602
52230
  return result;