@thunderclap-engine/host-capacitor 0.0.0-stage → 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mandalorian007 / ThunderClap Gaming
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,140 @@
1
- # Temporary Holding Version
1
+ # @thunderclap-engine/host-capacitor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The ThunderClap mobile host. A game's web build runs unchanged inside a Capacitor WebView on
4
+ iOS or Android, and `createCapacitorHost()` fills the engine's `Host` interface
5
+ (`@thunderclap-engine/engine/host`, D-041) from the Capacitor plugins the app installs. The package
6
+ contains no game nouns, the engine never imports it, and it reaches the engine only through the
7
+ engine's exports map (`pnpm check:boundaries`).
8
+
9
+ It also never imports a `@capacitor/*` package. The plugins are the app's own: it installs the
10
+ ones it wants and hands them in through `plugins`. `src/plugins.ts` types the slices this host
11
+ calls by hand, so the package typechecks and tests here without Capacitor in the store (the repo
12
+ installs offline), and the real plugin objects satisfy those types structurally.
13
+
14
+ ## Layout
15
+
16
+ | Path | What it is |
17
+ |---|---|
18
+ | `src/index.ts` | What a game imports: `createCapacitorHost`, `CapacitorHost`, `CapacitorHostOptions`, `HostBack`, the three facet factories, the plugin types |
19
+ | `src/capacitor.ts` | `createCapacitorHost()` and `createAppLifecycle`, `createCapacitorHaptics`, `createPreferencesStorage` |
20
+ | `src/plugins.ts` | `CapacitorPlugins` and the per-plugin types (`AppPlugin`, `BrowserPlugin`, `ClipboardPlugin`, `HapticsPlugin`, `PreferencesPlugin`, `ScreenOrientationPlugin`, `PluginListenerHandle`, `ListenerResult`, `AppState`, `BackButtonEvent`, `ImpactStyle`, `NotificationType`, `OrientationLockType`) |
21
+ | `src/capacitor.test.ts` | Unit tests against fake plugins (Node, happy-dom) |
22
+
23
+ ## Using it from a game
24
+
25
+ ```ts
26
+ import { Capacitor } from '@capacitor/core';
27
+ import { App } from '@capacitor/app';
28
+ import { Browser } from '@capacitor/browser';
29
+ import { Clipboard } from '@capacitor/clipboard';
30
+ import { Haptics } from '@capacitor/haptics';
31
+ import { Preferences } from '@capacitor/preferences';
32
+ import { ScreenOrientation } from '@capacitor/screen-orientation';
33
+ import { createWebHost, setDefaultHost } from '@thunderclap-engine/engine/host';
34
+ import { createCapacitorHost } from '@thunderclap-engine/host-capacitor';
35
+
36
+ const host = Capacitor.isNativePlatform()
37
+ ? createCapacitorHost({ plugins: { App, Browser, Clipboard, Haptics, Preferences, ScreenOrientation } })
38
+ : createWebHost();
39
+ setDefaultHost(host);
40
+ if ('ready' in host) await host.ready; // Preferences read into the mirror; saves can open
41
+ const ctx = await createGame({ root, title, host, save: (host) => createSaveStore({ ..., host }) });
42
+ ```
43
+
44
+ Pass only the plugins the app installed; each missing one falls back to the `base` host (a web
45
+ host over the WebView's window by default) or to a no-op.
46
+
47
+ | `Host` member | With the plugin | Without it |
48
+ |---|---|---|
49
+ | `kind` | `'capacitor'` | |
50
+ | `requestFrame` / `cancelFrame`, `embed` | the WebView's `requestAnimationFrame`, via `base` | |
51
+ | `openExternal(url)` | `Browser.open` (an in-app sheet; every activation opens, `invalid` for a bad URL) | `base.openExternal` (`window.open`) |
52
+ | `copyText(text)` | `Clipboard.write` → `'copied'`, `'manual'` on a rejection | `base.copyText` |
53
+ | `clipboardWriteAllowed()` | `true` | `base`'s answer |
54
+ | `fullscreen` | `available()` and the `active` getter false, `toggle()` resolves false; `toggle({ lockLandscape: true })` calls `ScreenOrientation.lock({ orientation: 'landscape' })` | the same, without the lock |
55
+ | `storage` | `kind: 'preferences'`: a synchronous `StorageLike` over an in-memory mirror of `Preferences` (keys prefixed `tc:`), reads from the mirror, writes through in order; `open()` is null until `await host.ready` | `base.storage` (the WebView's `localStorage`) |
56
+ | `lifecycle` | `App` `appStateChange` / `pause` / `resume`; starts `active`, corrected from `App.getState()` | `base.lifecycle` (document visibility) |
57
+ | `haptics` | `Haptics.impact` (`LIGHT` / `MEDIUM` / `HEAVY`), `Haptics.notification`, `Haptics.vibrate`; `available()` true | `base.haptics` (`navigator.vibrate`) |
58
+ | `safeArea` | `base.safeArea`: `env(safe-area-inset-*)` from the WebView (needs `viewport-fit=cover` in the page's viewport meta) | |
59
+ | `window` | absent | |
60
+ | `back` (this host only) | `{ watch(fn: (e: { canGoBack }) => void): () => void }` over `App` `backButton` | absent |
61
+ | `ready` (this host only) | resolves when the Preferences mirror is loaded | resolves at once |
62
+
63
+ Why Preferences for saves: a WebView's `localStorage` can be evicted by the OS under storage
64
+ pressure and is not restored from a device backup; Preferences (`UserDefaults` / `SharedPreferences`)
65
+ survive both. The mirror keeps `StorageLike` synchronous, as the save store expects; a write that
66
+ the plugin rejects stays in the mirror and the next write still goes through.
67
+
68
+ ## Wrapping a game: ten lines of config
69
+
70
+ ```ts
71
+ // capacitor.config.ts
72
+ import type { CapacitorConfig } from '@capacitor/cli';
73
+
74
+ const config: CapacitorConfig = {
75
+ appId: 'com.example.mygame',
76
+ appName: 'My Game',
77
+ webDir: 'dist', // the game's web build (vite build)
78
+ server: { androidScheme: 'https' },
79
+ ios: { contentInset: 'never' }, // the page reaches under the notch; the host's safeArea pads the UI
80
+ android: { allowMixedContent: false },
81
+ plugins: { SplashScreen: { launchAutoHide: true } },
82
+ };
83
+ export default config;
84
+ ```
85
+
86
+ ```sh
87
+ pnpm add @capacitor/core @capacitor/cli
88
+ pnpm add @capacitor/app @capacitor/browser @capacitor/clipboard @capacitor/haptics @capacitor/preferences @capacitor/screen-orientation
89
+ npx cap init # writes the config above if you do not already have one
90
+ pnpm build # the game's web build into webDir
91
+ npx cap add ios # or: npx cap add android
92
+ npx cap sync # copies webDir and the plugins into the native project
93
+ npx cap open ios # Xcode / Android Studio: run on a device
94
+ ```
95
+
96
+ After each web build: `pnpm build && npx cap sync`. Keep `<meta name="viewport" content="...,
97
+ viewport-fit=cover">` on the page so `env(safe-area-inset-*)` reports the notch; the engine's
98
+ layout then pads its regions through `host.safeArea`.
99
+
100
+ ## The Android back button
101
+
102
+ Registering any `backButton` listener turns off Capacitor's default (which leaves the app), so
103
+ decide once, in the game, what back means:
104
+
105
+ ```ts
106
+ host.back?.watch(({ canGoBack }) => {
107
+ if (ctx.stack.length > 1) ctx.stack.pop(); // close the top menu or panel first
108
+ else if (ctx.ui.modal.open) ctx.ui.modal.close();
109
+ else if (canGoBack) history.back(); // only if the game uses the WebView's history (most do not)
110
+ else void App.minimizeApp(); // or App.exitApp(); the host.lifecycle flush has already run
111
+ });
112
+ ```
113
+
114
+ On iOS there is no back button; the swipe-from-edge gesture is the WebView's history navigation,
115
+ which a single-page game never has, so nothing is needed. `host.back` is `undefined` when
116
+ `plugins.App` was not given.
117
+
118
+ ## Lifecycle on a phone
119
+
120
+ `createGame` already watches `host.lifecycle`: on `background` it stops requesting frames and
121
+ flushes the save; on `active` it starts again, then calls `hooks.lifecycle(state)`. A game that
122
+ wants to autosave more than the store's debounced writes, or to pause its music, does it there:
123
+
124
+ ```ts
125
+ ctx.hooks.lifecycle = (state) => {
126
+ if (state === 'background') { ctx.save?.flush(); music.stop(); }
127
+ else music.play('theme');
128
+ };
129
+ ```
130
+
131
+ iOS may terminate a backgrounded app at any time, so treat `background` as "the last chance to
132
+ write"; the host's lifecycle fires it from `appStateChange`, which arrives before the WebView is
133
+ frozen.
134
+
135
+ ## Checks
136
+
137
+ `pnpm check:host-capacitor` from the repo root (typecheck, Biome, Vitest), also run by `pnpm check`.
138
+ There is no e2e here: the facets are exercised against the web host on the engine's matrix
139
+ (`e2e/host.spec.ts`, `harness/host.html`), and this host's plugin mapping is covered by the unit
140
+ tests with fake plugins.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The Capacitor host: `Host` (engine/host, D-041) for a game packaged as an iOS or Android app
3
+ * with Capacitor. The page is the game's web build in the platform WebView, so frames, the
4
+ * safe area and the fallbacks for everything else come from a web host (`base`); each Capacitor
5
+ * plugin the app hands in (`plugins`) replaces one facet: links open in the in-app browser,
6
+ * the clipboard and haptics go native, saves live in Preferences (which survive the WebView's
7
+ * storage being cleared), and the lifecycle follows the app's foreground state instead of the
8
+ * document's visibility. Nothing here imports a Capacitor package: the plugins are peers,
9
+ * typed by hand in plugins.ts.
10
+ */
11
+ import { type Host, type HostHaptics, type HostLifecycle, type HostStorage } from '@thunderclap-engine/engine/host';
12
+ import type { AppPlugin, BackButtonEvent, CapacitorPlugins, HapticsPlugin, PreferencesPlugin } from './plugins.js';
13
+ export interface CapacitorHostOptions {
14
+ /** The Capacitor plugins the app installed. Each missing one falls back to `base`. */
15
+ plugins?: Partial<CapacitorPlugins>;
16
+ /** The host answering for everything a plugin does not. Default: `createWebHost({ win })`. */
17
+ base?: Host;
18
+ /** The WebView's window, for the default `base`. Default: the global `window`. */
19
+ win?: Window;
20
+ }
21
+ /** Android's hardware or gesture back. Present only when `plugins.App` was given. */
22
+ export interface HostBack {
23
+ /**
24
+ * Calls `fn` on each back press. Registering any listener takes over the platform default
25
+ * (leaving the app), so a game that has nothing to close should call `App.exitApp()` itself.
26
+ * Returns unsubscribe.
27
+ */
28
+ watch(fn: (event: BackButtonEvent) => void): () => void;
29
+ }
30
+ export interface CapacitorHost extends Host {
31
+ readonly kind: 'capacitor';
32
+ /**
33
+ * Resolves once `storage.open()` can answer: the Preferences store has been read into the
34
+ * mirror. Until then `open()` returns null and the save store tries again on its next write.
35
+ * Resolves at once when there is no `Preferences` plugin (saves then use `base.storage`).
36
+ */
37
+ readonly ready: Promise<void>;
38
+ readonly back?: HostBack;
39
+ }
40
+ export declare function createCapacitorHost(opts?: CapacitorHostOptions): CapacitorHost;
41
+ /**
42
+ * The app's foreground state as a `HostLifecycle`. `appStateChange` carries it; `pause` and
43
+ * `resume` are honoured too, for platforms that send only those. Starts `active` (the page is
44
+ * running) and corrects itself from `getState()`.
45
+ */
46
+ export declare function createAppLifecycle(app: AppPlugin): HostLifecycle;
47
+ /** `@capacitor/haptics` as a `HostHaptics`. Every call is fire-and-forget; a rejection is dropped. */
48
+ export declare function createCapacitorHaptics(haptics: HapticsPlugin): HostHaptics;
49
+ /**
50
+ * Preferences as a synchronous `StorageLike` (`kind: 'preferences'`): reads are served from an
51
+ * in-memory mirror loaded once from the plugin, writes update the mirror at once and reach the
52
+ * plugin in order behind a promise chain. `open()` returns null until the load finished (`ready`),
53
+ * so an early save-store write misses nothing: it retries on the next write.
54
+ */
55
+ export declare function createPreferencesStorage(prefs: PreferencesPlugin): {
56
+ storage: HostStorage;
57
+ ready: Promise<void>;
58
+ };
@@ -0,0 +1,209 @@
1
+ /**
2
+ * The Capacitor host: `Host` (engine/host, D-041) for a game packaged as an iOS or Android app
3
+ * with Capacitor. The page is the game's web build in the platform WebView, so frames, the
4
+ * safe area and the fallbacks for everything else come from a web host (`base`); each Capacitor
5
+ * plugin the app hands in (`plugins`) replaces one facet: links open in the in-app browser,
6
+ * the clipboard and haptics go native, saves live in Preferences (which survive the WebView's
7
+ * storage being cleared), and the lifecycle follows the app's foreground state instead of the
8
+ * document's visibility. Nothing here imports a Capacitor package: the plugins are peers,
9
+ * typed by hand in plugins.ts.
10
+ */
11
+ import { createWebHost, } from '@thunderclap-engine/engine/host';
12
+ import { safeExternalUrl } from '@thunderclap-engine/engine/platform';
13
+ /** Key under which every save key is kept in Preferences, so a `keys()` sweep finds only ours. */
14
+ const PREFIX = 'tc:';
15
+ export function createCapacitorHost(opts = {}) {
16
+ const plugins = opts.plugins ?? {};
17
+ const base = opts.base ?? createWebHost({ win: opts.win });
18
+ const { App, Browser, Clipboard, Haptics, Preferences, ScreenOrientation } = plugins;
19
+ const storage = Preferences ? createPreferencesStorage(Preferences) : null;
20
+ const host = {
21
+ kind: 'capacitor',
22
+ // The WebView is a page: frames come from its requestAnimationFrame via the base host.
23
+ requestFrame: (cb) => base.requestFrame(cb),
24
+ cancelFrame: (handle) => base.cancelFrame(handle),
25
+ get embed() {
26
+ return base.embed;
27
+ },
28
+ openExternal(url, o) {
29
+ if (!Browser)
30
+ return base.openExternal(url, o);
31
+ const href = safeExternalUrl(url);
32
+ if (!href)
33
+ return 'invalid';
34
+ // An in-app browser sheet has no popup blocker: every activation opens.
35
+ void Browser.open({ url: href }).catch(() => { });
36
+ return 'opened';
37
+ },
38
+ async copyText(text) {
39
+ if (!Clipboard)
40
+ return base.copyText(text);
41
+ try {
42
+ await Clipboard.write({ string: String(text) });
43
+ return 'copied';
44
+ }
45
+ catch {
46
+ return 'manual';
47
+ }
48
+ },
49
+ clipboardWriteAllowed: () => (Clipboard ? true : base.clipboardWriteAllowed()),
50
+ fullscreen: {
51
+ // A store app already fills the screen; there is nothing to enter or leave.
52
+ available: () => false,
53
+ active: false,
54
+ async toggle(o) {
55
+ if (o?.lockLandscape && ScreenOrientation) {
56
+ try {
57
+ await ScreenOrientation.lock({ orientation: 'landscape' });
58
+ }
59
+ catch {
60
+ // Refused (a tablet policy, an unsupported platform): leave things as they were.
61
+ }
62
+ }
63
+ return false;
64
+ },
65
+ watch: () => () => { },
66
+ },
67
+ storage: storage?.storage ?? base.storage,
68
+ ready: storage?.ready ?? Promise.resolve(),
69
+ lifecycle: App ? createAppLifecycle(App) : base.lifecycle,
70
+ haptics: Haptics ? createCapacitorHaptics(Haptics) : base.haptics,
71
+ safeArea: base.safeArea,
72
+ // No `window`: a phone app has no title bar or close box.
73
+ };
74
+ if (App) {
75
+ host.back = {
76
+ watch: (fn) => listen(App.addListener('backButton', fn)),
77
+ };
78
+ }
79
+ return host;
80
+ }
81
+ /** Subscribes through a Capacitor listener handle (direct or promised). Returns unsubscribe. */
82
+ function listen(result) {
83
+ let removed = false;
84
+ const handle = Promise.resolve(result);
85
+ handle.catch(() => { });
86
+ return () => {
87
+ if (removed)
88
+ return;
89
+ removed = true;
90
+ void handle.then((h) => h.remove()).catch(() => { });
91
+ };
92
+ }
93
+ /**
94
+ * The app's foreground state as a `HostLifecycle`. `appStateChange` carries it; `pause` and
95
+ * `resume` are honoured too, for platforms that send only those. Starts `active` (the page is
96
+ * running) and corrects itself from `getState()`.
97
+ */
98
+ export function createAppLifecycle(app) {
99
+ let state = 'active';
100
+ const watchers = new Set();
101
+ const set = (next) => {
102
+ if (next === state)
103
+ return;
104
+ state = next;
105
+ for (const fn of [...watchers])
106
+ fn(state);
107
+ };
108
+ let listening = false;
109
+ const listen = () => {
110
+ if (listening)
111
+ return;
112
+ listening = true;
113
+ app.addListener('appStateChange', (s) => set(s.isActive ? 'active' : 'background'));
114
+ app.addListener('pause', () => set('background'));
115
+ app.addListener('resume', () => set('active'));
116
+ app
117
+ .getState()
118
+ .then((s) => set(s.isActive ? 'active' : 'background'))
119
+ .catch(() => { });
120
+ };
121
+ return {
122
+ get state() {
123
+ return state;
124
+ },
125
+ watch(fn) {
126
+ listen();
127
+ watchers.add(fn);
128
+ return () => {
129
+ watchers.delete(fn);
130
+ };
131
+ },
132
+ };
133
+ }
134
+ const IMPACT = {
135
+ light: 'LIGHT',
136
+ medium: 'MEDIUM',
137
+ heavy: 'HEAVY',
138
+ };
139
+ const NOTIFICATION = {
140
+ success: 'SUCCESS',
141
+ warning: 'WARNING',
142
+ error: 'ERROR',
143
+ };
144
+ /** `@capacitor/haptics` as a `HostHaptics`. Every call is fire-and-forget; a rejection is dropped. */
145
+ export function createCapacitorHaptics(haptics) {
146
+ const fire = (p) => {
147
+ p.catch(() => { });
148
+ };
149
+ return {
150
+ available: () => true,
151
+ impact: (style) => fire(haptics.impact({ style: IMPACT[style] ?? 'MEDIUM' })),
152
+ notification: (kind) => fire(haptics.notification({ type: NOTIFICATION[kind] ?? 'SUCCESS' })),
153
+ vibrate: (ms) => {
154
+ const duration = Math.max(0, Math.round(ms));
155
+ if (duration > 0)
156
+ fire(haptics.vibrate({ duration }));
157
+ },
158
+ };
159
+ }
160
+ /**
161
+ * Preferences as a synchronous `StorageLike` (`kind: 'preferences'`): reads are served from an
162
+ * in-memory mirror loaded once from the plugin, writes update the mirror at once and reach the
163
+ * plugin in order behind a promise chain. `open()` returns null until the load finished (`ready`),
164
+ * so an early save-store write misses nothing: it retries on the next write.
165
+ */
166
+ export function createPreferencesStorage(prefs) {
167
+ const map = new Map();
168
+ let loaded = false;
169
+ let tail = Promise.resolve();
170
+ const enqueue = (op) => {
171
+ tail = tail.then(op, op).catch(() => { });
172
+ };
173
+ const ready = (async () => {
174
+ try {
175
+ const { keys } = await prefs.keys();
176
+ for (const key of keys) {
177
+ if (!key.startsWith(PREFIX))
178
+ continue;
179
+ const { value } = await prefs.get({ key });
180
+ if (value !== null)
181
+ map.set(key.slice(PREFIX.length), value);
182
+ }
183
+ }
184
+ catch {
185
+ // Unreadable: start empty rather than never becoming usable.
186
+ }
187
+ loaded = true;
188
+ })();
189
+ const mirror = {
190
+ getItem: (key) => map.get(key) ?? null,
191
+ setItem(key, value) {
192
+ const v = String(value);
193
+ map.set(key, v);
194
+ enqueue(() => prefs.set({ key: PREFIX + key, value: v }));
195
+ },
196
+ removeItem(key) {
197
+ map.delete(key);
198
+ enqueue(() => prefs.remove({ key: PREFIX + key }));
199
+ },
200
+ get length() {
201
+ return map.size;
202
+ },
203
+ key: (index) => [...map.keys()][index] ?? null,
204
+ };
205
+ return {
206
+ ready,
207
+ storage: { kind: 'preferences', open: () => (loaded ? mirror : null) },
208
+ };
209
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `@thunderclap-engine/host-capacitor`: the engine's `Host` for a Capacitor (iOS / Android) app.
3
+ * See README.md for the capacitor.config.ts recipe and the back-button guidance.
4
+ */
5
+ export { type CapacitorHost, type CapacitorHostOptions, createAppLifecycle, createCapacitorHaptics, createCapacitorHost, createPreferencesStorage, type HostBack, } from './capacitor.js';
6
+ export type { AppPlugin, AppState, BackButtonEvent, BrowserPlugin, CapacitorPlugins, ClipboardPlugin, HapticsPlugin, ImpactStyle, ListenerResult, NotificationType, OrientationLockType, PluginListenerHandle, PreferencesPlugin, ScreenOrientationPlugin, } from './plugins.js';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * `@thunderclap-engine/host-capacitor`: the engine's `Host` for a Capacitor (iOS / Android) app.
3
+ * See README.md for the capacitor.config.ts recipe and the back-button guidance.
4
+ */
5
+ export { createAppLifecycle, createCapacitorHaptics, createCapacitorHost, createPreferencesStorage, } from './capacitor.js';
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The slices of Capacitor's plugin APIs this host calls, written out by hand so the package
3
+ * compiles and tests without the `@capacitor/*` packages installed (they are optional peers the
4
+ * app installs and hands to `createCapacitorHost({ plugins })`). The shapes follow Capacitor 5+;
5
+ * the real plugin objects satisfy them structurally. `addListener` returns the handle directly
6
+ * in some versions and a promise of it in others, so both are accepted.
7
+ */
8
+ export interface PluginListenerHandle {
9
+ remove(): Promise<void>;
10
+ }
11
+ export type ListenerResult = PluginListenerHandle | Promise<PluginListenerHandle>;
12
+ export interface AppState {
13
+ isActive: boolean;
14
+ }
15
+ export interface BackButtonEvent {
16
+ /** True when the WebView has history to go back to. */
17
+ canGoBack: boolean;
18
+ }
19
+ /** `@capacitor/app`: the app's foreground state and Android's back button. */
20
+ export interface AppPlugin {
21
+ getState(): Promise<AppState>;
22
+ addListener(event: 'appStateChange', fn: (state: AppState) => void): ListenerResult;
23
+ addListener(event: 'pause' | 'resume', fn: () => void): ListenerResult;
24
+ addListener(event: 'backButton', fn: (event: BackButtonEvent) => void): ListenerResult;
25
+ exitApp(): Promise<void>;
26
+ }
27
+ /** `@capacitor/browser`: an in-app browser sheet for external links. */
28
+ export interface BrowserPlugin {
29
+ open(options: {
30
+ url: string;
31
+ }): Promise<void>;
32
+ }
33
+ /** `@capacitor/clipboard`. */
34
+ export interface ClipboardPlugin {
35
+ write(options: {
36
+ string: string;
37
+ }): Promise<void>;
38
+ }
39
+ export type ImpactStyle = 'LIGHT' | 'MEDIUM' | 'HEAVY';
40
+ export type NotificationType = 'SUCCESS' | 'WARNING' | 'ERROR';
41
+ /** `@capacitor/haptics`. The enums are string-valued, so their literals are accepted. */
42
+ export interface HapticsPlugin {
43
+ impact(options?: {
44
+ style: ImpactStyle;
45
+ }): Promise<void>;
46
+ notification(options?: {
47
+ type: NotificationType;
48
+ }): Promise<void>;
49
+ vibrate(options?: {
50
+ duration: number;
51
+ }): Promise<void>;
52
+ }
53
+ /** `@capacitor/preferences`: a string key-value store that survives app updates. */
54
+ export interface PreferencesPlugin {
55
+ get(options: {
56
+ key: string;
57
+ }): Promise<{
58
+ value: string | null;
59
+ }>;
60
+ set(options: {
61
+ key: string;
62
+ value: string;
63
+ }): Promise<void>;
64
+ remove(options: {
65
+ key: string;
66
+ }): Promise<void>;
67
+ keys(): Promise<{
68
+ keys: string[];
69
+ }>;
70
+ }
71
+ export type OrientationLockType = 'any' | 'natural' | 'landscape' | 'portrait' | 'portrait-primary' | 'portrait-secondary' | 'landscape-primary' | 'landscape-secondary';
72
+ /** `@capacitor/screen-orientation`. */
73
+ export interface ScreenOrientationPlugin {
74
+ lock(options: {
75
+ orientation: OrientationLockType;
76
+ }): Promise<void>;
77
+ unlock(): Promise<void>;
78
+ }
79
+ /**
80
+ * Every plugin the host can use, keyed by the name each package exports. Pass the ones the app
81
+ * installed; each missing one falls back to the base (web) host's answer or to a no-op.
82
+ */
83
+ export interface CapacitorPlugins {
84
+ App: AppPlugin;
85
+ Browser: BrowserPlugin;
86
+ Clipboard: ClipboardPlugin;
87
+ Haptics: HapticsPlugin;
88
+ Preferences: PreferencesPlugin;
89
+ ScreenOrientation: ScreenOrientationPlugin;
90
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The slices of Capacitor's plugin APIs this host calls, written out by hand so the package
3
+ * compiles and tests without the `@capacitor/*` packages installed (they are optional peers the
4
+ * app installs and hands to `createCapacitorHost({ plugins })`). The shapes follow Capacitor 5+;
5
+ * the real plugin objects satisfy them structurally. `addListener` returns the handle directly
6
+ * in some versions and a promise of it in others, so both are accepted.
7
+ */
8
+ export {};
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "@thunderclap-engine/host-capacitor",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "ThunderClap's Capacitor host: createCapacitorHost() fills the engine's Host interface from Capacitor's plugins for an iOS or Android store app. It knows nothing about any game and has no dependencies; the @capacitor/* plugins are the app's own (not declared here: pnpm would try to install them), handed in through the plugins option.",
6
+ "license": "MIT",
7
+ "author": "ThunderClap",
8
+ "homepage": "https://github.com/Mandalorian007/thunderclap-engine/tree/main/packages/host-capacitor#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/Mandalorian007/thunderclap-engine/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/Mandalorian007/thunderclap-engine.git",
15
+ "directory": "packages/host-capacitor"
16
+ },
17
+ "keywords": [
18
+ "thunderclap",
19
+ "capacitor",
20
+ "ios",
21
+ "android",
22
+ "webgpu",
23
+ "game-engine"
24
+ ],
25
+ "sideEffects": false,
26
+ "exports": {
27
+ ".": {
28
+ "types": "./dist/index.d.ts",
29
+ "import": "./dist/index.js"
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "publishConfig": {
39
+ "access": "public",
40
+ "provenance": true
41
+ },
42
+ "peerDependencies": {
43
+ "@thunderclap-engine/engine": "^1.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@biomejs/biome": "^2.6.4",
47
+ "@types/node": "^26.6.6",
48
+ "@webgpu/types": "^0.1.74",
49
+ "happy-dom": "^21.0.4",
50
+ "typescript": "^7.0.2",
51
+ "vite": "^8.3.5",
52
+ "vitest": "^5.0.5",
53
+ "@thunderclap-engine/engine": "1.1.0"
54
+ },
55
+ "scripts": {
56
+ "typecheck": "tsc --noEmit",
57
+ "lint": "biome check .",
58
+ "test": "vitest run",
59
+ "check": "pnpm typecheck && pnpm lint && pnpm test"
60
+ },
61
+ "types": "./dist/index.d.ts",
62
+ "main": "./dist/index.js"
6
63
  }