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.
@@ -26221,7 +26221,7 @@ function shouldInterruptAfterDeadline(deadline) {
26221
26221
  var import_sucrase2 = __toESM(require_dist2(), 1);
26222
26222
 
26223
26223
  // ../core/dist/version.js
26224
- var ENGINE_VERSION = "0.9.1";
26224
+ var ENGINE_VERSION = "0.9.2";
26225
26225
 
26226
26226
  // ../core/dist/home.js
26227
26227
  import { existsSync, renameSync, symlinkSync } from "node:fs";
@@ -29858,6 +29858,170 @@ function impulseFor(height, gravity, terminalFall) {
29858
29858
  return impulseUpTo(height, gravity, terminalFall);
29859
29859
  }
29860
29860
 
29861
+ // ../core/dist/script/ads.js
29862
+ var AD_KINDS = ["break", "reward"];
29863
+ var NO_ADS_YET = { pending: null, lastBreak: null, lastPress: null, lastPlay: null, played: false, scene: null, ready: AD_KINDS, banner: null };
29864
+ var PLACEMENT = /^[a-z0-9][a-z0-9-]{0,39}$/;
29865
+ var PLACEMENT_WORDS = "a placement is named with 1 to 40 lowercase letters, digits and hyphens, like level-end or extra-life";
29866
+ var adsOf = (kept) => kept ?? NO_ADS_YET;
29867
+ var usesAds = (sources) => sources.some((source2) => /\bads\s*\.\s*(break|reward|ready|banner)\b/.test(source2));
29868
+ var BREAK_GAP_TICKS = 3600;
29869
+ var REWARD_PRESS_TICKS = 60;
29870
+ var PLAY_PRESS_TICKS = 30;
29871
+ var SCENE_SETTLE_TICKS = 120;
29872
+ var BANNER_EDGE_PX = 8;
29873
+ var AD_WORDS = {
29874
+ launch: "a break before the player's first input is refused: AdMob forbids ads at launch",
29875
+ backToBack: "a break less than 60 seconds after the last one is refused: AdMob forbids ads back to back",
29876
+ unchosen: "a reward the player did not choose is refused: AdMob rewards only an ad the player asks for, with a press just before",
29877
+ showing: "an ad is already showing",
29878
+ children: "this game is made for children: it shows no ads",
29879
+ 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",
29880
+ banner: (name) => `a banner beside the game's button '${name}': AdMob forbids ads where players tap; keep buttons away from the banner's edge`
29881
+ };
29882
+ function adRefusal(ads, kind, now, off) {
29883
+ if (off !== null)
29884
+ return off;
29885
+ if (ads.pending !== null)
29886
+ return AD_WORDS.showing;
29887
+ if (kind === "break") {
29888
+ if (!ads.played)
29889
+ return AD_WORDS.launch;
29890
+ if (ads.lastBreak !== null && now - ads.lastBreak < BREAK_GAP_TICKS)
29891
+ return AD_WORDS.backToBack;
29892
+ return null;
29893
+ }
29894
+ return ads.lastPress !== null && now - ads.lastPress <= REWARD_PRESS_TICKS ? null : AD_WORDS.unchosen;
29895
+ }
29896
+ var SIX = ["left", "right", "up", "down", "jump", "action"];
29897
+ var playButtons = (input) => (input.buttons ?? []).filter((name) => name !== "click" && !name.startsWith("click:"));
29898
+ var playHeld = (input) => SIX.some((button) => input[button]) || playButtons(input).length > 0;
29899
+ var playPressed = (input, before2) => SIX.some((button) => input[button] && !before2[button]) || playButtons(input).some((name) => !playButtons(before2).includes(name));
29900
+ 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 !== "";
29901
+
29902
+ // ../core/dist/script/online.js
29903
+ var ONLINE_STATES = ["online", "resting", "off"];
29904
+ var PUSH_RESULTS = ["granted", "denied", "unavailable"];
29905
+ var CALL_RESULTS = ["ok", "error", "busy", "resting", "refused"];
29906
+ var ONLINE_LIMITS = {
29907
+ asksPerTick: 32,
29908
+ dataBytes: 8192,
29909
+ callName: /^[a-zA-Z][a-zA-Z0-9_]{0,39}$/,
29910
+ room: /^[a-z0-9][a-z0-9-]{0,39}$/,
29911
+ /** A file a game's scripts upload, as JSON; the screen's picture is the page's, capped by the server's size. */
29912
+ fileBytes: 1e6,
29913
+ /** What a download gives a game, as JSON. */
29914
+ downloadBytes: 1e6
29915
+ };
29916
+ var TOO_MANY_ASKS = `a tick asks the game's server at most ${String(ONLINE_LIMITS.asksPerTick)} times: spread the calls over ticks`;
29917
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29918
+ var jsonBytes = (data) => {
29919
+ try {
29920
+ const text3 = JSON.stringify(data);
29921
+ return text3 === void 0 ? null : new TextEncoder().encode(text3).length;
29922
+ } catch {
29923
+ return null;
29924
+ }
29925
+ };
29926
+ var dataProblem = (data) => {
29927
+ const bytes = jsonBytes(data === void 0 ? null : data);
29928
+ if (bytes === null)
29929
+ return "online data is plain data: numbers, strings, booleans, null, lists and objects of them";
29930
+ return bytes > ONLINE_LIMITS.dataBytes ? `online data is at most 8,192 bytes as JSON, and this is ${String(bytes)}` : null;
29931
+ };
29932
+ 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)}`;
29933
+ var uploadProblem = (ask) => {
29934
+ const file = "file" in ask && ask["file"] !== void 0;
29935
+ if (file === (ask["screen"] === true))
29936
+ return "an upload is a file or the screen: give file data, or screen: true, not both";
29937
+ if (!file)
29938
+ return null;
29939
+ const bytes = jsonBytes(ask["file"]);
29940
+ if (bytes === null)
29941
+ return "a file is plain data: numbers, strings, booleans, null, lists and objects of them";
29942
+ 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;
29943
+ };
29944
+ function askProblem(ask) {
29945
+ const kinds = "an online ask is a call, join, leave, send, upload, download or askPush";
29946
+ if (!isObject(ask))
29947
+ return kinds;
29948
+ switch (ask["kind"]) {
29949
+ case "call":
29950
+ return nameProblem(ask["name"]) ?? dataProblem(ask["data"]);
29951
+ case "upload":
29952
+ return nameProblem(ask["name"]) ?? dataProblem(ask["data"]) ?? uploadProblem(ask);
29953
+ case "download":
29954
+ 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://";
29955
+ case "askPush":
29956
+ return null;
29957
+ case "join":
29958
+ case "leave":
29959
+ case "send":
29960
+ if (typeof ask["room"] !== "string" || !ONLINE_LIMITS.room.test(ask["room"])) {
29961
+ return `a room's name is 1 to 40 lowercase letters, digits and hyphens, like lobby-1, not ${JSON.stringify(ask["room"])}`;
29962
+ }
29963
+ return ask["kind"] === "send" ? dataProblem(ask["data"]) : null;
29964
+ default:
29965
+ return kinds;
29966
+ }
29967
+ }
29968
+ var usesOnline = (sources) => sources.some((source2) => /\bonline\s*\.\s*(call|join|leave|send|state|upload|uploadScreen|download|askPush)\b/.test(source2));
29969
+ function isOnlineHeard(value) {
29970
+ if (!isObject(value))
29971
+ return false;
29972
+ switch (value["kind"]) {
29973
+ case "answer":
29974
+ 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");
29975
+ case "message":
29976
+ return typeof value["room"] === "string" && value["data"] !== void 0;
29977
+ case "joined":
29978
+ case "left":
29979
+ case "full":
29980
+ return typeof value["room"] === "string";
29981
+ case "upload":
29982
+ 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");
29983
+ case "download":
29984
+ return typeof value["id"] === "number" && Number.isInteger(value["id"]) && CALL_RESULTS.includes(value["result"]) && (value["reason"] === void 0 || typeof value["reason"] === "string");
29985
+ case "push":
29986
+ return PUSH_RESULTS.includes(value["result"]);
29987
+ case "pushOpened":
29988
+ return value["data"] !== void 0;
29989
+ default:
29990
+ return false;
29991
+ }
29992
+ }
29993
+
29994
+ // ../core/dist/script/purchases.js
29995
+ var PURCHASE_RESULTS = ["granted", "pending", "cancelled", "unavailable", "refused"];
29996
+ var PRODUCT_ID = /^[a-z0-9][a-z0-9_.]{0,39}$/;
29997
+ var isObject2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29998
+ function purchaseAskProblem(ask) {
29999
+ if (!isObject2(ask))
30000
+ return "a purchase ask is a buy of a product or a restore";
30001
+ if (ask["kind"] === "restore")
30002
+ return null;
30003
+ if (ask["kind"] !== "buy")
30004
+ return "a purchase ask is a buy of a product or a restore";
30005
+ const id = ask["id"];
30006
+ 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)}`;
30007
+ }
30008
+ var isProduct = (value) => isObject2(value) && typeof value["id"] === "string" && typeof value["price"] === "string" && typeof value["name"] === "string";
30009
+ function isPurchaseHeard(value) {
30010
+ if (!isObject2(value))
30011
+ return false;
30012
+ switch (value["kind"]) {
30013
+ case "products":
30014
+ return Array.isArray(value["list"]) && value["list"].every(isProduct);
30015
+ case "bought":
30016
+ 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");
30017
+ case "restored":
30018
+ return Array.isArray(value["ids"]) && value["ids"].every((id) => typeof id === "string");
30019
+ default:
30020
+ return false;
30021
+ }
30022
+ }
30023
+ var usesPurchases = (sources) => sources.some((source2) => /\bpurchases\s*\.\s*(buy|restore|products)\b/.test(source2));
30024
+
29861
30025
  // ../core/dist/script/runtime.js
29862
30026
  var import_sucrase = __toESM(require_dist2(), 1);
29863
30027
 
@@ -30761,6 +30925,33 @@ const __soundHow = (options, label) => {
30761
30925
  }
30762
30926
  return out
30763
30927
  }
30928
+ /** Online (the online services spec): asked of the game's server through the page; what comes back is heard on a later
30929
+ * tick as an 'online' event. */
30930
+ const __online = Object.freeze({
30931
+ call(name, data) { return __call('onlineCall', { name, data: data === undefined ? null : data }) },
30932
+ join(room) { __call('onlineJoin', { room }) },
30933
+ leave(room) { __call('onlineLeave', { room }) },
30934
+ send(room, data) { __call('onlineSend', { room, data: data === undefined ? null : data }) },
30935
+ upload(name, file, data) { return __call('onlineUpload', { name, file: file === undefined ? null : file, data: data === undefined ? null : data }) },
30936
+ uploadScreen(name, data) { return __call('onlineUpload', { name, screen: true, data: data === undefined ? null : data }) },
30937
+ download(url) { return __call('onlineDownload', { url }) },
30938
+ askPush() { __call('onlineAskPush', {}) },
30939
+ get state() { return __call('onlineState', {}) },
30940
+ })
30941
+ /** Purchases (the online services spec, part 3): asked of the page, which asks the store; answered on a later tick as a
30942
+ * 'purchase' event. */
30943
+ const __purchases = Object.freeze({
30944
+ get products() { return __frozen(__revived(__call('purchaseProducts', {}))) },
30945
+ buy(id) { __call('purchaseBuy', { id }) },
30946
+ restore() { __call('purchaseRestore', {}) },
30947
+ })
30948
+ /** Ads (the ads spec): asked of the page, answered on a later tick as an 'ad' event. */
30949
+ const __ads = Object.freeze({
30950
+ break(placement) { __call('adBreak', { placement }) },
30951
+ reward(placement) { __call('adReward', { placement }) },
30952
+ ready(kind) { return __call('adReady', { kind }) === true },
30953
+ banner(position) { __call('adBanner', { position: position === undefined ? null : position }) },
30954
+ })
30764
30955
  const __sound = {
30765
30956
  play(name, options) { __call('sound', Object.assign({ name }, __soundHow(options, 'sound.play'))) },
30766
30957
  loop(name, options) { return __call('soundLoop', Object.assign({ name }, __soundHow(options, 'sound.loop'))) },
@@ -31013,6 +31204,9 @@ const __game = {
31013
31204
  get vars() { return __vars },
31014
31205
  get turns() { return __turns },
31015
31206
  get ui() { return __ui },
31207
+ get ads() { return __ads },
31208
+ get online() { return __online },
31209
+ get purchases() { return __purchases },
31016
31210
  /** The game script's own fields, read only; null without one. */
31017
31211
  get script() { return __frozen(__revived(__call('gameScript', null))) },
31018
31212
  get input() { return __input },
@@ -31074,6 +31268,7 @@ const __game = {
31074
31268
  restart(options) { __call('restart', { fade: __num(__opt(options).fade, null) }) },
31075
31269
  flash(colour, seconds) { __call('screen', { flash: { color: __color(colour, 0xffffff), seconds: seconds === undefined ? 0.2 : seconds } }) },
31076
31270
  fade(alpha, seconds, colour) { __call('screen', { fade: { alpha, seconds, color: __color(colour, 0x000000) } }) },
31271
+ openPrivacy() { __call('openPrivacy', null) },
31077
31272
  save(key, value) { __call('save', { key, value: value === undefined ? null : value }) },
31078
31273
  load(key) { return __call('load', { key }) },
31079
31274
  get camera() { return __camera },
@@ -31513,7 +31708,7 @@ var FAILURES_BEFORE_PAUSE = 10;
31513
31708
  var MEMORY_LIMIT = 32 * 1024 * 1024;
31514
31709
  var INTERRUPTS_PER_CALL = 1e3;
31515
31710
  var INTERRUPTS_PER_PHASE = 2e3;
31516
- var STACK_BYTES = 256 * 1024;
31711
+ var STACK_BYTES = 128 * 1024;
31517
31712
  var GAME_KEY = "__game";
31518
31713
  var PRELUDE = `'use strict'
31519
31714
  ;(function () {
@@ -32204,7 +32399,8 @@ function build(sources, skipped) {
32204
32399
  return { fields: fieldsOf(text3), error: null, ...wantReturn ? { returned } : {} };
32205
32400
  } catch (error2) {
32206
32401
  healthy = false;
32207
- return { ...failed(`the script broke the interpreter and was stopped: ${error2 instanceof Error ? error2.message : String(error2)}`), runaway: true };
32402
+ const hostStack = error2 instanceof RangeError && /call stack/i.test(error2.message);
32403
+ return { ...failed(hostStack ? messageOf({ message: "stack overflow" }) : `the script broke the interpreter and was stopped: ${error2 instanceof Error ? error2.message : String(error2)}`), runaway: true };
32208
32404
  } finally {
32209
32405
  for (const handle of handles)
32210
32406
  handle.dispose();
@@ -32276,6 +32472,7 @@ function mixAt(level2, real) {
32276
32472
  return level2.from;
32277
32473
  return level2.from + (level2.to - level2.from) * (real - level2.start) / level2.ticks;
32278
32474
  }
32475
+ var NO_PURCHASES_YET = { products: [], pending: null };
32279
32476
  var START_CLOCK = { real: 0, rate: 60, owed: 0, paused: false, stopUntil: 0, latched: [] };
32280
32477
  var ticking = /* @__PURE__ */ new Map();
32281
32478
  var drawing = /* @__PURE__ */ new Map();
@@ -32304,7 +32501,7 @@ function runtimeOf(sources, draw = false) {
32304
32501
  function hasScripts(scripts) {
32305
32502
  return scripts.sources.game !== null || Object.keys(scripts.sources.kinds).length > 0;
32306
32503
  }
32307
- function initialScripts(given, kinds, seed = 1, saved = {}, audio, fonts) {
32504
+ function initialScripts(given, kinds, seed = 1, saved = {}, audio, fonts, adsOff) {
32308
32505
  const sources = {
32309
32506
  kinds: { ...given.kinds },
32310
32507
  game: given.game,
@@ -32350,7 +32547,14 @@ function initialScripts(given, kinds, seed = 1, saved = {}, audio, fonts) {
32350
32547
  sequences: [],
32351
32548
  clock: START_CLOCK,
32352
32549
  ...audio === void 0 ? {} : { audio },
32353
- ...fonts === void 0 ? {} : { fonts }
32550
+ ...fonts === void 0 ? {} : { fonts },
32551
+ // A game whose scripts mention ads keeps them from the first tick, its presses known before it first asks.
32552
+ ...usesAds([...Object.values(sources.kinds), sources.game ?? "", ...Object.values(sources.modules ?? {})]) ? { ads: NO_ADS_YET } : {},
32553
+ ...adsOff === void 0 ? {} : { adsOff },
32554
+ // A game whose scripts mention online keeps its state and call count from the first tick; any other never has them.
32555
+ ...usesOnline([...Object.values(sources.kinds), sources.game ?? "", ...Object.values(sources.modules ?? {})]) ? { onlineState: "off", nextCall: 1 } : {},
32556
+ // A game whose scripts mention purchases keeps its products from the first tick; any other never has them.
32557
+ ...usesPurchases([...Object.values(sources.kinds), sources.game ?? "", ...Object.values(sources.modules ?? {})]) ? { purchases: NO_PURCHASES_YET } : {}
32354
32558
  };
32355
32559
  }
32356
32560
 
@@ -32659,7 +32863,7 @@ function numericProps(props) {
32659
32863
  }
32660
32864
  function load(ast, options = {}) {
32661
32865
  const palette = paletteOf(ast);
32662
- const scripts = options.scripts === void 0 ? null : initialScripts(options.scripts, new Set(ast.entities.map((e2) => e2.name)), options.seed, options.saved, options.audio, options.fonts);
32866
+ const scripts = options.scripts === void 0 ? null : initialScripts(options.scripts, new Set(ast.entities.map((e2) => e2.name)), options.seed, options.saved, options.audio, options.fonts, options.adsOff);
32663
32867
  const defs = [];
32664
32868
  const defIndexOf = /* @__PURE__ */ new Map();
32665
32869
  const kindDefs = /* @__PURE__ */ new Map();
@@ -33163,6 +33367,71 @@ interface Mixer {
33163
33367
  }
33164
33368
 
33165
33369
  /** game.turns: whose turn it is, in an order of objects. */
33370
+ /** Ads, for a game in Google Play and the App Store, from its maker's own AdMob account (the guide's publish topic).
33371
+ * The page answers a break or a reward on a later tick, and every script hears it as onEvent('ad', { kind, placement,
33372
+ * result, reason? }): result is 'shown', 'rewarded', 'closed', 'unavailable' or 'refused'. Give a reward only on
33373
+ * 'rewarded'. In the editor an ad is a placeholder card; in browser play every ad is 'unavailable'. */
33374
+ interface Ads {
33375
+ /** A full-screen break at a natural pause, like a level's end: placement names it, like 'level-end'. Refused before
33376
+ * the player's first input, and less than 60 seconds after the last break. */
33377
+ break(placement: string): void
33378
+ /** An ad the player chose to watch for a reward, like 'extra-life': ask it right after the player's press on the
33379
+ * game's own offer. Refused with no press in the last second. */
33380
+ reward(placement: string): void
33381
+ /** 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. */
33382
+ ready(kind: 'break' | 'reward'): boolean
33383
+ /** A banner at the top or bottom of the screen, which the game's view makes room for; null hides it. */
33384
+ banner(position: 'top' | 'bottom' | null): void
33385
+ }
33386
+
33387
+ /** Real-money purchases in the game's store app (the guide's purchases topic): the products purchases.json declares,
33388
+ * bought through Apple's and Google's own in-app purchases. Each answer reaches every script on a later tick as
33389
+ * onEvent('purchase', heard), where heard is { kind: 'products', list }, { kind: 'bought', id, result, data?, reason?,
33390
+ * test? } or { kind: 'restored', ids }. Give what was bought only on result 'granted' (data is what the game's server
33391
+ * answered from onPurchase); 'pending' is not paid yet, and its answer comes again once it is. In a browser and a web
33392
+ * export, every buy is 'unavailable'. */
33393
+ interface Purchases {
33394
+ /** The products as the store shows them to this player: the price in their money, the name in their language; empty
33395
+ * until the store answers, and in a browser. */
33396
+ readonly products: readonly { readonly id: string; readonly price: string; readonly name: string }[]
33397
+ /** Buys a product by its id, like 'coins_100': the store's own sheet asks the player. One at a time: a second buy
33398
+ * while one waits for its answer is heard 'refused'. */
33399
+ buy(id: string): void
33400
+ /** Asks the store for the unlocks and subscriptions this player owns, heard as { kind: 'restored', ids }: Apple asks for
33401
+ * a Restore purchases button wherever unlocks are sold. */
33402
+ restore(): void
33403
+ }
33404
+
33405
+ /** The game's own server, for a game whose Online tab turned it on (the guide's online topic). Each ask goes to the
33406
+ * server through the page; what comes back reaches every script on a later tick as onEvent('online', heard), where
33407
+ * heard is { kind: 'answer', id, name, result, data?, reason? } (result 'ok', 'error', 'busy', 'resting' or 'refused'),
33408
+ * { kind: 'message', room, data }, or { kind: 'joined' | 'left' | 'full', room }. A tick never waits on the network. */
33409
+ interface Online {
33410
+ /** Asks the server's call by name, like 'postScore', with plain data (at most 8,192 bytes as JSON); answers the call's
33411
+ * number, which its answer carries. */
33412
+ call(name: string, data?: Plain): number
33413
+ /** Joins a live room by name (lowercase letters, digits and hyphens, like 'lobby-1'); heard as 'joined', or 'full'. */
33414
+ join(room: string): void
33415
+ leave(room: string): void
33416
+ /** Sends plain data to a room's code, which decides what everyone hears. */
33417
+ send(room: string, data: Plain): void
33418
+ /** 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,
33419
+ * which gets { size, type, data } and answers what ctx.files.uploadUrl gives; heard as { kind: 'upload', id, name,
33420
+ * result, reason? }. Answers the upload's number. */
33421
+ upload(call: string, file: Plain, data?: Plain): number
33422
+ /** Uploads a picture of the game's screen (a PNG) the same way. */
33423
+ uploadScreen(call: string, data?: Plain): number
33424
+ /** Reads a file at an address the game's server gave (ctx.files.downloadUrl): heard as { kind: 'download', id, result,
33425
+ * data?, reason? }, its data the file's JSON (at most 1,000,000 bytes). Answers the download's number. */
33426
+ download(url: string): number
33427
+ /** Asks the player whether the game may send them notifications, where it can (a phone app with push on): heard as
33428
+ * { kind: 'push', result: 'granted' | 'denied' | 'unavailable' }. Never required to play, never rewarded. A
33429
+ * notification the player tapped to open the game is heard as { kind: 'pushOpened', data }. */
33430
+ askPush(): void
33431
+ /** 'online', 'resting' (the server is off or paused, or its wallet is empty) or 'off' (no connection yet). */
33432
+ readonly state: 'online' | 'resting' | 'off'
33433
+ }
33434
+
33166
33435
  interface Turns {
33167
33436
  /** Begins an order of these objects: the first's turn, round 1. */
33168
33437
  begin(things: Thing[]): void
@@ -33536,6 +33805,15 @@ interface Game {
33536
33805
  /** The game's UI elements: placed, hovered, pressed and clicked by the engine, drawn by the game. Kept through a goTo,
33537
33806
  * gone with a restart. */
33538
33807
  readonly ui: Ui
33808
+ /** Breaks, rewards and banners from the game's maker's own AdMob account. */
33809
+ readonly ads: Ads
33810
+ /** The game's own server: calls and live rooms. */
33811
+ readonly online: Online
33812
+ /** Real-money purchases in the game's store app. */
33813
+ readonly purchases: Purchases
33814
+ /** Opens the game's privacy policy, as a game on the app stores must from inside itself: call it when the player
33815
+ * taps the game's own Privacy button. The page knows where the policy is; with none yet, nothing opens. */
33816
+ openPrivacy(): void
33539
33817
  /** Kept across restarts and levels, and between plays in the Game tab (in that
33540
33818
  * browser). A playtest starts with nothing saved. */
33541
33819
  save(key: string, value: Plain): void
@@ -48283,9 +48561,68 @@ function keyOf2(input) {
48283
48561
  const wheel = input.wheel === void 0 || input.wheel === 0 ? "" : `~${String(input.wheel)}`;
48284
48562
  const sticks = input.sticks === void 0 || input.sticks.length === 0 ? "" : `&${input.sticks.map((s2) => `${String(s2.lx)},${String(s2.ly)},${String(s2.rx)},${String(s2.ry)}`).join("/")}`;
48285
48563
  const touches = input.touches === void 0 || input.touches.length === 0 ? "" : `!${input.touches.map((t2) => `${String(t2.x)},${String(t2.y)}`).join(";")}`;
48564
+ const ready = readyKey(input.adsReady);
48565
+ const answer = input.adAnswer === void 0 ? "" : `$${ANSWER_LETTERS[input.adAnswer]}`;
48566
+ const online = onlineKey(input);
48286
48567
  const language = input.language === void 0 ? "" : `=${input.language}`;
48287
48568
  const typed = input.typed === void 0 || input.typed === "" ? "" : `"${escapeTyped(input.typed)}"`;
48288
- return `${letters}${own2}${aim}${screen}${size}${wheel}${sticks}${touches}${language}${typed}`;
48569
+ return `${letters}${own2}${aim}${screen}${size}${wheel}${sticks}${touches}${ready}${answer}${online}${language}${typed}`;
48570
+ }
48571
+ function onlineKey(input) {
48572
+ if (input.online === void 0 && input.onlineState === void 0 && input.purchases === void 0)
48573
+ return "";
48574
+ const json = JSON.stringify({
48575
+ ...input.online === void 0 ? {} : { h: input.online },
48576
+ ...input.onlineState === void 0 ? {} : { s: input.onlineState },
48577
+ ...input.purchases === void 0 ? {} : { p: input.purchases }
48578
+ });
48579
+ let text3 = "";
48580
+ for (const byte of new TextEncoder().encode(json))
48581
+ text3 += String.fromCharCode(byte);
48582
+ return `>${btoa(text3).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
48583
+ }
48584
+ function onlineOf(text3) {
48585
+ if (!/^[A-Za-z0-9_-]+$/.test(text3))
48586
+ return null;
48587
+ try {
48588
+ const padded = text3.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - text3.length % 4) % 4);
48589
+ const bytes = Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
48590
+ const read2 = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
48591
+ if (typeof read2 !== "object" || read2 === null || Array.isArray(read2))
48592
+ return null;
48593
+ const { h: h2, s: s2, p } = read2;
48594
+ if (h2 !== void 0 && (!Array.isArray(h2) || !h2.every(isOnlineHeard)))
48595
+ return null;
48596
+ if (s2 !== void 0 && !ONLINE_STATES.includes(s2))
48597
+ return null;
48598
+ if (p !== void 0 && (!Array.isArray(p) || !p.every(isPurchaseHeard)))
48599
+ return null;
48600
+ if (h2 === void 0 && s2 === void 0 && p === void 0)
48601
+ return null;
48602
+ return {
48603
+ ...h2 === void 0 ? {} : { online: h2 },
48604
+ ...s2 === void 0 ? {} : { onlineState: s2 },
48605
+ ...p === void 0 ? {} : { purchases: p }
48606
+ };
48607
+ } catch {
48608
+ return null;
48609
+ }
48610
+ }
48611
+ var ANSWER_LETTERS = { shown: "s", rewarded: "r", closed: "c", unavailable: "u" };
48612
+ var ANSWER_OF = { s: "shown", r: "rewarded", c: "closed", u: "unavailable" };
48613
+ var READY_LETTERS = [["b", "break"], ["w", "reward"]];
48614
+ function readyKey(ready) {
48615
+ if (ready === void 0)
48616
+ return "";
48617
+ return `#${READY_LETTERS.filter(([, kind]) => ready.includes(kind)).map(([letter]) => letter).join("")}`;
48618
+ }
48619
+ function answerOf(text3) {
48620
+ return text3.length === 1 ? ANSWER_OF[text3] ?? null : null;
48621
+ }
48622
+ function readyOf(text3) {
48623
+ if (![...text3].every((ch) => READY_LETTERS.some(([letter]) => letter === ch)))
48624
+ return null;
48625
+ return READY_LETTERS.filter(([letter]) => text3.includes(letter)).map(([, kind]) => kind);
48289
48626
  }
48290
48627
  var TYPED_ESCAPES = { "\\": "\\\\", '"': '\\"', " ": "\\s", "\b": "\\b", "\n": "\\n" };
48291
48628
  var UNESCAPED = { "\\": "\\", '"': '"', s: " ", b: "\b", n: "\n" };
@@ -48344,8 +48681,17 @@ function inputOf(run2) {
48344
48681
  const spoken = quote === -1 ? run2 : run2.slice(0, quote);
48345
48682
  const typed = quote === -1 ? null : typedOf(run2.slice(quote + 1, -1));
48346
48683
  const languageAt = spoken.indexOf("=");
48347
- const fingered = languageAt === -1 ? spoken : spoken.slice(0, languageAt);
48684
+ const heard = languageAt === -1 ? spoken : spoken.slice(0, languageAt);
48348
48685
  const language = languageAt === -1 ? null : spoken.slice(languageAt + 1);
48686
+ const onlineAt = heard.indexOf(">");
48687
+ const answered = onlineAt === -1 ? heard : heard.slice(0, onlineAt);
48688
+ const online = onlineAt === -1 ? null : onlineOf(heard.slice(onlineAt + 1));
48689
+ const answerAt = answered.indexOf("$");
48690
+ const readied = answerAt === -1 ? answered : answered.slice(0, answerAt);
48691
+ const answer = answerAt === -1 ? null : answerOf(answered.slice(answerAt + 1));
48692
+ const readyAt = readied.indexOf("#");
48693
+ const fingered = readyAt === -1 ? readied : readied.slice(0, readyAt);
48694
+ const ready = readyAt === -1 ? null : readyOf(readied.slice(readyAt + 1));
48349
48695
  const touchesAt = fingered.indexOf("!");
48350
48696
  const full = touchesAt === -1 ? fingered : fingered.slice(0, touchesAt);
48351
48697
  const touches = touchesAt === -1 ? null : touchesOf(fingered.slice(touchesAt + 1));
@@ -48369,6 +48715,9 @@ function inputOf(run2) {
48369
48715
  ...wheel === null ? {} : { wheel },
48370
48716
  ...sticks === null ? {} : { sticks },
48371
48717
  ...touches === null ? {} : { touches },
48718
+ ...ready === null ? {} : { adsReady: ready },
48719
+ ...answer === null ? {} : { adAnswer: answer },
48720
+ ...online ?? {},
48372
48721
  ...language === null ? {} : { language },
48373
48722
  ...typed === null ? {} : { typed }
48374
48723
  };
@@ -48599,13 +48948,43 @@ function scan(text3, chars) {
48599
48948
  }
48600
48949
  languageKey = `=${language}`;
48601
48950
  }
48602
- const touchesSign = rawFingered.indexOf("!");
48603
- const rawFull = touchesSign === -1 ? rawFingered : rawFingered.slice(0, touchesSign);
48951
+ const onlineSign = rawFingered.indexOf(">");
48952
+ const rawAnswered = onlineSign === -1 ? rawFingered : rawFingered.slice(0, onlineSign);
48953
+ let onlineKeyed = "";
48954
+ if (onlineSign !== -1) {
48955
+ const online = onlineOf(rawFingered.slice(onlineSign + 1));
48956
+ if (online === null) {
48957
+ throw new TrackError(`run ${index} has '${rawFingered.slice(onlineSign, onlineSign + 24)}': what a game heard online rides as > and the base64url of its JSON, as the page wrote it`, index, at2);
48958
+ }
48959
+ onlineKeyed = onlineKey(online);
48960
+ }
48961
+ const answerSign = rawAnswered.indexOf("$");
48962
+ const rawReadied = answerSign === -1 ? rawAnswered : rawAnswered.slice(0, answerSign);
48963
+ let answerKey = "";
48964
+ if (answerSign !== -1) {
48965
+ const answer = answerOf(rawAnswered.slice(answerSign + 1));
48966
+ if (answer === null) {
48967
+ throw new TrackError(`run ${index} has '${rawAnswered.slice(answerSign)}': an ad's answer is $s (shown), $r (rewarded), $c (closed) or $u (unavailable)`, index, at2);
48968
+ }
48969
+ answerKey = `$${ANSWER_LETTERS[answer]}`;
48970
+ }
48971
+ const readySign = rawReadied.indexOf("#");
48972
+ const rawTouched = readySign === -1 ? rawReadied : rawReadied.slice(0, readySign);
48973
+ let readyKeyed = "";
48974
+ if (readySign !== -1) {
48975
+ const ready = readyOf(rawReadied.slice(readySign + 1));
48976
+ if (ready === null) {
48977
+ throw new TrackError(`run ${index} has '${rawReadied.slice(readySign)}': ads ready are # with b (break) and w (reward), or # alone for none, like #bw`, index, at2);
48978
+ }
48979
+ readyKeyed = readyKey(ready);
48980
+ }
48981
+ const touchesSign = rawTouched.indexOf("!");
48982
+ const rawFull = touchesSign === -1 ? rawTouched : rawTouched.slice(0, touchesSign);
48604
48983
  let touchesKey = "";
48605
48984
  if (touchesSign !== -1) {
48606
- const touches = touchesOf(rawFingered.slice(touchesSign + 1));
48985
+ const touches = touchesOf(rawTouched.slice(touchesSign + 1));
48607
48986
  if (touches === null) {
48608
- throw new TrackError(`run ${index} 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`, index, at2);
48987
+ throw new TrackError(`run ${index} 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`, index, at2);
48609
48988
  }
48610
48989
  touchesKey = `!${touches.map((t2) => `${String(t2.x)},${String(t2.y)}`).join(";")}`;
48611
48990
  }
@@ -48691,7 +49070,7 @@ function scan(text3, chars) {
48691
49070
  aimKey = `@${String(aim.x / AIM_PER_TILE)},${String(aim.y / AIM_PER_TILE)}`;
48692
49071
  }
48693
49072
  const lettered = rawButtons.toLowerCase().replace(/[+|]/g, "");
48694
- const buttons = lettered === "" && (ownKey !== "" || aimKey !== "" || pointKey !== "" || sizeKey !== "" || wheelKey !== "" || sticksKey !== "" || touchesKey !== "" || languageKey !== "" || typedKey !== "") ? "-" : lettered;
49073
+ const buttons = lettered === "" && (ownKey !== "" || aimKey !== "" || pointKey !== "" || sizeKey !== "" || wheelKey !== "" || sticksKey !== "" || touchesKey !== "" || readyKeyed !== "" || answerKey !== "" || onlineKeyed !== "" || languageKey !== "" || typedKey !== "") ? "-" : lettered;
48695
49074
  if (buttons === "") {
48696
49075
  throw new TrackError(`run ${index} has a count but no buttons: '${token}'`, index, at2);
48697
49076
  }
@@ -48706,7 +49085,7 @@ function scan(text3, chars) {
48706
49085
  }
48707
49086
  key = keyOf2(inputOf(buttons));
48708
49087
  }
48709
- key += ownKey + aimKey + pointKey + sizeKey + wheelKey + sticksKey + touchesKey + languageKey + typedKey;
49088
+ key += ownKey + aimKey + pointKey + sizeKey + wheelKey + sticksKey + touchesKey + readyKeyed + answerKey + onlineKeyed + languageKey + typedKey;
48710
49089
  if (!/^[1-9][0-9]*$/.test(rawCount)) {
48711
49090
  throw new TrackError(`run ${index} has the count '${rawCount}', which must be a whole number from 1 to ${MAX_RUN_COUNT} with no leading zero`, index, at2);
48712
49091
  }
@@ -50231,9 +50610,10 @@ function pressedMeanwhile(input, latched) {
50231
50610
  function quiet(scripts) {
50232
50611
  if (scripts === null)
50233
50612
  return null;
50234
- if (scripts.errors.length === 0 && scripts.warnings.length === 0 && scripts.logs.length === 0 && scripts.sounds.length === 0)
50613
+ 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)
50235
50614
  return scripts;
50236
- return { ...scripts, errors: [], warnings: [], logs: [], sounds: [] };
50615
+ const { privacy: _privacy, ad: _ad, online: _online, purchase: _purchase, ...rest } = scripts;
50616
+ return { ...rest, errors: [], warnings: [], logs: [], sounds: [] };
50237
50617
  }
50238
50618
  function heldTick(state, input) {
50239
50619
  const scripts = quiet(state.scripts);
@@ -50469,6 +50849,7 @@ function ownBodyWith(was, args) {
50469
50849
  var LOGS_PER_TICK = 50;
50470
50850
  var LOG_CHARS = 500;
50471
50851
  var EVENTS_PER_TICK = 256;
50852
+ var AD_EVENTS_PER_TICK = 16;
50472
50853
  var TIMERS_PER_OWNER = 64;
50473
50854
  var TWEENS_PER_OWNER = 64;
50474
50855
  var SEQUENCES_PER_OWNER = 16;
@@ -50483,7 +50864,7 @@ var OWN_AUDIO = /^own:[a-z0-9][a-z0-9-]{0,39}$/;
50483
50864
  var LIB_AUDIO = /^lib:[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/;
50484
50865
  var MOST_BURSTS = 64;
50485
50866
  var MOST_DEFERRED = 1024;
50486
- 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"]);
50867
+ 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"]);
50487
50868
  function screenOf2(input) {
50488
50869
  const size = input.screenSize ?? SCREEN_DEFAULT;
50489
50870
  return { screen: input.screenPointer ?? null, size: { width: size.w, height: size.h } };
@@ -50552,7 +50933,7 @@ function fromUnit(raw, unit, what) {
50552
50933
  return fromInt(Math.round(value));
50553
50934
  }
50554
50935
  }
50555
- 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"]);
50936
+ 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"]);
50556
50937
  function sidesOf(normal) {
50557
50938
  switch (normal) {
50558
50939
  case "a_above_b":
@@ -50664,6 +51045,20 @@ var Turn = class {
50664
51045
  language;
50665
51046
  camera;
50666
51047
  sounds;
51048
+ /** game.openPrivacy() was called this tick (the store publishing spec). */
51049
+ privacy;
51050
+ /** The ad asked this tick, and what the game keeps of its ads, absent until it has any (the ads spec). */
51051
+ ad;
51052
+ ads;
51053
+ /** What the scripts asked of the game's server this tick; whether it is online; the next call's number, absent for a
51054
+ * game that never mentions online (the online services spec). */
51055
+ online;
51056
+ onlineState;
51057
+ nextCall;
51058
+ /** What the scripts asked of the store this tick, and what the game keeps of its purchases, absent for a game that
51059
+ * never mentions them (the online services spec, part 3). */
51060
+ purchase;
51061
+ purchases;
50667
51062
  music;
50668
51063
  /** Sounds playing round, and the mixer, replaced whole when changed
50669
51064
  * (sub-project 9). */
@@ -50724,6 +51119,14 @@ var Turn = class {
50724
51119
  this.saved = scripts.saved;
50725
51120
  this.camera = scripts.camera;
50726
51121
  this.sounds = fresh ? [] : [...scripts.sounds];
51122
+ this.privacy = !fresh && scripts.privacy === true;
51123
+ this.ad = fresh ? void 0 : scripts.ad;
51124
+ this.ads = scripts.ads;
51125
+ this.online = fresh || scripts.online === void 0 ? void 0 : [...scripts.online];
51126
+ this.onlineState = scripts.onlineState;
51127
+ this.nextCall = scripts.nextCall;
51128
+ this.purchase = fresh ? void 0 : scripts.purchase;
51129
+ this.purchases = scripts.purchases;
50727
51130
  this.music = scripts.music;
50728
51131
  this.loops = scripts.loops;
50729
51132
  this.mixer = scripts.mixer;
@@ -50869,6 +51272,14 @@ var Turn = class {
50869
51272
  logs: this.logs.length === 0 ? [] : [...this.logs],
50870
51273
  warnings: this.warnings.length,
50871
51274
  sounds: this.sounds.length,
51275
+ privacy: this.privacy,
51276
+ ad: this.ad,
51277
+ ads: this.ads,
51278
+ online: this.online === void 0 ? void 0 : [...this.online],
51279
+ onlineState: this.onlineState,
51280
+ nextCall: this.nextCall,
51281
+ purchase: this.purchase,
51282
+ purchases: this.purchases,
50872
51283
  bursts: this.bursts.length === 0 ? [] : [...this.bursts],
50873
51284
  screen: this.screen,
50874
51285
  waiting: this.waiting,
@@ -50922,6 +51333,14 @@ var Turn = class {
50922
51333
  this.logs.splice(0, this.logs.length, ...was.logs);
50923
51334
  this.warnings.length = was.warnings;
50924
51335
  this.sounds.length = was.sounds;
51336
+ this.privacy = was.privacy;
51337
+ this.ad = was.ad;
51338
+ this.ads = was.ads;
51339
+ this.online = was.online;
51340
+ this.onlineState = was.onlineState;
51341
+ this.nextCall = was.nextCall;
51342
+ this.purchase = was.purchase;
51343
+ this.purchases = was.purchases;
50925
51344
  this.bursts = was.bursts;
50926
51345
  this.screen = was.screen;
50927
51346
  this.waiting = was.waiting;
@@ -52193,6 +52612,76 @@ var Turn = class {
52193
52612
  // kind's script to read; null for a game without one.
52194
52613
  case "gameScript":
52195
52614
  return this.scripts.sources.game === null ? null : this.game ?? {};
52615
+ // The game's privacy policy (the store publishing spec): asked of the page, which knows where it is.
52616
+ // Ads (the ads spec): asked of the page, which answers on a later tick, as input.
52617
+ // Online (the online services spec): asked of the page, which carries it to the game's server; what comes back
52618
+ // is heard on a later tick, as input.
52619
+ case "onlineCall": {
52620
+ const id = this.nextCall ?? 1;
52621
+ this.askOnline({ kind: "call", id, name: args.name, data: args.data ?? null });
52622
+ this.nextCall = id + 1;
52623
+ return id;
52624
+ }
52625
+ case "onlineJoin":
52626
+ case "onlineLeave":
52627
+ this.askOnline({ kind: op === "onlineJoin" ? "join" : "leave", room: args.room });
52628
+ return null;
52629
+ case "onlineSend":
52630
+ this.askOnline({ kind: "send", room: args.room, data: args.data ?? null });
52631
+ return null;
52632
+ // Part 2: an upload through one of the game's calls, a download, and asking for push; numbered with the calls.
52633
+ case "onlineUpload":
52634
+ case "onlineDownload": {
52635
+ const id = this.nextCall ?? 1;
52636
+ 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 } });
52637
+ this.nextCall = id + 1;
52638
+ return id;
52639
+ }
52640
+ case "onlineAskPush":
52641
+ this.askOnline({ kind: "askPush" });
52642
+ return null;
52643
+ case "onlineState":
52644
+ return this.onlineState ?? "off";
52645
+ // Purchases (the online services spec, part 3): asked of the page, which asks the store; the answer is heard on a
52646
+ // later tick, as input.
52647
+ case "purchaseBuy":
52648
+ this.askPurchase({ kind: "buy", id: args.id });
52649
+ return null;
52650
+ case "purchaseRestore":
52651
+ this.askPurchase({ kind: "restore" });
52652
+ return null;
52653
+ case "purchaseProducts":
52654
+ return (this.purchases ?? NO_PURCHASES_YET).products;
52655
+ case "adBreak":
52656
+ case "adReward": {
52657
+ const placement = args.placement;
52658
+ if (typeof placement !== "string" || !PLACEMENT.test(placement))
52659
+ throw new Error(`${op === "adBreak" ? "ads.break" : "ads.reward"} was given ${JSON.stringify(placement)}: ${PLACEMENT_WORDS}`);
52660
+ this.askAd({ kind: op === "adBreak" ? "break" : "reward", placement });
52661
+ return null;
52662
+ }
52663
+ case "adReady": {
52664
+ const kind = args.kind;
52665
+ if (!AD_KINDS.includes(kind))
52666
+ throw new Error(`ads.ready takes 'break' or 'reward', not ${JSON.stringify(kind)}`);
52667
+ return this.scripts.adsOff === void 0 && adsOf(this.ads).ready.includes(kind);
52668
+ }
52669
+ case "adBanner": {
52670
+ const position = args.position;
52671
+ if (position !== null && position !== "top" && position !== "bottom")
52672
+ throw new Error(`ads.banner takes 'top', 'bottom' or null, not ${JSON.stringify(position)}`);
52673
+ if (position !== null && this.scripts.adsOff !== void 0) {
52674
+ this.warn(this.scripts.adsOff);
52675
+ return null;
52676
+ }
52677
+ this.ads = { ...adsOf(this.ads), banner: position };
52678
+ if (position !== null && this.ui !== void 0)
52679
+ this.bannerBeside(position, this.ui);
52680
+ return null;
52681
+ }
52682
+ case "openPrivacy":
52683
+ this.privacy = true;
52684
+ return null;
52196
52685
  case "save": {
52197
52686
  this.saved = { ...this.saved, [text(args.key, "save: key")]: args.value ?? null };
52198
52687
  return null;
@@ -52773,8 +53262,132 @@ var Turn = class {
52773
53262
  this.setDriven(id, { ...this.driven.get(id) ?? { x: false, y: false }, facing: true });
52774
53263
  return this.row(e2);
52775
53264
  }
53265
+ /** An ad asked of the page for this tick, and kept until its answer comes (the ads spec); refused at once, in the
53266
+ * words of the rule it breaks, when it breaks one the engine can tell exactly, and warned of when a break comes in
53267
+ * the middle of play. */
53268
+ askAd(ask) {
53269
+ const ads = adsOf(this.ads);
53270
+ const refused = adRefusal(ads, ask.kind, this.real, this.scripts.adsOff ?? null);
53271
+ if (refused !== null) {
53272
+ this.warn(refused);
53273
+ this.hearAd({ kind: ask.kind, placement: ask.placement, result: "refused", reason: refused });
53274
+ return;
53275
+ }
53276
+ if (ask.kind === "break" && !this.clock.paused) {
53277
+ const playing2 = playHeld(this.input) || ads.lastPlay !== null && this.real - ads.lastPlay <= PLAY_PRESS_TICKS;
53278
+ const settled = ads.scene === null || this.real - ads.scene.at > SCENE_SETTLE_TICKS;
53279
+ if (playing2 && settled)
53280
+ this.warn(AD_WORDS.midPlay);
53281
+ }
53282
+ this.ad = ask;
53283
+ this.ads = { ...ads, pending: ask };
53284
+ }
53285
+ /** A banner beside one of the game's own buttons, warned of: a button within a few pixels of the banner's edge. */
53286
+ bannerBeside(position, ui) {
53287
+ const screen = screenOf2(this.input).size;
53288
+ for (const [name, box] of layoutUi(ui, screen)) {
53289
+ if (ui.elements[name]?.kind !== "button")
53290
+ continue;
53291
+ const beside = position === "top" ? box.y <= BANNER_EDGE_PX : box.y + box.h >= screen.height - BANNER_EDGE_PX;
53292
+ if (beside)
53293
+ this.warn(AD_WORDS.banner(name));
53294
+ }
53295
+ }
53296
+ /** What the tick's input says of ads: the kinds ready, and the page's answer to the ad pending, heard as an 'ad'
53297
+ * event; an answer with none pending is let go (the ads spec). */
53298
+ takeAdInput() {
53299
+ const { adAnswer, adsReady } = this.input;
53300
+ if (this.ads !== void 0) {
53301
+ const before2 = this.state.prevInput;
53302
+ const pressed = anyPressed(this.input, before2) || (this.ui?.clicked.length ?? 0) > 0;
53303
+ const played = playPressed(this.input, before2);
53304
+ const key = `${String(this.state.level)}:${String(this.scripts.loads)}`;
53305
+ const scene = this.ads.scene?.key === key ? this.ads.scene : { key, at: this.real };
53306
+ if (pressed || played || scene !== this.ads.scene) {
53307
+ this.ads = {
53308
+ ...this.ads,
53309
+ scene,
53310
+ ...pressed ? { played: true, lastPress: this.real } : {},
53311
+ ...played ? { lastPlay: this.real } : {}
53312
+ };
53313
+ }
53314
+ }
53315
+ if (adsReady !== void 0)
53316
+ this.ads = { ...adsOf(this.ads), ready: AD_KINDS.filter((kind) => adsReady.includes(kind)) };
53317
+ const pending = this.ads?.pending ?? null;
53318
+ if (adAnswer === void 0 || pending === null)
53319
+ return;
53320
+ this.ads = { ...adsOf(this.ads), pending: null, ...pending.kind === "break" && adAnswer === "shown" ? { lastBreak: this.real } : {} };
53321
+ this.hearAd({ kind: pending.kind, placement: pending.placement, result: adAnswer });
53322
+ }
53323
+ /** An ask of the game's server, refused in words when it is not one, or past a tick's asks. */
53324
+ askOnline(ask) {
53325
+ const problem = askProblem(ask);
53326
+ if (problem !== null)
53327
+ throw new Error(problem);
53328
+ const asked = this.online ?? [];
53329
+ if (asked.length >= ONLINE_LIMITS.asksPerTick)
53330
+ throw new Error(TOO_MANY_ASKS);
53331
+ this.online = [...asked, ask];
53332
+ }
53333
+ /** What the page brought back from the game's server, and whether it is online, for a game that keeps online: each
53334
+ * heard as an 'online' event. A game that never mentions online ignores it. */
53335
+ takeOnlineInput() {
53336
+ if (this.nextCall === void 0)
53337
+ return;
53338
+ const { online, onlineState } = this.input;
53339
+ if (onlineState !== void 0)
53340
+ this.onlineState = onlineState;
53341
+ for (const heard of online ?? [])
53342
+ this.hearHost("online", heard);
53343
+ }
53344
+ /** 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
53345
+ * buy while another waits for its answer heard at once as refused. */
53346
+ askPurchase(ask) {
53347
+ const problem = purchaseAskProblem(ask);
53348
+ if (problem !== null)
53349
+ throw new Error(problem);
53350
+ if (this.purchase !== void 0)
53351
+ throw new Error("game.purchases asks the store once a tick: ask again on a later tick");
53352
+ const purchases = this.purchases ?? NO_PURCHASES_YET;
53353
+ if (ask.kind === "buy" && purchases.pending !== null) {
53354
+ this.hearHost("purchase", { kind: "bought", id: ask.id, result: "refused", reason: "a purchase is already in progress: wait for its answer" });
53355
+ return;
53356
+ }
53357
+ this.purchase = ask;
53358
+ if (ask.kind === "buy")
53359
+ this.purchases = { ...purchases, pending: ask.id };
53360
+ }
53361
+ /** What the store and the game's server answered of purchases, for a game that keeps them: the products kept, and each
53362
+ * heard as a 'purchase' event; the buy pending let go once its answer comes. A game that never mentions purchases
53363
+ * ignores it. */
53364
+ takePurchaseInput() {
53365
+ if (this.purchases === void 0)
53366
+ return;
53367
+ for (const heard of this.input.purchases ?? []) {
53368
+ if (heard.kind === "products")
53369
+ this.purchases = { ...this.purchases, products: heard.list };
53370
+ if (heard.kind === "bought" && heard.id === this.purchases.pending)
53371
+ this.purchases = { ...this.purchases, pending: null };
53372
+ this.hearHost("purchase", heard);
53373
+ }
53374
+ }
53375
+ /** An 'ad' event every onEvent hears on the next tick, as an emitted one is. */
53376
+ hearAd(data) {
53377
+ this.hearHost("ad", data);
53378
+ }
53379
+ /** An event from the page (an ad's answer, what the game heard online), heard past a tick's emitted events, up to its
53380
+ * own few (the final review of ads). */
53381
+ hearHost(name, data) {
53382
+ if (this.emitted.length >= EVENTS_PER_TICK + AD_EVENTS_PER_TICK)
53383
+ return;
53384
+ if (this.emitted.length >= EVENTS_PER_TICK && this.emitted.filter((one2) => one2.name === name).length >= AD_EVENTS_PER_TICK)
53385
+ return;
53386
+ this.emitted.push({ name, data: { ...data }, at: this.events.length });
53387
+ this.events.push(name);
53388
+ }
52776
53389
  scriptsState() {
52777
- const { starting: _was, loops: _loops, mixer: _mixer, typing: _typing, ui: _ui, ...before2 } = this.scripts;
53390
+ 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;
52778
53391
  return {
52779
53392
  ...before2,
52780
53393
  fields: this.fields,
@@ -52800,6 +53413,14 @@ var Turn = class {
52800
53413
  ...this.mixer === void 0 ? {} : { mixer: this.mixer },
52801
53414
  camera: this.camera,
52802
53415
  sounds: this.sounds,
53416
+ ...this.privacy ? { privacy: true } : {},
53417
+ ...this.ad === void 0 ? {} : { ad: this.ad },
53418
+ ...this.ads === void 0 ? {} : { ads: this.ads },
53419
+ ...this.online === void 0 || this.online.length === 0 ? {} : { online: this.online },
53420
+ ...this.onlineState === void 0 ? {} : { onlineState: this.onlineState },
53421
+ ...this.nextCall === void 0 ? {} : { nextCall: this.nextCall },
53422
+ ...this.purchase === void 0 ? {} : { purchase: this.purchase },
53423
+ ...this.purchases === void 0 ? {} : { purchases: this.purchases },
52803
53424
  music: this.music,
52804
53425
  bursts: this.bursts,
52805
53426
  deferred: this.deferred,
@@ -52867,6 +53488,9 @@ function beginScripts(state, input) {
52867
53488
  if (scripts === null || !hasScripts(scripts))
52868
53489
  return null;
52869
53490
  const turn = new Turn({ ...state, events: [] }, scripts, input, state.contacts, state.step, true, scripts.driven);
53491
+ turn.takeAdInput();
53492
+ turn.takeOnlineInput();
53493
+ turn.takePurchaseInput();
52870
53494
  turn.bursts = turn.bursts.filter((b) => turn.now - b.step < b.life * 60);
52871
53495
  dueChange(turn);
52872
53496
  for (const late of scripts.deferred) {
@@ -52971,6 +53595,9 @@ function pausedScripts(state, input) {
52971
53595
  if (scripts === null)
52972
53596
  throw new Error("a game without scripts cannot be paused");
52973
53597
  const turn = new Turn({ ...state, events: [] }, scripts, input, state.contacts, state.step, true, scripts.driven);
53598
+ turn.takeAdInput();
53599
+ turn.takeOnlineInput();
53600
+ turn.takePurchaseInput();
52974
53601
  dueChange(turn);
52975
53602
  turn.call(null, "whilePaused", [DT]);
52976
53603
  if (turn.clock.paused && (turn.paused.includes(0) || !(turn.runtime.has(null) && turn.runtime.hasHook(null, "whilePaused")))) {
@@ -52979,7 +53606,8 @@ function pausedScripts(state, input) {
52979
53606
  }
52980
53607
  heardClicks(turn);
52981
53608
  const heard = closeScripts(turn.scriptsState(), turn.events).inbox;
52982
- const result = turn.result([...scripts.inbox, ...heard].slice(0, EVENTS_PER_TICK));
53609
+ const all = [...scripts.inbox, ...heard];
53610
+ 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)]);
52983
53611
  const held = result.state.scripts;
52984
53612
  if (held === null)
52985
53613
  return result;