@waica/engine 0.29.1 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/game.d.ts +24 -0
- package/dist/game.js +28 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/platform/desktop-platform-backend.d.ts +20 -0
- package/dist/platform/desktop-platform-backend.js +101 -0
- package/dist/platform/platform-backend.d.ts +67 -0
- package/dist/platform/platform-backend.js +42 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -487,3 +487,19 @@ game.assets.status // { pending, loaded, failed } — a fresh object on every re
|
|
|
487
487
|
- **Models.** `game.assets.model(uri)` is the glTF half of the same cache (see [3D scenes](#3d-scenes)): one load per URL, `status` and `ready()` count it, `preload` routes `.glb` and `.gltf` to it.
|
|
488
488
|
- **`GameOptions.textures`.** Replaces the real `THREE.TextureLoader` backend for that Game (ADR 0013's seam, applied to textures): `happy-dom` decodes no images, so a project's own tests inject a backend that resolves at once. The Runtime Bridge reports `assets` in its metadata under the `'assets'` capability, and a Run Session waits for `pending === 0` at readiness, after a `scene` operation and before every screenshot.
|
|
489
489
|
|
|
490
|
+
## The host platform: quit, fullscreen and achievements
|
|
491
|
+
|
|
492
|
+
Every `Game` talks to the host it runs in through a replaceable platform seam (ADR 0032): the browser, or the desktop shell that `waica export desktop` packages the game into. A game asks the same questions on both and gets an honest answer:
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
if (game.canQuit) menu.add('Quit', () => game.quit())
|
|
496
|
+
await game.setFullscreen(!game.isFullscreen())
|
|
497
|
+
game.achievements.unlock('first-kill')
|
|
498
|
+
game.achievements.setStat('kills', 3)
|
|
499
|
+
const kills = await game.achievements.getStat('kills') // number | undefined
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
- **`game.canQuit` and `game.quit()`.** `canQuit` is `true` in the desktop shell, where `quit()` closes the app, and `false` on the web, where a page cannot close itself: there `quit()` does nothing (the loop keeps running and nothing throws), so a game hides its Quit button instead.
|
|
503
|
+
- **`game.setFullscreen(on)` and `game.isFullscreen()`.** On the web, the browser's Fullscreen API: `setFullscreen(true)` requests it for the page and `setFullscreen(false)` leaves it; `isFullscreen()` is `document.fullscreenElement !== null`. A browser refuses a request made without a user gesture: the promise still resolves, never rejects, and `isFullscreen()` stays `false`, so call it from a key or click handler. In the desktop shell both go to the window, and F11 and Alt+Enter toggle it too. There `isFullscreen()` is a synchronous round trip to the main process that blocks the page until it answers, so read it when a key is pressed or a menu opens, not in every `update`.
|
|
504
|
+
- **`game.achievements`.** Store-agnostic: `unlock(id)`, `setStat(id, value)`, `getStat(id): Promise<number | undefined>` and `available`. On the web, and in a desktop build exported without Steam, `available` is `false`, `unlock` and `setStat` do nothing and `getStat` resolves `undefined`. In a desktop build with Steam (`game.json`'s `desktop.steam`) and a running Steam client, `available` is `true` and each call reaches Steamworks; declare every id in `desktop.steam.achievements` or `desktop.steam.stats`.
|
|
505
|
+
- **`GameOptions.platform`.** Absent, the Game picks the desktop backend when `window.waicaDesktop` (the shell's preload bridge, `DesktopBridge`) exists, and `WebPlatformBackend` otherwise. A `window.waicaDesktop` of another shape (an engine and a shell whose versions disagree) falls back to the web backend with a console warning that names what it lacks. A project's own tests pass a fake `PlatformBackend` to observe every call. `new WebPlatformBackend(() => element)` fullscreens that element instead of the whole page: the editor's Play passes its Viewport's surface.
|
package/dist/game.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { GameTime } from './game-time.js';
|
|
|
16
16
|
import { Input, type InputBindings } from './input.js';
|
|
17
17
|
import { PhysicsHost } from './physics-3d/physics-host.js';
|
|
18
18
|
import type { PhysicsBackend } from './physics-3d/rapier-module.js';
|
|
19
|
+
import type { PlatformAchievements, PlatformBackend } from './platform/platform-backend.js';
|
|
19
20
|
import { Pointer } from './pointer.js';
|
|
20
21
|
import { GamePost } from './post-effects.js';
|
|
21
22
|
import { type RenderBackend } from './render-readiness.js';
|
|
@@ -70,6 +71,13 @@ export interface GameOptions {
|
|
|
70
71
|
* pass the module they initialized once, or a rejecting stub.
|
|
71
72
|
*/
|
|
72
73
|
physics?: PhysicsBackend;
|
|
74
|
+
/**
|
|
75
|
+
* The host platform behind `quit`, `setFullscreen` and `achievements`
|
|
76
|
+
* (ADR 0032, the seam ADR 0013 describes): the desktop shell's bridge when
|
|
77
|
+
* `window.waicaDesktop` exists, otherwise the browser. A project's own
|
|
78
|
+
* tests pass a fake.
|
|
79
|
+
*/
|
|
80
|
+
platform?: PlatformBackend;
|
|
73
81
|
}
|
|
74
82
|
export type UpdateFn = (dt: number) => void;
|
|
75
83
|
export interface SpawnPrefabOptions {
|
|
@@ -126,6 +134,8 @@ export declare class Game {
|
|
|
126
134
|
* or collisions — the editor's edit mode.
|
|
127
135
|
*/
|
|
128
136
|
simulate: boolean;
|
|
137
|
+
/** The browser or the desktop shell (ADR 0032). */
|
|
138
|
+
private readonly platform;
|
|
129
139
|
/** WebGPU when the browser offers it, else three's WebGL2 fallback (ADR 0025). */
|
|
130
140
|
private readonly renderer;
|
|
131
141
|
private readonly readiness;
|
|
@@ -182,6 +192,20 @@ export declare class Game {
|
|
|
182
192
|
ready(): Promise<void>;
|
|
183
193
|
/** The Render Backend the renderer settled on; null until ready() resolved. */
|
|
184
194
|
get backend(): RenderBackend | null;
|
|
195
|
+
/** Whether `quit()` closes the game: true in the desktop shell, false on the web (ADR 0032). */
|
|
196
|
+
get canQuit(): boolean;
|
|
197
|
+
/** Store achievements and stats (ADR 0032): unavailable on the web, where every call is a no-op. */
|
|
198
|
+
get achievements(): PlatformAchievements;
|
|
199
|
+
/** Closes the desktop app; on the web, where a page cannot close itself, it does nothing. */
|
|
200
|
+
quit(): void;
|
|
201
|
+
/** Enters or leaves fullscreen. Never rejects: a browser refuses without a user gesture. */
|
|
202
|
+
setFullscreen(on: boolean): Promise<void>;
|
|
203
|
+
/**
|
|
204
|
+
* Whether the game is fullscreen now. In the desktop shell it asks the main
|
|
205
|
+
* process synchronously: read it on a key press or when a menu opens, not
|
|
206
|
+
* every frame.
|
|
207
|
+
*/
|
|
208
|
+
isFullscreen(): boolean;
|
|
185
209
|
/** Creates a live entity in the scene. */
|
|
186
210
|
spawn(name: string): Entity;
|
|
187
211
|
/** Instantiates a registered prefab after a scene has supplied the registry. */
|
package/dist/game.js
CHANGED
|
@@ -14,6 +14,7 @@ import { consumeSimulationSteps, MAX_CHAINED_HOPS, SIMULATION_STEP, snapElapsedT
|
|
|
14
14
|
import { advanceGameTime, GameTime } from './game-time.js';
|
|
15
15
|
import { Input } from './input.js';
|
|
16
16
|
import { PhysicsHost } from './physics-3d/physics-host.js';
|
|
17
|
+
import { defaultPlatformBackend } from './platform/desktop-platform-backend.js';
|
|
17
18
|
import { Pointer } from './pointer.js';
|
|
18
19
|
import { GamePost } from './post-effects.js';
|
|
19
20
|
import { activeRuntimeBridgeHook, EngineRuntimeBridge, } from './runtime-bridge.js';
|
|
@@ -78,6 +79,8 @@ export class Game {
|
|
|
78
79
|
* or collisions — the editor's edit mode.
|
|
79
80
|
*/
|
|
80
81
|
simulate = true;
|
|
82
|
+
/** The browser or the desktop shell (ADR 0032). */
|
|
83
|
+
platform;
|
|
81
84
|
/** WebGPU when the browser offers it, else three's WebGL2 fallback (ADR 0025). */
|
|
82
85
|
renderer;
|
|
83
86
|
readiness;
|
|
@@ -168,6 +171,7 @@ export class Game {
|
|
|
168
171
|
});
|
|
169
172
|
this.spriteBatches = createSpriteBatches(this, this.assets);
|
|
170
173
|
this.physics = new PhysicsHost(this, options.physics);
|
|
174
|
+
this.platform = options.platform ?? defaultPlatformBackend();
|
|
171
175
|
this.cameraEffects = new CameraEffects({
|
|
172
176
|
host: () => canvas.parentElement ?? document.body,
|
|
173
177
|
// One screen pixel in world units: the shake snaps to it (never the base).
|
|
@@ -213,6 +217,30 @@ export class Game {
|
|
|
213
217
|
get backend() {
|
|
214
218
|
return this.readiness.backend;
|
|
215
219
|
}
|
|
220
|
+
/** Whether `quit()` closes the game: true in the desktop shell, false on the web (ADR 0032). */
|
|
221
|
+
get canQuit() {
|
|
222
|
+
return this.platform.canQuit;
|
|
223
|
+
}
|
|
224
|
+
/** Store achievements and stats (ADR 0032): unavailable on the web, where every call is a no-op. */
|
|
225
|
+
get achievements() {
|
|
226
|
+
return this.platform.achievements;
|
|
227
|
+
}
|
|
228
|
+
/** Closes the desktop app; on the web, where a page cannot close itself, it does nothing. */
|
|
229
|
+
quit() {
|
|
230
|
+
this.platform.quit();
|
|
231
|
+
}
|
|
232
|
+
/** Enters or leaves fullscreen. Never rejects: a browser refuses without a user gesture. */
|
|
233
|
+
setFullscreen(on) {
|
|
234
|
+
return this.platform.setFullscreen(on);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Whether the game is fullscreen now. In the desktop shell it asks the main
|
|
238
|
+
* process synchronously: read it on a key press or when a menu opens, not
|
|
239
|
+
* every frame.
|
|
240
|
+
*/
|
|
241
|
+
isFullscreen() {
|
|
242
|
+
return this.platform.isFullscreen();
|
|
243
|
+
}
|
|
216
244
|
/** Creates a live entity in the scene. */
|
|
217
245
|
spawn(name) {
|
|
218
246
|
const entity = new Entity(this, name);
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
|
11
11
|
export type { AudioSubsystemOptions } from './audio/audio-subsystem.js';
|
|
12
12
|
export type { AudioBackend, AudioResource, BackendPlayHandle, BackendPlayOptions } from './audio/backend.js';
|
|
13
13
|
export type { AudioChannelState, AudioPlayOptions, LiveSoundInfo, SoundHandle } from './audio/types.js';
|
|
14
|
+
export { WebPlatformBackend, type DesktopBridge, type PlatformAchievements, type PlatformBackend } from './platform/platform-backend.js';
|
|
14
15
|
export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
|
|
15
16
|
export type { AnimationFacingProvider, DirectionalAnimation, DirectionalFallback, ResolvedDirectionalClip, } from './animation/directional.js';
|
|
16
17
|
export { isClipTarget } from './animation/clip-target.js';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { AssetLoader } from './assets/asset-loader.js';
|
|
|
3
3
|
export { GltfModelBackend } from './assets/model-backend.js';
|
|
4
4
|
export { glbClipNames } from './assets/glb-clip-names.js';
|
|
5
5
|
export { AudioSubsystem } from './audio/audio-subsystem.js';
|
|
6
|
+
export { WebPlatformBackend } from './platform/platform-backend.js';
|
|
6
7
|
export { installDirectionalAnimation, installedDirectionalAnimation, isAnimationFacingProvider, resolveDirectionalClip, } from './animation/directional.js';
|
|
7
8
|
export { isClipTarget } from './animation/clip-target.js';
|
|
8
9
|
export { isYSortBatchParticipant, isYSortParticipant, ySortZ } from './render-sort.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type DesktopBridge, type PlatformAchievements, type PlatformBackend } from './platform-backend.js';
|
|
2
|
+
/** `window.waicaDesktop` is page-reachable data: only the shape the shell exposes counts as the bridge. */
|
|
3
|
+
export declare function isDesktopBridge(value: unknown): value is DesktopBridge;
|
|
4
|
+
/** The packaged desktop app as a platform: every call goes to the shell's main process. */
|
|
5
|
+
export declare class DesktopPlatformBackend implements PlatformBackend {
|
|
6
|
+
private readonly bridge;
|
|
7
|
+
readonly canQuit = true;
|
|
8
|
+
readonly achievements: PlatformAchievements;
|
|
9
|
+
constructor(bridge: DesktopBridge);
|
|
10
|
+
quit(): void;
|
|
11
|
+
setFullscreen(on: boolean): Promise<void>;
|
|
12
|
+
isFullscreen(): boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The desktop backend inside the shell (`window.waicaDesktop` exists), the
|
|
16
|
+
* web one everywhere else. A `window.waicaDesktop` of another shape (an
|
|
17
|
+
* engine and a shell from versions that disagree) falls back to the web
|
|
18
|
+
* backend and says what it lacks, so a missing Quit has a cause in the console.
|
|
19
|
+
*/
|
|
20
|
+
export declare function defaultPlatformBackend(): PlatformBackend;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { NO_ACHIEVEMENTS, WebPlatformBackend, } from './platform-backend.js';
|
|
2
|
+
function isObject(value) {
|
|
3
|
+
return typeof value === 'object' && value !== null;
|
|
4
|
+
}
|
|
5
|
+
function isAchievements(value) {
|
|
6
|
+
return (isObject(value) &&
|
|
7
|
+
typeof value['available'] === 'boolean' &&
|
|
8
|
+
typeof value['unlock'] === 'function' &&
|
|
9
|
+
typeof value['setStat'] === 'function' &&
|
|
10
|
+
typeof value['getStat'] === 'function');
|
|
11
|
+
}
|
|
12
|
+
const BRIDGE_METHODS = ['quit', 'setFullscreen', 'isFullscreen'];
|
|
13
|
+
/** The members a `window.waicaDesktop` lacks (or has with the wrong type) to be the shell's bridge. */
|
|
14
|
+
function bridgeGaps(value) {
|
|
15
|
+
if (!isObject(value))
|
|
16
|
+
return ['every member'];
|
|
17
|
+
const gaps = BRIDGE_METHODS.filter((name) => typeof value[name] !== 'function');
|
|
18
|
+
if (value['achievements'] !== undefined && !isAchievements(value['achievements']))
|
|
19
|
+
gaps.push('achievements');
|
|
20
|
+
return gaps;
|
|
21
|
+
}
|
|
22
|
+
/** `window.waicaDesktop` is page-reachable data: only the shape the shell exposes counts as the bridge. */
|
|
23
|
+
export function isDesktopBridge(value) {
|
|
24
|
+
return bridgeGaps(value).length === 0;
|
|
25
|
+
}
|
|
26
|
+
/** A throw from the bridge (say, the window closing after `quit()`) stays here instead of reaching the game's script. */
|
|
27
|
+
function callShell(call) {
|
|
28
|
+
try {
|
|
29
|
+
call();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// The shell is gone or refused: an achievement call is fire-and-forget.
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The shell's achievements, held to the contract the engine documents: no
|
|
37
|
+
* call throws, and `getStat` resolves a number or `undefined`, whatever the
|
|
38
|
+
* bridge does.
|
|
39
|
+
*/
|
|
40
|
+
function forwardedAchievements(shell) {
|
|
41
|
+
return {
|
|
42
|
+
available: true,
|
|
43
|
+
unlock: (id) => callShell(() => shell.unlock(id)),
|
|
44
|
+
setStat: (id, value) => callShell(() => shell.setStat(id, value)),
|
|
45
|
+
getStat: async (id) => {
|
|
46
|
+
try {
|
|
47
|
+
const value = await shell.getStat(id);
|
|
48
|
+
return typeof value === 'number' ? value : undefined;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** The packaged desktop app as a platform: every call goes to the shell's main process. */
|
|
57
|
+
export class DesktopPlatformBackend {
|
|
58
|
+
bridge;
|
|
59
|
+
canQuit = true;
|
|
60
|
+
achievements;
|
|
61
|
+
constructor(bridge) {
|
|
62
|
+
this.bridge = bridge;
|
|
63
|
+
const shell = bridge.achievements;
|
|
64
|
+
this.achievements = shell?.available === true ? forwardedAchievements(shell) : NO_ACHIEVEMENTS;
|
|
65
|
+
}
|
|
66
|
+
quit() {
|
|
67
|
+
this.bridge.quit();
|
|
68
|
+
}
|
|
69
|
+
async setFullscreen(on) {
|
|
70
|
+
try {
|
|
71
|
+
await this.bridge.setFullscreen(on);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// The window is gone or the shell refused: the state stays what isFullscreen() reports.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
isFullscreen() {
|
|
78
|
+
try {
|
|
79
|
+
return this.bridge.isFullscreen() === true;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// The window is gone: it is not fullscreen.
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The desktop backend inside the shell (`window.waicaDesktop` exists), the
|
|
89
|
+
* web one everywhere else. A `window.waicaDesktop` of another shape (an
|
|
90
|
+
* engine and a shell from versions that disagree) falls back to the web
|
|
91
|
+
* backend and says what it lacks, so a missing Quit has a cause in the console.
|
|
92
|
+
*/
|
|
93
|
+
export function defaultPlatformBackend() {
|
|
94
|
+
const candidate = Reflect.get(globalThis, 'waicaDesktop');
|
|
95
|
+
if (isDesktopBridge(candidate))
|
|
96
|
+
return new DesktopPlatformBackend(candidate);
|
|
97
|
+
if (candidate !== undefined) {
|
|
98
|
+
console.warn(`[waica] window.waicaDesktop is not the desktop shell's bridge (${bridgeGaps(candidate).join(', ')} missing or wrong): the game runs without quit, native fullscreen or achievements`);
|
|
99
|
+
}
|
|
100
|
+
return new WebPlatformBackend();
|
|
101
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store achievements and stats, whichever store the game ships on (issue #200).
|
|
3
|
+
* Unavailable on the web and in a desktop build without a store client, where
|
|
4
|
+
* every call is a no-op and `getStat` resolves `undefined`.
|
|
5
|
+
*/
|
|
6
|
+
export interface PlatformAchievements {
|
|
7
|
+
/** True only while a store client is connected (the desktop shell with Steam). */
|
|
8
|
+
readonly available: boolean;
|
|
9
|
+
/** Activates an achievement by its store id. */
|
|
10
|
+
unlock(id: string): void;
|
|
11
|
+
/** Sets an integer stat by its store id and stores it. */
|
|
12
|
+
setStat(id: string, value: number): void;
|
|
13
|
+
/** A stat's stored value; `undefined` where none can be read. */
|
|
14
|
+
getStat(id: string): Promise<number | undefined>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Everything Game needs from the host it runs in (ADR 0032): the browser, or
|
|
18
|
+
* the desktop shell's preload bridge. The web implementation is the default;
|
|
19
|
+
* the desktop one is picked when `window.waicaDesktop` exists; a project's
|
|
20
|
+
* own tests replace either through `GameOptions.platform`.
|
|
21
|
+
*/
|
|
22
|
+
export interface PlatformBackend {
|
|
23
|
+
/** Whether `quit()` can close the game: false where a page cannot close itself. */
|
|
24
|
+
readonly canQuit: boolean;
|
|
25
|
+
/** Closes the game where `canQuit` is true; a no-op elsewhere. */
|
|
26
|
+
quit(): void;
|
|
27
|
+
/** Enters or leaves fullscreen. Never rejects: a refused request leaves `isFullscreen()` as it was. */
|
|
28
|
+
setFullscreen(on: boolean): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Whether the game is fullscreen now. In the desktop shell this is a
|
|
31
|
+
* synchronous round trip to the main process, which blocks the page until
|
|
32
|
+
* it answers: read it when a key is pressed or a menu opens, not every frame.
|
|
33
|
+
*/
|
|
34
|
+
isFullscreen(): boolean;
|
|
35
|
+
readonly achievements: PlatformAchievements;
|
|
36
|
+
}
|
|
37
|
+
/** Achievements where no store client is connected: unavailable, and every call a no-op. */
|
|
38
|
+
export declare const NO_ACHIEVEMENTS: PlatformAchievements;
|
|
39
|
+
/**
|
|
40
|
+
* What the desktop shell's preload exposes as `window.waicaDesktop` through
|
|
41
|
+
* `contextBridge` (ADR 0032). `achievements` is absent or unavailable without
|
|
42
|
+
* Steam.
|
|
43
|
+
*/
|
|
44
|
+
export interface DesktopBridge {
|
|
45
|
+
quit(): void;
|
|
46
|
+
setFullscreen(on: boolean): Promise<void>;
|
|
47
|
+
isFullscreen(): boolean;
|
|
48
|
+
achievements?: PlatformAchievements;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The browser as a platform (ADR 0032): a page cannot close itself, fullscreen
|
|
52
|
+
* is the Fullscreen API, and there is no store, so no achievements.
|
|
53
|
+
*/
|
|
54
|
+
export declare class WebPlatformBackend implements PlatformBackend {
|
|
55
|
+
private readonly fullscreenTarget;
|
|
56
|
+
readonly canQuit = false;
|
|
57
|
+
readonly achievements: PlatformAchievements;
|
|
58
|
+
/**
|
|
59
|
+
* @param fullscreenTarget The element `setFullscreen(true)` asks to fill the
|
|
60
|
+
* screen: the whole page by default. A host that shows the game inside a
|
|
61
|
+
* larger page, like the editor's Viewport, passes the game's own surface.
|
|
62
|
+
*/
|
|
63
|
+
constructor(fullscreenTarget?: () => Element);
|
|
64
|
+
quit(): void;
|
|
65
|
+
setFullscreen(on: boolean): Promise<void>;
|
|
66
|
+
isFullscreen(): boolean;
|
|
67
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Achievements where no store client is connected: unavailable, and every call a no-op. */
|
|
2
|
+
export const NO_ACHIEVEMENTS = Object.freeze({
|
|
3
|
+
available: false,
|
|
4
|
+
unlock: () => { },
|
|
5
|
+
setStat: () => { },
|
|
6
|
+
getStat: () => Promise.resolve(undefined),
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* The browser as a platform (ADR 0032): a page cannot close itself, fullscreen
|
|
10
|
+
* is the Fullscreen API, and there is no store, so no achievements.
|
|
11
|
+
*/
|
|
12
|
+
export class WebPlatformBackend {
|
|
13
|
+
fullscreenTarget;
|
|
14
|
+
canQuit = false;
|
|
15
|
+
achievements = NO_ACHIEVEMENTS;
|
|
16
|
+
/**
|
|
17
|
+
* @param fullscreenTarget The element `setFullscreen(true)` asks to fill the
|
|
18
|
+
* screen: the whole page by default. A host that shows the game inside a
|
|
19
|
+
* larger page, like the editor's Viewport, passes the game's own surface.
|
|
20
|
+
*/
|
|
21
|
+
constructor(fullscreenTarget = () => document.documentElement) {
|
|
22
|
+
this.fullscreenTarget = fullscreenTarget;
|
|
23
|
+
}
|
|
24
|
+
quit() {
|
|
25
|
+
// A page cannot close its own tab; canQuit tells the game not to offer it.
|
|
26
|
+
}
|
|
27
|
+
async setFullscreen(on) {
|
|
28
|
+
try {
|
|
29
|
+
if (on)
|
|
30
|
+
await this.fullscreenTarget().requestFullscreen();
|
|
31
|
+
else
|
|
32
|
+
await document.exitFullscreen();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Refused (no user gesture, an iframe without permission) or no Fullscreen API at all.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
isFullscreen() {
|
|
39
|
+
// `undefined` where the page has no Fullscreen API: never fullscreen there.
|
|
40
|
+
return Boolean(document.fullscreenElement);
|
|
41
|
+
}
|
|
42
|
+
}
|