@fzgame/kit 0.0.0-stage → 0.2.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 +15 -0
- package/README.md +182 -2
- package/dist/index.cjs +435 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +127 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +395 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fanzeal
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,183 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @fzgame/kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The client kit a FZ GameHub game loads: it resolves which game instance is being played, creates or
|
|
4
|
+
resumes a play session, keeps a local snapshot of the session, and posts lifecycle events back to the
|
|
5
|
+
server. The editor and the sandbox talk to a game through the same message protocol.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @fzgame/kit
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The kit is ESM-first and also ships a CommonJS build. In the GameHub hosts a game loads the bundle as
|
|
14
|
+
a dev-time URL or as `game/vendor/kit.js` from a published zip, so a game does not bundle the kit
|
|
15
|
+
itself; the CLI's `fzgame build` vendors it. Items below assume a bundler when you import it directly.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import { createKit } from '@fzgame/kit'
|
|
21
|
+
|
|
22
|
+
// Reads apiUrl, gameInstanceId, and playerToken from the query string.
|
|
23
|
+
const kit = createKit()
|
|
24
|
+
|
|
25
|
+
await kit.load() // the published game instance
|
|
26
|
+
await kit.connect() // a play session
|
|
27
|
+
await kit.emit('ready')
|
|
28
|
+
|
|
29
|
+
await kit.emit('start')
|
|
30
|
+
kit.setState({ clicks: 1 })
|
|
31
|
+
await kit.emit('score', { score: 1 })
|
|
32
|
+
await kit.emit('finish', { score: 1, result: { clicks: 1 } })
|
|
33
|
+
|
|
34
|
+
kit.snapshot // { status, score, result, state }
|
|
35
|
+
kit.config() // validated form values for this instance
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Pass the values explicitly instead of reading the query string:
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
const kit = createKit({ apiUrl, gameInstanceId, playerToken })
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`createKit()` reads anything you leave out from the query string (`apiUrl`, `gameInstanceId`,
|
|
45
|
+
`playerToken`). A value that is still missing throws at construction.
|
|
46
|
+
|
|
47
|
+
## Lifecycle
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
CREATED → READY → PLAYING ⇄ PAUSED → FINISHED
|
|
51
|
+
↘ ABORTED
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`emit(type, payload?)` sends the event and adopts the session the server returns, so `kit.snapshot`
|
|
55
|
+
tracks the server rather than your local guess. The events the server acts on are `ready`, `start`,
|
|
56
|
+
`pause`, `finish`, `abort`, `score`, and `snapshot` (also exported as `LIFECYCLE_EVENTS`). Any other
|
|
57
|
+
string is stored as the session's last event and leaves the status alone.
|
|
58
|
+
|
|
59
|
+
| Event | Effect |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `ready` | Status `READY`. |
|
|
62
|
+
| `start` | Status `PLAYING`. |
|
|
63
|
+
| `pause` | Status `PAUSED`. |
|
|
64
|
+
| `finish` | Status `FINISHED`; `payload.score` and `payload.result` are stored. |
|
|
65
|
+
| `abort` | Status `ABORTED`. |
|
|
66
|
+
| `score` | Stores `payload.score`. |
|
|
67
|
+
| `snapshot` | Stores `payload.state` as the session snapshot. |
|
|
68
|
+
|
|
69
|
+
`emit()` and `setState()` require a session and throw if you call them before `connect()`.
|
|
70
|
+
|
|
71
|
+
## Snapshot
|
|
72
|
+
|
|
73
|
+
`kit.snapshot` is `{ status, score, result, state }`, refreshed after every call that returns a
|
|
74
|
+
session. `setState(state)` also stores the value in the session's metadata, so a resumed session
|
|
75
|
+
restores where the player stopped.
|
|
76
|
+
|
|
77
|
+
## Config
|
|
78
|
+
|
|
79
|
+
`kit.config()` merges, in increasing priority: defaults from the game instance's config schema, the
|
|
80
|
+
instance overrides an operator set, and the config passed to `createKit` (or read from the query
|
|
81
|
+
string). `readFormSchema(schema)` exposes the schema itself when you render the settings form.
|
|
82
|
+
|
|
83
|
+
## Preview
|
|
84
|
+
|
|
85
|
+
`?preview=true` (also `1` / `yes`) skips every server call. `load`, `connect`, `emit`, and `setState`
|
|
86
|
+
stay in memory so the game is free to play, and credentials become optional. The sandbox and editor
|
|
87
|
+
put unsaved settings in `?gameSetting={"goal":10}`; it wins over the older `?config=`, so Play reloads
|
|
88
|
+
with your draft before you submit.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
/game/index.html?preview=true&gameSetting=%7B%22goal%22%3A20%7D&kit=/@fzgame/kit.js
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Editor protocol
|
|
95
|
+
|
|
96
|
+
A game running inside the editor can send its draft config out and receive the editor's init message.
|
|
97
|
+
Both directions post to `window.parent`; the editor and sandbox listen on the `fzgame-editor` and
|
|
98
|
+
`fzgame-host` sources exported as `EDITOR_SOURCE` and `HOST_SOURCE`.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
import { postEditorSave, onHostInit } from '@fzgame/kit'
|
|
102
|
+
|
|
103
|
+
const stop = onHostInit(({ config, configSchema, gameInstanceId }) => { /* apply */ })
|
|
104
|
+
postEditorSave({ goal: 20 })
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`onHostInit` returns an unsubscribe function. Messages are posted with a `'*'` target origin, because
|
|
108
|
+
a game running in a cross-origin frame cannot read the parent's origin.
|
|
109
|
+
|
|
110
|
+
## Errors
|
|
111
|
+
|
|
112
|
+
| Error | When |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| `Error` | A required option is missing, or `emit` / `setState` runs before `connect()`. |
|
|
115
|
+
| `KitApiError` | A request answered with a non-OK status. Carries `status`, `statusText`, `method`, `url`, the parsed `body`, and `code` when the body has one. |
|
|
116
|
+
| `KitTimeoutError` | A request outlived `timeoutMs`. Carries `timeoutMs`, `method`, and `url`. |
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
import { KitApiError } from '@fzgame/kit'
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await kit.connect()
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error instanceof KitApiError && error.status === 404) {
|
|
125
|
+
// the instance is gone
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Timeouts
|
|
131
|
+
|
|
132
|
+
Every request is aborted after `timeoutMs`, 30 seconds by default, so a game's loading screen never
|
|
133
|
+
hangs on a dead socket. Set `timeoutMs: 0` to wait without a deadline.
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
const kit = createKit({ timeoutMs: 5_000 })
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The kit uses `AbortController` rather than `AbortSignal.timeout`, so it runs in older browsers.
|
|
140
|
+
|
|
141
|
+
## Debug channel
|
|
142
|
+
|
|
143
|
+
When the game runs in a frame, the kit posts its progress to the parent as
|
|
144
|
+
`{ source: 'fzgame-kit', type: 'debug', event, payload, at }`. The sandbox shows these in its log
|
|
145
|
+
panel. Credential-looking keys (`token`, `secret`, `password`, `authorization`, `apiKey`) and
|
|
146
|
+
reference cycles are replaced before posting.
|
|
147
|
+
|
|
148
|
+
## API
|
|
149
|
+
|
|
150
|
+
| Export | Purpose |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `createKit(options?)` | Build a kit from options plus the query string. |
|
|
153
|
+
| `FzGameKit` | The class, if you would rather construct it directly. |
|
|
154
|
+
| `DEFAULT_TIMEOUT_MS` | 30000. |
|
|
155
|
+
| `KIT_SOURCE`, `EDITOR_SOURCE`, `HOST_SOURCE` | Message `source` values. |
|
|
156
|
+
| `postEditorSave`, `onHostInit` | Outgoing save and incoming init for the editor. |
|
|
157
|
+
| `readQueryConfig`, `readFormSchema`, `isPreviewFlag` | Query and schema helpers. |
|
|
158
|
+
| `LIFECYCLE_EVENTS`, `PLAY_SESSION_STATUSES`, `emptySnapshot` | Lifecycle constants. |
|
|
159
|
+
| `KitApiError`, `KitTimeoutError` | Typed failures. |
|
|
160
|
+
| `KitOptions`, `EmitType`, `Snapshot`, `LifecycleEvent`, `GameInstancePayload`, `PlaySessionPayload` | Types. |
|
|
161
|
+
|
|
162
|
+
## What it does not do
|
|
163
|
+
|
|
164
|
+
- It renders nothing. No DOM, no canvas, no styles: your game draws and the kit keeps score.
|
|
165
|
+
- It stores nothing locally. No `localStorage`, no cookies. State reaches the server through
|
|
166
|
+
`setState` and `emit`, so a player who reloads outside a resumed session starts over.
|
|
167
|
+
- It does not retry. A request that fails or times out rejects; nothing is queued, so a `finish`
|
|
168
|
+
event during a network blip is lost unless the game sends it again.
|
|
169
|
+
- It does not validate the config it returns. `kit.config()` merges default values, instance
|
|
170
|
+
overrides, and your own in that order and hands the result over as-is.
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
pnpm build # tsup, ESM + CJS + declarations
|
|
176
|
+
pnpm check-types
|
|
177
|
+
pnpm test:run
|
|
178
|
+
pnpm smoke # pack the tarball and consume it from a CJS and an ESM consumer
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
ISC
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
21
|
+
|
|
22
|
+
// src/index.ts
|
|
23
|
+
var src_exports = {};
|
|
24
|
+
__export(src_exports, {
|
|
25
|
+
DEFAULT_TIMEOUT_MS: () => DEFAULT_TIMEOUT_MS,
|
|
26
|
+
EDITOR_SOURCE: () => EDITOR_SOURCE,
|
|
27
|
+
FzGameKit: () => FzGameKit,
|
|
28
|
+
HOST_SOURCE: () => HOST_SOURCE,
|
|
29
|
+
KIT_SOURCE: () => KIT_SOURCE,
|
|
30
|
+
KitApiError: () => KitApiError,
|
|
31
|
+
KitTimeoutError: () => KitTimeoutError,
|
|
32
|
+
LIFECYCLE_EVENTS: () => LIFECYCLE_EVENTS,
|
|
33
|
+
PLAY_SESSION_STATUSES: () => PLAY_SESSION_STATUSES,
|
|
34
|
+
createKit: () => createKit,
|
|
35
|
+
emptySnapshot: () => emptySnapshot,
|
|
36
|
+
isPreviewFlag: () => isPreviewFlag,
|
|
37
|
+
onHostInit: () => onHostInit,
|
|
38
|
+
postEditorSave: () => postEditorSave,
|
|
39
|
+
readFormSchema: () => readFormSchema,
|
|
40
|
+
readQueryConfig: () => readQueryConfig
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(src_exports);
|
|
43
|
+
|
|
44
|
+
// src/lifecycle.ts
|
|
45
|
+
var PLAY_SESSION_STATUSES = [
|
|
46
|
+
"CREATED",
|
|
47
|
+
"READY",
|
|
48
|
+
"PLAYING",
|
|
49
|
+
"PAUSED",
|
|
50
|
+
"FINISHED",
|
|
51
|
+
"ABORTED",
|
|
52
|
+
"EXPIRED"
|
|
53
|
+
];
|
|
54
|
+
var LIFECYCLE_EVENTS = [
|
|
55
|
+
"ready",
|
|
56
|
+
"start",
|
|
57
|
+
"pause",
|
|
58
|
+
"finish",
|
|
59
|
+
"abort",
|
|
60
|
+
"score",
|
|
61
|
+
"snapshot"
|
|
62
|
+
];
|
|
63
|
+
function emptySnapshot() {
|
|
64
|
+
return { status: "CREATED", score: null, result: null, state: null };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/kit.ts
|
|
68
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
69
|
+
var KIT_SOURCE = "fzgame-kit";
|
|
70
|
+
var SECRET_KEY = /(token|secret|password|authorization|api[-_]?key)/i;
|
|
71
|
+
function redact(value, seen = /* @__PURE__ */ new WeakSet(), depth = 0) {
|
|
72
|
+
if (depth > 8 || value === null || typeof value !== "object") return value;
|
|
73
|
+
if (seen.has(value)) return "[circular]";
|
|
74
|
+
seen.add(value);
|
|
75
|
+
const out = Array.isArray(value) ? value.map((item) => redact(item, seen, depth + 1)) : Object.fromEntries(
|
|
76
|
+
Object.entries(value).map(([key, item]) => [
|
|
77
|
+
key,
|
|
78
|
+
SECRET_KEY.test(key) ? "[redacted]" : redact(item, seen, depth + 1)
|
|
79
|
+
])
|
|
80
|
+
);
|
|
81
|
+
seen.delete(value);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
var KitApiError = class extends Error {
|
|
85
|
+
constructor(response, body, method, url) {
|
|
86
|
+
super(`${method} ${url} failed with ${response.status} ${response.statusText}`);
|
|
87
|
+
__publicField(this, "status");
|
|
88
|
+
__publicField(this, "statusText");
|
|
89
|
+
__publicField(this, "method");
|
|
90
|
+
__publicField(this, "url");
|
|
91
|
+
/** Parsed response body, or the raw text when it is not JSON. */
|
|
92
|
+
__publicField(this, "body");
|
|
93
|
+
/** `body.code` when the server sends one. */
|
|
94
|
+
__publicField(this, "code");
|
|
95
|
+
this.name = "KitApiError";
|
|
96
|
+
this.status = response.status;
|
|
97
|
+
this.statusText = response.statusText;
|
|
98
|
+
this.method = method;
|
|
99
|
+
this.url = url;
|
|
100
|
+
this.body = body;
|
|
101
|
+
const code = body && typeof body === "object" ? body.code : void 0;
|
|
102
|
+
if (typeof code === "string") this.code = code;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var KitTimeoutError = class extends Error {
|
|
106
|
+
constructor(timeoutMs, method, url) {
|
|
107
|
+
super(`${method} ${url} did not answer within ${timeoutMs}ms`);
|
|
108
|
+
__publicField(this, "timeoutMs");
|
|
109
|
+
__publicField(this, "method");
|
|
110
|
+
__publicField(this, "url");
|
|
111
|
+
this.name = "KitTimeoutError";
|
|
112
|
+
this.timeoutMs = timeoutMs;
|
|
113
|
+
this.method = method;
|
|
114
|
+
this.url = url;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
var FzGameKit = class {
|
|
118
|
+
constructor(options) {
|
|
119
|
+
__publicField(this, "options");
|
|
120
|
+
__publicField(this, "instance", null);
|
|
121
|
+
__publicField(this, "session", null);
|
|
122
|
+
__publicField(this, "snapshot", emptySnapshot());
|
|
123
|
+
__publicField(this, "injectedConfig");
|
|
124
|
+
__publicField(this, "connecting", null);
|
|
125
|
+
const preview = Boolean(options.preview);
|
|
126
|
+
this.options = {
|
|
127
|
+
apiUrl: (options.apiUrl || "").replace(/\/$/, ""),
|
|
128
|
+
gameInstanceId: options.gameInstanceId || (preview ? "preview" : ""),
|
|
129
|
+
playerToken: options.playerToken || (preview ? "preview" : ""),
|
|
130
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
131
|
+
config: options.config,
|
|
132
|
+
preview
|
|
133
|
+
};
|
|
134
|
+
if (!preview && (!this.options.apiUrl || !this.options.gameInstanceId || !this.options.playerToken)) {
|
|
135
|
+
throw new Error("FzGameKit requires apiUrl, gameInstanceId, and playerToken");
|
|
136
|
+
}
|
|
137
|
+
this.injectedConfig = options.config ?? readQueryConfig() ?? {};
|
|
138
|
+
}
|
|
139
|
+
get preview() {
|
|
140
|
+
return this.options.preview;
|
|
141
|
+
}
|
|
142
|
+
async load() {
|
|
143
|
+
try {
|
|
144
|
+
if (this.options.preview) {
|
|
145
|
+
this.instance = {
|
|
146
|
+
id: this.options.gameInstanceId,
|
|
147
|
+
name: "preview",
|
|
148
|
+
overrides: this.injectedConfig
|
|
149
|
+
};
|
|
150
|
+
this.debug("load", { instance: this.instance, preview: true });
|
|
151
|
+
return this.instance;
|
|
152
|
+
}
|
|
153
|
+
this.instance = await this.request(
|
|
154
|
+
`/pub/game-instances/${this.options.gameInstanceId}`
|
|
155
|
+
);
|
|
156
|
+
this.debug("load", { instance: this.instance });
|
|
157
|
+
return this.instance;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
this.debug("load", { error: errorMessage(error) });
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async connect() {
|
|
164
|
+
this.connecting ?? (this.connecting = this.startSession().finally(() => {
|
|
165
|
+
this.connecting = null;
|
|
166
|
+
}));
|
|
167
|
+
return this.connecting;
|
|
168
|
+
}
|
|
169
|
+
async startSession() {
|
|
170
|
+
try {
|
|
171
|
+
if (this.options.preview) {
|
|
172
|
+
this.session = previewSession();
|
|
173
|
+
this.hydrate();
|
|
174
|
+
this.debug("connect", { session: this.session, preview: true });
|
|
175
|
+
return this.session;
|
|
176
|
+
}
|
|
177
|
+
this.session = await this.request(
|
|
178
|
+
"/games/runtime/sessions",
|
|
179
|
+
{
|
|
180
|
+
method: "POST",
|
|
181
|
+
body: JSON.stringify({
|
|
182
|
+
gameInstanceId: this.options.gameInstanceId,
|
|
183
|
+
playerToken: this.options.playerToken
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
this.hydrate();
|
|
188
|
+
this.debug("connect", { session: this.session });
|
|
189
|
+
return this.session;
|
|
190
|
+
} catch (error) {
|
|
191
|
+
this.debug("connect", { error: errorMessage(error) });
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async emit(type, payload) {
|
|
196
|
+
try {
|
|
197
|
+
if (this.options.preview) {
|
|
198
|
+
this.session = this.session ?? previewSession();
|
|
199
|
+
applyLocalEmit(this.session, type, payload);
|
|
200
|
+
this.hydrate();
|
|
201
|
+
this.debug("emit", { type, payload: payload ?? null, session: this.session, preview: true });
|
|
202
|
+
return this.session;
|
|
203
|
+
}
|
|
204
|
+
const token = this.requireToken();
|
|
205
|
+
this.session = await this.request(
|
|
206
|
+
`/games/runtime/sessions/${token}/emit`,
|
|
207
|
+
{
|
|
208
|
+
method: "POST",
|
|
209
|
+
body: JSON.stringify({ type, payload })
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
this.hydrate();
|
|
213
|
+
this.debug("emit", { type, payload: payload ?? null, session: this.session });
|
|
214
|
+
return this.session;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
this.debug("emit", { type, payload: payload ?? null, error: errorMessage(error) });
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async setState(state) {
|
|
221
|
+
try {
|
|
222
|
+
this.snapshot = { ...this.snapshot, state };
|
|
223
|
+
if (this.options.preview) {
|
|
224
|
+
this.session = this.session ?? previewSession();
|
|
225
|
+
this.session.metadata = { snapshot: state };
|
|
226
|
+
this.hydrate();
|
|
227
|
+
this.debug("snapshot", { state, session: this.session, preview: true });
|
|
228
|
+
return this.snapshot;
|
|
229
|
+
}
|
|
230
|
+
const token = this.requireToken();
|
|
231
|
+
this.session = await this.request(
|
|
232
|
+
`/games/runtime/sessions/${token}/snapshot`,
|
|
233
|
+
{
|
|
234
|
+
method: "PUT",
|
|
235
|
+
body: JSON.stringify({ state })
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
this.hydrate();
|
|
239
|
+
this.debug("snapshot", { state, session: this.session });
|
|
240
|
+
return this.snapshot;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
this.debug("snapshot", { state, error: errorMessage(error) });
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
config() {
|
|
247
|
+
const schema = readFormSchema(this.instance?.game?.configSchema);
|
|
248
|
+
const defaults = readDefaults(schema);
|
|
249
|
+
return { ...defaults, ...this.instance?.overrides ?? {}, ...this.injectedConfig };
|
|
250
|
+
}
|
|
251
|
+
hydrate() {
|
|
252
|
+
if (!this.session) return;
|
|
253
|
+
const meta = this.session.metadata;
|
|
254
|
+
this.snapshot = {
|
|
255
|
+
status: this.session.status,
|
|
256
|
+
score: this.session.score ?? null,
|
|
257
|
+
result: this.session.result ?? null,
|
|
258
|
+
state: meta && typeof meta === "object" ? meta.snapshot ?? this.snapshot.state : this.snapshot.state
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
requireToken() {
|
|
262
|
+
if (!this.session?.token) {
|
|
263
|
+
throw new Error("Call connect() before emit or setState");
|
|
264
|
+
}
|
|
265
|
+
return this.session.token;
|
|
266
|
+
}
|
|
267
|
+
debug(event, payload) {
|
|
268
|
+
const w = typeof globalThis !== "undefined" ? globalThis.window : void 0;
|
|
269
|
+
if (!w?.parent?.postMessage) return;
|
|
270
|
+
const message = {
|
|
271
|
+
source: KIT_SOURCE,
|
|
272
|
+
type: "debug",
|
|
273
|
+
event,
|
|
274
|
+
payload: redact(payload),
|
|
275
|
+
at: Date.now()
|
|
276
|
+
};
|
|
277
|
+
try {
|
|
278
|
+
w.parent.postMessage(message, "*");
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async request(path, init = {}) {
|
|
283
|
+
const url = `${this.options.apiUrl}${path}`;
|
|
284
|
+
const method = init.method ?? "GET";
|
|
285
|
+
const headers = new Headers(init.headers);
|
|
286
|
+
if (init.body && !headers.has("content-type")) {
|
|
287
|
+
headers.set("content-type", "application/json");
|
|
288
|
+
}
|
|
289
|
+
const res = await this.fetchWithTimeout(url, { ...init, headers }, method);
|
|
290
|
+
if (!res.ok) {
|
|
291
|
+
throw new KitApiError(res, await readBody(res), method, url);
|
|
292
|
+
}
|
|
293
|
+
return await res.json();
|
|
294
|
+
}
|
|
295
|
+
async fetchWithTimeout(url, init, method) {
|
|
296
|
+
const { timeoutMs } = this.options;
|
|
297
|
+
if (!timeoutMs || timeoutMs <= 0) return fetch(url, init);
|
|
298
|
+
const controller = new AbortController();
|
|
299
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
300
|
+
try {
|
|
301
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (controller.signal.aborted) throw new KitTimeoutError(timeoutMs, method, url);
|
|
304
|
+
throw error;
|
|
305
|
+
} finally {
|
|
306
|
+
clearTimeout(timer);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
async function readBody(res) {
|
|
311
|
+
const text = await res.text();
|
|
312
|
+
if (!text) return void 0;
|
|
313
|
+
try {
|
|
314
|
+
return JSON.parse(text);
|
|
315
|
+
} catch {
|
|
316
|
+
return text;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function createKit(options = {}) {
|
|
320
|
+
const params = typeof location !== "undefined" ? new URLSearchParams(location.search) : new URLSearchParams();
|
|
321
|
+
const preview = options.preview ?? isPreviewFlag(params.get("preview"));
|
|
322
|
+
const apiUrl = options.apiUrl || params.get("apiUrl") || "";
|
|
323
|
+
const gameInstanceId = options.gameInstanceId || params.get("gameInstanceId") || "";
|
|
324
|
+
const playerToken = options.playerToken || params.get("playerToken") || "";
|
|
325
|
+
const config = options.config ?? readQueryConfig();
|
|
326
|
+
return new FzGameKit({
|
|
327
|
+
apiUrl,
|
|
328
|
+
gameInstanceId,
|
|
329
|
+
playerToken,
|
|
330
|
+
config,
|
|
331
|
+
preview,
|
|
332
|
+
timeoutMs: options.timeoutMs
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function readQueryConfig() {
|
|
336
|
+
if (typeof location === "undefined") return void 0;
|
|
337
|
+
const params = new URLSearchParams(location.search);
|
|
338
|
+
return parseConfigParam(params.get("gameSetting")) ?? parseConfigParam(params.get("config"));
|
|
339
|
+
}
|
|
340
|
+
function isPreviewFlag(raw) {
|
|
341
|
+
if (!raw) return false;
|
|
342
|
+
const value = raw.trim().toLowerCase();
|
|
343
|
+
return value === "true" || value === "1" || value === "yes";
|
|
344
|
+
}
|
|
345
|
+
function parseConfigParam(raw) {
|
|
346
|
+
if (!raw) return void 0;
|
|
347
|
+
try {
|
|
348
|
+
const parsed = JSON.parse(raw);
|
|
349
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
350
|
+
return parsed;
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
return void 0;
|
|
355
|
+
}
|
|
356
|
+
function previewSession() {
|
|
357
|
+
return {
|
|
358
|
+
id: "preview",
|
|
359
|
+
token: "preview",
|
|
360
|
+
status: "CREATED",
|
|
361
|
+
score: null,
|
|
362
|
+
result: null,
|
|
363
|
+
metadata: { snapshot: {} }
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function applyLocalEmit(session, type, payload) {
|
|
367
|
+
if (type === "ready") session.status = "READY";
|
|
368
|
+
if (type === "start") session.status = "PLAYING";
|
|
369
|
+
if (type === "pause") session.status = "PAUSED";
|
|
370
|
+
if (type === "finish") {
|
|
371
|
+
session.status = "FINISHED";
|
|
372
|
+
if (typeof payload?.score === "number") session.score = payload.score;
|
|
373
|
+
if (payload && "result" in payload) session.result = payload.result;
|
|
374
|
+
}
|
|
375
|
+
if (type === "abort") session.status = "ABORTED";
|
|
376
|
+
if (type === "score" && typeof payload?.score === "number") session.score = payload.score;
|
|
377
|
+
if (type === "snapshot") {
|
|
378
|
+
session.metadata = { snapshot: payload?.state ?? payload ?? {} };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function errorMessage(error) {
|
|
382
|
+
return error instanceof Error ? error.message : String(error);
|
|
383
|
+
}
|
|
384
|
+
function readFormSchema(schema) {
|
|
385
|
+
if (!schema) return {};
|
|
386
|
+
const form = schema.formSchema;
|
|
387
|
+
if (form && typeof form === "object" && !Array.isArray(form)) {
|
|
388
|
+
return form;
|
|
389
|
+
}
|
|
390
|
+
return schema;
|
|
391
|
+
}
|
|
392
|
+
function readDefaults(schema) {
|
|
393
|
+
const properties = schema.properties;
|
|
394
|
+
if (!properties || typeof properties !== "object") return {};
|
|
395
|
+
const out = {};
|
|
396
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
397
|
+
if (value && "default" in value) out[key] = value.default;
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
var EDITOR_SOURCE = "fzgame-editor";
|
|
402
|
+
var HOST_SOURCE = "fzgame-host";
|
|
403
|
+
function postEditorSave(config, target = window.parent) {
|
|
404
|
+
const message = { source: EDITOR_SOURCE, type: "save", config };
|
|
405
|
+
target.postMessage(message, "*");
|
|
406
|
+
}
|
|
407
|
+
function onHostInit(handler) {
|
|
408
|
+
const listener = (event) => {
|
|
409
|
+
const data = event.data;
|
|
410
|
+
if (!data || data.source !== HOST_SOURCE || data.type !== "init") return;
|
|
411
|
+
handler(data);
|
|
412
|
+
};
|
|
413
|
+
window.addEventListener("message", listener);
|
|
414
|
+
return () => window.removeEventListener("message", listener);
|
|
415
|
+
}
|
|
416
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
417
|
+
0 && (module.exports = {
|
|
418
|
+
DEFAULT_TIMEOUT_MS,
|
|
419
|
+
EDITOR_SOURCE,
|
|
420
|
+
FzGameKit,
|
|
421
|
+
HOST_SOURCE,
|
|
422
|
+
KIT_SOURCE,
|
|
423
|
+
KitApiError,
|
|
424
|
+
KitTimeoutError,
|
|
425
|
+
LIFECYCLE_EVENTS,
|
|
426
|
+
PLAY_SESSION_STATUSES,
|
|
427
|
+
createKit,
|
|
428
|
+
emptySnapshot,
|
|
429
|
+
isPreviewFlag,
|
|
430
|
+
onHostInit,
|
|
431
|
+
postEditorSave,
|
|
432
|
+
readFormSchema,
|
|
433
|
+
readQueryConfig
|
|
434
|
+
});
|
|
435
|
+
//# sourceMappingURL=index.cjs.map
|