@mpgd/cli 0.47.0 → 0.49.0

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.
Files changed (59) hide show
  1. package/dist/command-launcher.d.ts +13 -0
  2. package/dist/command-launcher.js +24 -0
  3. package/dist/devvit-output-collisions.d.ts +49 -0
  4. package/dist/devvit-output-collisions.js +120 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +186 -10
  7. package/dist/microsoft-store-submission.js +11 -6
  8. package/dist/native-build-info.json +2 -2
  9. package/dist/native-build-target.js +2134 -351
  10. package/dist/tooling-migration.d.ts +24 -0
  11. package/dist/tooling-migration.js +497 -0
  12. package/dist/windows-host/Core/BridgeCommand.cs +17 -0
  13. package/dist/windows-host/Core/Mpgd.MicrosoftStore.Host.csproj +9 -0
  14. package/dist/windows-host/Core/NativeBridgeContracts.cs +33 -0
  15. package/dist/windows-host/Core/NativeBridgeHost.cs +162 -0
  16. package/dist/windows-host/Core/NativeBridgeOrigin.cs +19 -0
  17. package/dist/windows-host/Core/NativeBridgeProtocol.cs +335 -0
  18. package/dist/windows-host/Core/NativeBridgeRequestGate.cs +76 -0
  19. package/dist/windows-host/Core/NativeBridgeResult.cs +65 -0
  20. package/dist/windows-host/README.md +118 -0
  21. package/dist/windows-host/WinUI/DispatcherQueueBridgeDispatcher.cs +39 -0
  22. package/dist/windows-host/WinUI/WebView2DocumentTransport.cs +46 -0
  23. package/dist/windows-host/WinUI/WebView2NativeBridge.cs +185 -0
  24. package/dist/windows-host/WinUI/WindowStoreContext.cs +20 -0
  25. package/dist/windows-native-tools.d.ts +4 -0
  26. package/dist/windows-native-tools.js +21 -0
  27. package/native-tools/windows-msix/WindowsMsixAcceptance.psm1 +333 -0
  28. package/package.json +30 -18
  29. package/templates/lite-overlay/.agents/skills/use-mpgd-kit/SKILL.md +22 -0
  30. package/templates/lite-overlay/AGENTS.md +32 -0
  31. package/templates/lite-overlay/README.md +37 -0
  32. package/templates/lite-overlay/agent/acceptance.md +27 -0
  33. package/templates/lite-overlay/agent/brief.md +17 -0
  34. package/templates/lite-overlay/apps/target-devvit/README.md +134 -0
  35. package/templates/lite-overlay/docs/MPGD_KIT_WORKFLOWS.md +37 -0
  36. package/templates/lite-overlay/src/lite/tapGame.ts +77 -0
  37. package/templates/lite-overlay/src/platform/devvitEntrypoint.ts +116 -0
  38. package/templates/lite-overlay/src/platform/devvitInlineMode.css +146 -0
  39. package/templates/lite-overlay/src/runtime/createGame.ts +57 -0
  40. package/templates/phaser-game/AGENTS.md +5 -0
  41. package/templates/phaser-game/README.md +5 -3
  42. package/templates/phaser-game/apps/target-ait/package.json +4 -4
  43. package/templates/phaser-game/apps/target-ait/src/main.ts +5 -5
  44. package/templates/phaser-game/apps/target-cloudflare-pages/package.json +2 -2
  45. package/templates/phaser-game/apps/target-devvit/package.json +2 -2
  46. package/templates/phaser-game/apps/target-devvit/src/server/bridge.ts +137 -18
  47. package/templates/phaser-game/apps/target-devvit/vite.config.ts +2 -0
  48. package/templates/phaser-game/package.json +8 -8
  49. package/templates/phaser-game/pnpm-workspace.yaml +2 -0
  50. package/templates/phaser-game/src/env.d.ts +2 -0
  51. package/templates/phaser-game/src/platform/buildGateways/ait.ts +1 -1
  52. package/templates/phaser-game/src/platform/buildGateways/aitSandbox.ts +9 -4
  53. package/templates/phaser-game/src/platform/buildGateways/microsoftStore.ts +2 -5
  54. package/templates/phaser-game/src/platform/buildGateways/reddit.ts +1 -1
  55. package/templates/phaser-game/src/platform/buildGateways/verse8.ts +1 -2
  56. package/templates/phaser-game/src/platform/verse8Agent8Client.ts +5 -6
  57. package/templates/phaser-game/tools/{run-game-acceptance.mjs → run-game-acceptance.ts} +1 -8
  58. package/templates/phaser-game/tsconfig.tools.json +12 -0
  59. package/templates/phaser-game/vite.shared.ts +40 -1
@@ -0,0 +1,134 @@
1
+ # __GAME_TITLE__ Devvit Target
2
+
3
+ This directory is the Reddit Devvit app root for `__GAME_NAME__`.
4
+
5
+ `devvit.json` belongs to the generated game, not to the mpgd-kit checkout. That keeps the
6
+ Reddit app name, menu entry, upload history, and playtest state owned by this game.
7
+ Its `name` is generated as a Devvit-safe slug; edit it before `pnpm devvit:init`
8
+ if you want a different Reddit app name.
9
+
10
+ ## First Playtest
11
+
12
+ ```sh
13
+ pnpm install -w
14
+ pnpm devvit:login
15
+ pnpm devvit:whoami
16
+ pnpm devvit:init
17
+ pnpm build:devvit
18
+ pnpm devvit:playtest
19
+ ```
20
+
21
+ `devvit:init` performs the first App Directory upload/link step with Devvit's
22
+ logged-in account. It may open a browser for authorization. Use
23
+ `pnpm devvit:init:copy-paste` from the game root if the browser flow is not
24
+ available and the CLI asks for a manual code. After that, use
25
+ `pnpm devvit:playtest`, `pnpm devvit:upload`, and `pnpm devvit:publish` from the
26
+ game root.
27
+
28
+ The official `@devvit/start/vite` plugin builds the game client into
29
+ `dist/client` and the CommonJS server bridge into `dist/server/index.cjs` in one
30
+ pass. The mpgd target build wraps that unified build with release provenance and
31
+ effective-target evidence while keeping Devvit SDK imports behind the platform gateway.
32
+ The game root pins `@mpgd/cli`, so these commands use the same CLI version as the
33
+ generated starter's other `@mpgd/*` dependencies.
34
+
35
+ The bridge endpoint at `/api/mpgd/rpc` uses the direct oRPC Node HTTP adapter
36
+ from `@mpgd/bridge/orpc/node`; Express, Hono, and Fetch request conversion are
37
+ not required. Devvit-owned menu, scheduler, trigger, and form callbacks remain
38
+ thin `/internal/...` routes and delegate to shared service functions. oRPC
39
+ Publisher helpers can broadcast results after a task completes, but they do not
40
+ replace the `devvit.json` scheduler endpoint and should not use process-local
41
+ memory when delivery must cross instances. The generated target exposes only
42
+ the oRPC bridge route; the former JSON compatibility route is not generated.
43
+
44
+ The default post entry uses `index.html` for a playable Canvas2D surface.
45
+ Play full screen requests the `game` entrypoint, which loads the full game from
46
+ the separate `game.html` document. Keep heavy imports behind the expanded entry
47
+ and preserve native feed scrolling in the inline surface.
48
+
49
+ Games that intentionally support inline gameplay can opt in through
50
+ `startDevvitWebView`; the generated default uses `startDevvitPreviewWebView`.
51
+ An opt-in inline surface must remain tap/click based and preserve Reddit-native
52
+ gestures across its complete gameplay flow.
53
+
54
+ The bridge method handlers live in `src/server/bridge.ts`; `src/server/index.ts`
55
+ only wires the Devvit `context`, `reddit`, and `redis` clients into them. Cloud
56
+ saves are bound to the authenticated Reddit user from the request context and
57
+ stored under `<game-name>:save:<user>:<key>`. Each value is capped at
58
+ `maxStorageValueBytes` and each user may hold at most `maxStorageKeysPerPlayer`
59
+ distinct keys, tracked in the `<game-name>:save-keys:<user>` hash and enforced
60
+ inside a WATCH/MULTI/EXEC transaction so concurrent saves cannot overshoot the
61
+ cap; saves past it fail with `DEVVIT_STORAGE_KEY_LIMIT`. Saves do not expire by
62
+ default; set `storageEntryTtlSeconds` to let idle saves age out. Keep
63
+ `bridge.ts` aligned with the kit's `apps/target-devvit/src/server/bridge.ts`
64
+ when upgrading so generated games inherit storage protections.
65
+
66
+ The generated bridge does not advertise or accept a generic platform
67
+ leaderboard. Devvit ranking should be owned by a server completion handler that
68
+ validates the game-specific attempt, records it through the verified leaderboard
69
+ provider from `@mpgd/adapter-devvit/server`, and exposes only an authenticated
70
+ game-scoped snapshot route. Do not enable `nativeLeaderboard` or trust a raw
71
+ client score submission for that flow. Keep `remoteLeaderboard` disabled too;
72
+ the verified completion route is not the generic `PlatformGateway` leaderboard.
73
+
74
+ `devvit playtest` runs the official unified Vite build in watch mode, so a
75
+ separate client/server watcher or staging prebuild is not required.
76
+
77
+ ## Durable Post Operations
78
+
79
+ Use `src/server/postOperationStore.ts` with `@mpgd/adapter-devvit/server` for a
80
+ server action or scheduler that may receive the same custom-post operation more
81
+ than once. Use `{ appScope, subredditId }` as the scope, a stable `operationType`
82
+ in the definition, and an `operationId` for each logical publication slot. Validate
83
+ the public payload and launch parameters, then inject the Redis-backed store into
84
+ `createDevvitPostOperationCoordinator`.
85
+
86
+ The contract is duplicate-safe and ambiguity-safe, not exactly-once. It persists
87
+ the attempt before invoking Reddit. If Reddit accepts the post but its response is
88
+ lost, or the process stops before the receipt is saved, repeating `execute` returns
89
+ `reconciliation-required` and never blindly calls the submit function again. An
90
+ explicit recovery endpoint or scheduler calls `reconcile`, which requires an exact
91
+ match of the full canonical envelope. A missing match from a bounded listing
92
+ remains unresolved rather than restoring submit permission.
93
+
94
+ The generated Redis wrapper implements `DevvitIndexedDurableOperationStore`.
95
+ Use `listPending()` with a small limit and its scope-bound continuation cursor to
96
+ discover prepared, attempted, and terminal work without scanning Redis keys.
97
+ Listing is read-only; prepared work still requires an explicit `execute` decision,
98
+ attempted work must use `reconcile`, and terminal ambiguity remains fail-closed.
99
+
100
+ Keep operation definitions and `@devvit/web/server` imports inside this target
101
+ app. The canonical `{ mpgd, launch, payload }` envelope is public and untrusted;
102
+ keep private content and authoritative records in server-side storage. Exercise
103
+ first success, duplicate calls, interruption,
104
+ response loss, lease expiry with concurrent reconciliation, malformed durable
105
+ state, invalid launch metadata, and cross-scope key isolation before enabling a
106
+ scheduled or user-triggered publication flow.
107
+
108
+ ## AI-assisted Devvit development
109
+
110
+ Devvit provides an optional MCP server for targeted documentation search and
111
+ deployed-app log inspection. See the official
112
+ [Devvit AI Tools guide](https://developers.reddit.com/docs/guides/ai), then
113
+ register it once in the developer's global Codex configuration:
114
+
115
+ ```sh
116
+ codex mcp add devvit -- mise --no-config exec node@22 -- npx -y @devvit/mcp@0.0.27
117
+ ```
118
+
119
+ The server needs Node 22: `@devvit/mcp` 0.0.27 depends on `better-sqlite3`
120
+ 11.9.1, which ships prebuilt binaries for Node 22 but not 24 and does not
121
+ build on 26. `--no-config` keeps mise from passing your mise-managed
122
+ environment to the server. Without mise, register `npx -y @devvit/mcp@0.0.27`
123
+ only if the MCP client's `PATH` resolves `npx` from Node 22. Running
124
+ `codex mcp add devvit` again replaces an older registration.
125
+
126
+ Restart Codex or begin a new task after registering it. Use `devvit_search`
127
+ before broad Devvit documentation exploration. Use the experimental
128
+ `devvit_logs` tool for a deployed app and subreddit when diagnosing production
129
+ or playtest behavior, and confirm its findings with local tests and
130
+ `pnpm devvit:playtest`.
131
+
132
+ The MCP server is an agent-side aid only. It is not required by the generated
133
+ game build, must not be bundled into target artifacts, and does not replace
134
+ Devvit CLI authentication or release verification.
@@ -0,0 +1,37 @@
1
+ # mpgd-kit Lite Workflows
2
+
3
+ Start with `agent/game-manifest.json`; enabled targets are build paths and future targets are design directions.
4
+
5
+ | Concern | Owner |
6
+ | --- | --- |
7
+ | Deterministic rules and state | `src/game` |
8
+ | Canvas2D rendering and input | `src/lite` through `@mpgd/lite-shell` |
9
+ | Renderer mounting and teardown | `src/runtime/createGame.ts` |
10
+ | Platform services | `src/platform` through `PlatformGateway` |
11
+ | Target build configuration | `mpgd.targets.json` and `@mpgd/target-config` |
12
+ | Runtime, recovery, and audio ownership | bootstrap and `@mpgd/game-runtime` |
13
+ | Acceptance | `agent/acceptance.md` and `pnpm accept` |
14
+
15
+ Use the generated icon and localization commands. Load browser images and fonts directly or through lite-shell font helpers; this template has no Phaser asset manifest.
16
+ Keep deterministic state independent of Canvas2D, DOM, network, and SDKs.
17
+ Reserve safe-area geometry once through CSS or viewport content bounds.
18
+ Never call SDKs from renderer code or grant purchases/rewards from client callbacks.
19
+
20
+ ## Platform integration
21
+
22
+ Choose the owning adapter in `src/platform/buildGateways`. Configure target features and service availability rather than bypassing them in game code.
23
+ Keep commerce verification and grants in the backend ledger; use authenticated sessions and durable encrypted recovery journals.
24
+ Construct one game runtime above renderer lifetimes, preserve native presentation ownership, and reconcile existing operations without reopening checkout.
25
+ Use generated target build/smoke and effective-config checks before changing a release contract.
26
+
27
+ ## Devvit
28
+
29
+ `apps/target-devvit` owns Reddit SDK and deployment files. The lite inline entry is playable and the expanded entry uses the same Canvas2D game.
30
+ Preserve native feed gestures. Keep optional heavy engines behind declared dynamic imports; static imports, HTML preloads, and CSS imports count toward first-load budget.
31
+ Use configured Devvit documentation/MCP for SDK or publish-flow changes and run real playtest for platform acceptance.
32
+
33
+ ## Target handoff
34
+
35
+ Run focused checks, `pnpm check`, `pnpm build`, and `pnpm accept`. Preserve generated evidence.
36
+ Use official target tooling and configured skills for native signing, Apps in Toss, ONE play, and Verse8 acceptance.
37
+ Keep credentials, signing material, and personal MCP configuration out of source control.
@@ -0,0 +1,77 @@
1
+ import {
2
+ createCanvasRenderer,
3
+ createLiteShell,
4
+ type CanvasSurface,
5
+ type LiteShell,
6
+ } from '@mpgd/lite-shell';
7
+
8
+ import { createStarterRunState, stepStarterRunState, type StarterRunState } from '../game/state';
9
+
10
+ export interface TapGameOptions {
11
+ readonly mount: HTMLElement;
12
+ readonly title: string;
13
+ readonly onHit?: (hits: number) => void;
14
+ }
15
+
16
+ /**
17
+ * Renderer-agnostic starter gameplay: tap the orbiting target. State lives in
18
+ * `src/game/state.ts`; this file only draws and routes taps. Swap
19
+ * `createCanvasRenderer` for another `LiteRenderer` (dom/svg) when one exists.
20
+ */
21
+ export function createTapGame(options: TapGameOptions): LiteShell<CanvasSurface> {
22
+ let run: StarterRunState = createStarterRunState();
23
+ let hits = 0;
24
+ let pulse = 0;
25
+ const target = (width: number, height: number) => {
26
+ const angle = run.phase * Math.PI * 2;
27
+ const radius = Math.min(width, height) * 0.28;
28
+ return {
29
+ x: width / 2 + Math.cos(angle) * radius,
30
+ y: height / 2 + Math.sin(angle) * radius,
31
+ r: 22,
32
+ };
33
+ };
34
+
35
+ const shell = createLiteShell<CanvasSurface>({
36
+ mount: options.mount,
37
+ renderer: createCanvasRenderer(),
38
+ frame({ context, size }, deltaMs) {
39
+ run = stepStarterRunState(run, deltaMs);
40
+ pulse = Math.max(0, pulse - deltaMs / 300);
41
+ const { width, height } = size;
42
+ context.fillStyle = '#07111f';
43
+ context.fillRect(0, 0, width, height);
44
+ const dot = target(width, height);
45
+ context.beginPath();
46
+ context.arc(dot.x, dot.y, dot.r * (1 + pulse * 0.4), 0, Math.PI * 2);
47
+ context.fillStyle = pulse > 0 ? '#5eead4' : '#38bdf8';
48
+ context.fill();
49
+ context.fillStyle = '#ffffff';
50
+ context.font = '600 16px system-ui, sans-serif';
51
+ context.textAlign = 'center';
52
+ context.fillText(`${options.title} · ${hits}`, width / 2, 28);
53
+ // Keep animating only while the document is visible; the loop sleeps otherwise.
54
+ return document.visibilityState === 'visible';
55
+ },
56
+ onTap({ x, y }) {
57
+ const dot = target(shell.size.width, shell.size.height);
58
+ if (Math.hypot(x - dot.x, y - dot.y) <= dot.r * 1.6) {
59
+ hits += 1;
60
+ pulse = 1;
61
+ options.onHit?.(hits);
62
+ }
63
+ },
64
+ });
65
+ const onVisibilityChange = (): void => shell.invalidate();
66
+ document.addEventListener('visibilitychange', onVisibilityChange);
67
+ return {
68
+ ...shell,
69
+ get size() {
70
+ return shell.size;
71
+ },
72
+ dispose() {
73
+ document.removeEventListener('visibilitychange', onVisibilityChange);
74
+ shell.dispose();
75
+ },
76
+ };
77
+ }
@@ -0,0 +1,116 @@
1
+ import { requestDevvitExpandedMode, startDevvitPreviewWebView } from '@mpgd/adapter-devvit/web';
2
+
3
+ import { createTapGame } from '../lite/tapGame';
4
+
5
+ const expandedRequestRecoveryMs = 8_000;
6
+
7
+ await startDevvitPreviewWebView({
8
+ async mountInlinePreview() {
9
+ await import('./devvitInlineMode.css');
10
+ renderInlineLaunchScreen();
11
+ },
12
+ async loadExpandedGame() {
13
+ await import('../main');
14
+ },
15
+ onModeUnavailable(error) {
16
+ if (!(error instanceof ReferenceError)) {
17
+ console.warn('[devvit] web view mode unavailable; loading expanded gameplay.', error);
18
+ }
19
+ },
20
+ });
21
+
22
+ function renderInlineLaunchScreen(): void {
23
+ const launchScreen = document.createElement('main');
24
+ launchScreen.className = 'devvit-launch-screen';
25
+
26
+ const eyebrow = document.createElement('p');
27
+ eyebrow.className = 'devvit-launch-screen__eyebrow';
28
+ eyebrow.textContent = 'Play in feed';
29
+
30
+ const title = document.createElement('h1');
31
+ title.textContent = '__GAME_TITLE__';
32
+
33
+ const description = document.createElement('p');
34
+ description.className = 'devvit-launch-screen__description';
35
+ description.textContent = 'Tap to play here, or open the full game.';
36
+
37
+ // Lite template: the inline surface is playable. It uses the same lite shell
38
+ // as the expanded game, never loads Phaser, and keeps feed scrolling (taps only).
39
+ const stage = document.createElement('div');
40
+ stage.className = 'devvit-launch-screen__stage';
41
+ stage.style.width = 'min(100%, 22rem)';
42
+ stage.style.height = '9rem';
43
+
44
+ const actions = document.createElement('div');
45
+ actions.className = 'devvit-launch-screen__actions';
46
+
47
+ const expandButton = document.createElement('button');
48
+ expandButton.className = 'devvit-launch-screen__button';
49
+ expandButton.type = 'button';
50
+ expandButton.textContent = 'Play full screen';
51
+
52
+ const status = document.createElement('p');
53
+ status.className = 'devvit-launch-screen__status';
54
+ status.setAttribute('aria-live', 'polite');
55
+ status.setAttribute('role', 'status');
56
+
57
+ let nextRequestId = 0;
58
+ let activeRequestId: number | undefined;
59
+ let recoveryTimeout: ReturnType<typeof globalThis.setTimeout> | undefined;
60
+ const recover = (requestId: number, message: string) => {
61
+ if (activeRequestId !== requestId) {
62
+ return;
63
+ }
64
+
65
+ activeRequestId = undefined;
66
+ if (recoveryTimeout !== undefined) {
67
+ globalThis.clearTimeout(recoveryTimeout);
68
+ recoveryTimeout = undefined;
69
+ }
70
+
71
+ expandButton.disabled = false;
72
+ status.textContent = message;
73
+ };
74
+ expandButton.addEventListener('click', (event) => {
75
+ const requestId = ++nextRequestId;
76
+
77
+ activeRequestId = requestId;
78
+ expandButton.disabled = true;
79
+ status.textContent = 'Opening full screen…';
80
+ recoveryTimeout = globalThis.setTimeout(() => {
81
+ recover(requestId, 'Full screen did not open. Try again.');
82
+ }, expandedRequestRecoveryMs);
83
+
84
+ void requestDevvitExpandedMode(event, 'game')
85
+ .catch((error: unknown) => {
86
+ console.error('[devvit] expanded mode request failed.', error);
87
+ recover(requestId, 'Full screen is unavailable. Try again.');
88
+ });
89
+ });
90
+ const recoverAfterReturn = () => {
91
+ if (activeRequestId !== undefined) {
92
+ recover(activeRequestId, '');
93
+ }
94
+ };
95
+ globalThis.addEventListener('focus', recoverAfterReturn);
96
+ globalThis.addEventListener('pageshow', recoverAfterReturn);
97
+
98
+ actions.append(expandButton);
99
+ launchScreen.append(eyebrow, title, description, stage, actions, status);
100
+ const body = requireDocumentBody();
101
+
102
+ body.classList.add('devvit-inline-mode-host');
103
+ delete body.dataset.mpgdPreserveBrowserTouchGestures;
104
+ body.replaceChildren(launchScreen);
105
+ createTapGame({ mount: stage, title: '__GAME_TITLE__' });
106
+ }
107
+
108
+ function requireDocumentBody(): HTMLElement {
109
+ const body = document.body;
110
+
111
+ if (body === null) {
112
+ throw new Error('Devvit inline mode requires a document body.');
113
+ }
114
+
115
+ return body;
116
+ }
@@ -0,0 +1,146 @@
1
+ html,
2
+ body {
3
+ width: 100%;
4
+ height: 100%;
5
+ margin: 0;
6
+ overflow: hidden;
7
+ overscroll-behavior: auto;
8
+ touch-action: pan-y;
9
+ }
10
+
11
+ body.devvit-inline-mode-host {
12
+ background: #0f172a;
13
+ }
14
+
15
+ .devvit-launch-screen,
16
+ .devvit-launch-screen * {
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ .devvit-launch-screen {
21
+ position: fixed;
22
+ inset: 0;
23
+ width: 100%;
24
+ height: 100dvh;
25
+ overflow: hidden;
26
+ display: grid;
27
+ place-content: safe center;
28
+ justify-items: center;
29
+ gap: 0.75rem;
30
+ --inline-padding-block: clamp(1.25rem, 5vw, 3rem);
31
+ --inline-padding-inline: clamp(1.25rem, 5vw, 3rem);
32
+ padding-block:
33
+ calc(var(--inline-padding-block) + env(safe-area-inset-top, 0px))
34
+ calc(var(--inline-padding-block) + env(safe-area-inset-bottom, 0px));
35
+ padding-inline:
36
+ calc(var(--inline-padding-inline) + env(safe-area-inset-left, 0px))
37
+ calc(var(--inline-padding-inline) + env(safe-area-inset-right, 0px));
38
+ background:
39
+ radial-gradient(circle at 85% 15%, rgb(56 189 248 / 0.2), transparent 38%),
40
+ linear-gradient(145deg, #0f172a, #172554);
41
+ color: #f8fafc;
42
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
43
+ text-align: center;
44
+ touch-action: pan-y;
45
+ }
46
+
47
+ .devvit-launch-screen__eyebrow,
48
+ .devvit-launch-screen__description,
49
+ .devvit-launch-screen__status,
50
+ .devvit-launch-screen h1 {
51
+ margin: 0;
52
+ }
53
+
54
+ .devvit-launch-screen__eyebrow {
55
+ color: #7dd3fc;
56
+ font-size: 0.75rem;
57
+ font-weight: 700;
58
+ letter-spacing: 0.12em;
59
+ text-transform: uppercase;
60
+ }
61
+
62
+ .devvit-launch-screen h1 {
63
+ display: -webkit-box;
64
+ max-width: 18ch;
65
+ max-height: 1.9em;
66
+ overflow: hidden;
67
+ font-size: clamp(2rem, 9vw, 4.5rem);
68
+ line-height: 0.95;
69
+ overflow-wrap: anywhere;
70
+ -webkit-box-orient: vertical;
71
+ -webkit-line-clamp: 2;
72
+ line-clamp: 2;
73
+ }
74
+
75
+ .devvit-launch-screen__description {
76
+ max-width: 36rem;
77
+ color: #cbd5e1;
78
+ line-height: 1.5;
79
+ }
80
+
81
+ .devvit-launch-screen__actions {
82
+ width: min(100%, 22rem);
83
+ display: grid;
84
+ margin-top: 0.5rem;
85
+ }
86
+
87
+ .devvit-launch-screen__button {
88
+ min-height: 2.75rem;
89
+ padding: 0.65rem 1.25rem;
90
+ border: 1px solid transparent;
91
+ border-radius: 999px;
92
+ color: #082f49;
93
+ background: #7dd3fc;
94
+ font: inherit;
95
+ font-weight: 800;
96
+ cursor: pointer;
97
+ }
98
+
99
+ .devvit-launch-screen__button:disabled {
100
+ cursor: wait;
101
+ opacity: 0.65;
102
+ }
103
+
104
+ .devvit-launch-screen__button:focus-visible {
105
+ outline: 3px solid #f8fafc;
106
+ outline-offset: 3px;
107
+ }
108
+
109
+ .devvit-launch-screen__status {
110
+ min-height: 1.5em;
111
+ color: #fda4af;
112
+ font-size: 0.875rem;
113
+ }
114
+
115
+ @media (hover: hover) {
116
+ .devvit-launch-screen__button:not(:disabled):hover {
117
+ filter: brightness(1.1);
118
+ }
119
+ }
120
+
121
+ @media (max-width: 420px) {
122
+ .devvit-launch-screen__actions,
123
+ .devvit-launch-screen__button {
124
+ width: 100%;
125
+ }
126
+ }
127
+
128
+ @media (max-height: 440px) {
129
+ .devvit-launch-screen {
130
+ gap: 0.45rem;
131
+ --inline-padding-block: 0.5rem;
132
+ }
133
+
134
+ .devvit-launch-screen h1 {
135
+ font-size: clamp(1.75rem, 8vw, 2.5rem);
136
+ }
137
+
138
+ .devvit-launch-screen__description {
139
+ font-size: 0.875rem;
140
+ line-height: 1.3;
141
+ }
142
+
143
+ .devvit-launch-screen__actions {
144
+ margin-top: 0.2rem;
145
+ }
146
+ }
@@ -0,0 +1,57 @@
1
+ import { bindGameAudio } from '@mpgd/game-runtime/audio';
2
+ import type { CanvasSurface, LiteShell } from '@mpgd/lite-shell';
3
+
4
+ import { createTapGame } from '../lite/tapGame';
5
+ import type { StarterContext } from './gameContext';
6
+
7
+ /** Lite template: no Phaser. Mount a renderer-agnostic lite shell into the game element. */
8
+ export function createStarterGame(input: {
9
+ readonly mountId: string;
10
+ readonly preserveBrowserTouchGestures?: boolean;
11
+ readonly context: StarterContext;
12
+ }): LiteShell<CanvasSurface> {
13
+ const mount = document.getElementById(input.mountId);
14
+ if (mount === null) {
15
+ throw new Error(`Missing game mount #${input.mountId}.`);
16
+ }
17
+ mount.style.width = '100%';
18
+ mount.style.height = '100%';
19
+ let muted = false;
20
+ const audio = bindGameAudio({
21
+ execution: input.context.gameRuntime.execution,
22
+ sink: {
23
+ getMuted: () => muted,
24
+ setMuted: (next) => {
25
+ muted = next;
26
+ },
27
+ },
28
+ onError: (error) => {
29
+ console.error('[game-audio]', error);
30
+ },
31
+ });
32
+ const shell = createTapGame({ mount, title: '__GAME_TITLE__' });
33
+ let disposed = false;
34
+ const dispose = (): void => {
35
+ if (disposed) {
36
+ return;
37
+ }
38
+ disposed = true;
39
+ globalThis.removeEventListener('pagehide', onPageHide);
40
+ audio.dispose();
41
+ input.context.gameRuntime.dispose();
42
+ shell.dispose();
43
+ };
44
+ const onPageHide = (event: PageTransitionEvent): void => {
45
+ if (!event.persisted) {
46
+ dispose();
47
+ }
48
+ };
49
+ globalThis.addEventListener('pagehide', onPageHide);
50
+ return {
51
+ ...shell,
52
+ get size() {
53
+ return shell.size;
54
+ },
55
+ dispose,
56
+ };
57
+ }
@@ -27,6 +27,11 @@ This repository is a standalone Phaser game generated with `@mpgd/create-game`.
27
27
 
28
28
  ## Tooling and checks
29
29
 
30
+ - Author game code, tests, and development tools in TypeScript (`.ts`). Include every tool in
31
+ `tsconfig.tools.json` and run it with `ttsx --project tsconfig.tools.json`.
32
+ - Use `.mjs`, `.cjs`, or authored `.js` only when a concrete consumer cannot run TypeScript
33
+ (for example a browser fixture or a JavaScript-only config loader). Document that constraint
34
+ beside the file. Convenience or avoiding type errors is not an exception.
30
35
  - Use `ttsx`, not `tsx`, for TypeScript scripts.
31
36
  - Use `ttsc` and `@ttsc/lint`; do not add ESLint or Prettier for TypeScript.
32
37
  - Run focused checks, then `pnpm check` and `pnpm build`.
@@ -459,9 +459,11 @@ HTTPS game-services backend that verifies purchase or reward evidence.
459
459
  Verse8-signed accounts from self-signed guest accounts. Ads are available
460
460
  only when placement mappings are present. VXShop stays capability-disabled
461
461
  until the gateway receives the literal `purchaseEventAuthority:
462
- 'agent8-server'`, catalog product mappings, and an authenticated Agent8
463
- entitlement reader. The client purchase result is always `pending`; grants
464
- belong only in the reserved Agent8 `$onItemPurchased` event. Do not use
462
+ 'agent8-server'`, catalog product mappings, and `purchases`, an
463
+ authenticated Agent8 source (`listPurchases`) for the purchases the server
464
+ recorded. The client purchase result is always `launched`; grants belong
465
+ only in the reserved Agent8 `$onItemPurchased` event, which the backend
466
+ ledger looks up when the reconciler verifies the purchase. Do not use
465
467
  `VXShop.onClose`, client metadata, or a client-created transaction ID as
466
468
  purchase evidence. Keep `@agent8/gameserver-node` in the separate Agent8
467
469
  server project and use `@mpgd/adapter-verse8/agent8` there for the locked,
@@ -16,14 +16,14 @@
16
16
  "fix": "ttsc fix"
17
17
  },
18
18
  "dependencies": {
19
- "@apps-in-toss/web-framework": "3.5.0",
19
+ "@apps-in-toss/web-framework": "3.6.0",
20
20
  "@mpgd/adapter-ait": "__MPGD_DEPENDENCY_VERSION_ADAPTER_AIT__"
21
21
  },
22
22
  "devDependencies": {
23
- "@apps-in-toss/cli": "3.5.0",
24
- "@ttsc/unplugin": "0.30.4",
23
+ "@apps-in-toss/cli": "3.6.0",
24
+ "@ttsc/unplugin": "0.32.1",
25
25
  "@types/node": "^24.0.0",
26
- "ttsc": "0.30.4",
26
+ "ttsc": "0.32.1",
27
27
  "typescript": "7.0.2",
28
28
  "vite": "8.1.3"
29
29
  }
@@ -26,14 +26,14 @@ if (app !== null) {
26
26
  * The IAP products come from the catalog (`platformProductIds.ait`) at build
27
27
  * time, so the wrapper and the game agree on every SKU. The host bridge still
28
28
  * sells nothing until the game supplies its server hooks (`prepareIap`,
29
- * `verifyIapProductGrant`, `readIapEntitlements`, see the mpgd-kit
30
- * APPS_IN_TOSS_PRODUCTION_EVIDENCE guide): without them every purchase and
31
- * every `commerce.v2.*` method fails closed, and the grant callback only ever
32
- * confirms through the game backend.
29
+ * `verifyIapProductGrant`, see the mpgd-kit APPS_IN_TOSS_PRODUCTION_EVIDENCE
30
+ * guide): without them every purchase and every `commerce.v2.*` method fails
31
+ * closed, and the grant callback only ever confirms through the game backend.
32
+ * Ownership is read from the backend ledger, not from the wrapper.
33
33
  */
34
34
  function iapBridgeOptions(): Pick<
35
35
  InstallAitHostBridgeOptions,
36
- 'iapProducts' | 'prepareIap' | 'verifyIapProductGrant' | 'readIapEntitlements'
36
+ 'iapProducts' | 'prepareIap' | 'verifyIapProductGrant'
37
37
  > {
38
38
  return runtimeConfig.iapProducts.length === 0 ? {} : { iapProducts: runtimeConfig.iapProducts };
39
39
  }
@@ -19,8 +19,8 @@
19
19
  },
20
20
  "devDependencies": {
21
21
  "@cloudflare/workers-types": "^5.20260707.1",
22
- "@ttsc/unplugin": "0.30.4",
23
- "ttsc": "0.30.4",
22
+ "@ttsc/unplugin": "0.32.1",
23
+ "ttsc": "0.32.1",
24
24
  "typescript": "7.0.2",
25
25
  "vite": "8.1.3",
26
26
  "wrangler": "^4.107.0"
@@ -26,10 +26,10 @@
26
26
  },
27
27
  "devDependencies": {
28
28
  "@devvit/start": "0.14.1",
29
- "@ttsc/unplugin": "0.30.4",
29
+ "@ttsc/unplugin": "0.32.1",
30
30
  "@types/node": "^24.0.0",
31
31
  "devvit": "0.14.1",
32
- "ttsc": "0.30.4",
32
+ "ttsc": "0.32.1",
33
33
  "typescript": "7.0.2",
34
34
  "vite": "7.3.6"
35
35
  }