@paizogames/sdk 0.0.0-stage → 1.0.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/PROTOCOL.md +54 -0
- package/README.md +100 -2
- package/index.d.ts +36 -0
- package/index.js +6 -0
- package/package.json +37 -4
- package/sdk/v1/paizo-sdk.js +379 -0
package/PROTOCOL.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# SDK ⇄ portal protocol (v1)
|
|
2
|
+
|
|
3
|
+
The game iframe and the portal talk with `window.postMessage`. Every message
|
|
4
|
+
is an object with `paizo: 1`.
|
|
5
|
+
|
|
6
|
+
- SDK side: `sdk/v1/paizo-sdk.js`
|
|
7
|
+
- Portal side: `src/lib/paizo-host/` and `src/components/Portal/GamePlayer.jsx`
|
|
8
|
+
in `paizogames-nextjs`
|
|
9
|
+
|
|
10
|
+
## Envelope
|
|
11
|
+
|
|
12
|
+
| Direction | Shape | Meaning |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| game → portal | `{ paizo, type, id, payload }` | Request. Gets exactly one reply. |
|
|
15
|
+
| game → portal | `{ paizo, type, payload }` | Notification. No reply. |
|
|
16
|
+
| portal → game | `{ paizo, id, ok: true, result }` | Reply. |
|
|
17
|
+
| portal → game | `{ paizo, id, ok: false, error }` | Failed reply. |
|
|
18
|
+
| portal → game | `{ paizo, event, id }` | Event tied to the request `id`. |
|
|
19
|
+
|
|
20
|
+
## Messages
|
|
21
|
+
|
|
22
|
+
| `type` | Kind | Payload | Result |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `init` | request | `{ sdkVersion, saveUpdatedAt }` | `{ user, save }` where `user` is `{ username, avatarUrl }` or `null`, and `save` is `{ updatedAt, data }` or `null` |
|
|
25
|
+
| `loaded` | notification | `{}` | |
|
|
26
|
+
| `gameplay` | notification | `{ playing: boolean }` | |
|
|
27
|
+
| `save` | notification | `{ updatedAt, data }` — `data` is the game's whole `localStorage` as `{ key: string }` | |
|
|
28
|
+
| `ad` | request | `{ kind: "commercial" \| "rewarded" }` | `{ shown: boolean }` |
|
|
29
|
+
| `login` | request | `{}` | `{ user }`, or an error while accounts do not exist |
|
|
30
|
+
|
|
31
|
+
Events: `adStarted` — sent before the `ad` reply, only if an ad actually starts.
|
|
32
|
+
|
|
33
|
+
## Trust
|
|
34
|
+
|
|
35
|
+
- The SDK accepts messages only from `window.parent`, and only if the parent's
|
|
36
|
+
origin is `paizogames.com`, a subdomain of it, or `localhost`. The first
|
|
37
|
+
message is sent to `*` (it carries nothing private); after the handshake the
|
|
38
|
+
SDK talks only to the origin that answered.
|
|
39
|
+
- The portal accepts messages only from the game iframe's window and only from
|
|
40
|
+
the game's own origin.
|
|
41
|
+
|
|
42
|
+
## Save conflict rule
|
|
43
|
+
|
|
44
|
+
Both sides keep `updatedAt` (ms). On `init`, the newer copy wins: if the
|
|
45
|
+
portal's is newer the SDK replaces the game's `localStorage` with it, otherwise
|
|
46
|
+
the SDK pushes the local copy. The portal ignores a `save` older than what it has.
|
|
47
|
+
|
|
48
|
+
## Ad rules (portal)
|
|
49
|
+
|
|
50
|
+
- `commercial`: shown only if the configured interval has passed since the last
|
|
51
|
+
ad of any kind. The interval starts when the game opens.
|
|
52
|
+
- `rewarded`: always attempted; resets the interval.
|
|
53
|
+
- With no ad provider configured, `commercial` replies `shown: false` and
|
|
54
|
+
`rewarded` replies `shown: true`, so games stay fully playable.
|
package/README.md
CHANGED
|
@@ -1,3 +1,101 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Paizo Games SDK
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The SDK every game on [paizogames.com](https://paizogames.com) loads. It connects
|
|
4
|
+
a game, running in a cross-origin iframe, to the portal that embeds it.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
sdk/v1/paizo-sdk.js the SDK (plain JS, no build step)
|
|
8
|
+
games/<slug>/ games; games/glide is the reference integration
|
|
9
|
+
scripts/serve.mjs local stand-in for the games CDN
|
|
10
|
+
PROTOCOL.md the postMessage protocol between SDK and portal
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Run locally
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm run dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Serves every game on its own origin, `http://<slug>.localhost:4100/`, and the
|
|
20
|
+
SDK at `/sdk/v1/paizo-sdk.js` on each of them. Start the portal
|
|
21
|
+
(`paizogames-nextjs`, port 3002) to play a game embedded the way players see it.
|
|
22
|
+
|
|
23
|
+
## Integrating a game
|
|
24
|
+
|
|
25
|
+
A game is a static folder with an `index.html`. Rules:
|
|
26
|
+
|
|
27
|
+
1. Load the SDK by URL, before the game's own scripts. Never copy it into the game.
|
|
28
|
+
```html
|
|
29
|
+
<script src="/sdk/v1/paizo-sdk.js"></script>
|
|
30
|
+
```
|
|
31
|
+
Games built with a bundler can use the npm package instead. It is the same
|
|
32
|
+
file, plus TypeScript types:
|
|
33
|
+
```bash
|
|
34
|
+
npm install @paizogames/sdk
|
|
35
|
+
```
|
|
36
|
+
```js
|
|
37
|
+
import PaizoSDK from "@paizogames/sdk";
|
|
38
|
+
```
|
|
39
|
+
The script tag is preferred: the portal can then ship SDK fixes without the
|
|
40
|
+
game being rebuilt.
|
|
41
|
+
2. Call `PaizoSDK.init()` first and wait for it.
|
|
42
|
+
3. Save progress with plain `localStorage`. The SDK syncs it; there is no save API.
|
|
43
|
+
4. No login, no accounts and no ads of your own inside the game.
|
|
44
|
+
5. The game must work when an ad is not shown and when opened outside the portal.
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
await PaizoSDK.init();
|
|
48
|
+
// ...load assets...
|
|
49
|
+
PaizoSDK.gameLoadingFinished();
|
|
50
|
+
|
|
51
|
+
PaizoSDK.gameplayStart(); // level starts / unpause
|
|
52
|
+
PaizoSDK.gameplayStop(); // level ends / pause / menu
|
|
53
|
+
|
|
54
|
+
await PaizoSDK.commercialBreak(); // between levels; may show nothing
|
|
55
|
+
startNextLevel();
|
|
56
|
+
|
|
57
|
+
const rewarded = await PaizoSDK.rewardedBreak(); // player clicked "watch ad"
|
|
58
|
+
if (rewarded) giveReward();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API
|
|
62
|
+
|
|
63
|
+
| Call | Returns | Notes |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `init()` | `Promise<void>` | Never rejects. Every other call waits for it. |
|
|
66
|
+
| `environment` | `"paizo"` \| `"local"` | `local` = not embedded in the portal. |
|
|
67
|
+
| `gameLoadingFinished()` | `Promise<void>` | Hides the portal's loading screen. |
|
|
68
|
+
| `gameplayStart()` / `gameplayStop()` | `Promise<void>` | Used for play-time stats. |
|
|
69
|
+
| `commercialBreak(onStart?)` | `Promise<void>` | Resolves when the game may continue. The portal decides whether an ad plays. |
|
|
70
|
+
| `rewardedBreak(onStart?)` | `Promise<boolean>` | `true` only if the reward should be granted. |
|
|
71
|
+
| `getUser()` | `Promise<{ username, avatarUrl } \| null>` | `null` for guests (everyone, for now). |
|
|
72
|
+
| `login()` | `Promise<user>` | Rejects until the portal has accounts. |
|
|
73
|
+
|
|
74
|
+
`onStart` is called only if an ad really starts; mute audio there.
|
|
75
|
+
|
|
76
|
+
Call `commercialBreak()` at every natural break. Frequency is the portal's job.
|
|
77
|
+
|
|
78
|
+
### Saves
|
|
79
|
+
|
|
80
|
+
The SDK watches the game's `localStorage` and mirrors it to the portal (at most
|
|
81
|
+
every 2 seconds, and when the page is hidden). On `init()` it restores the
|
|
82
|
+
portal's copy if that one is newer. Limits and escape hatch:
|
|
83
|
+
|
|
84
|
+
- 1 MB as JSON. Past that, syncing stops for that player.
|
|
85
|
+
- Keys starting with `paizo_ignore` are never synced (caches, temporary state).
|
|
86
|
+
|
|
87
|
+
### Local mode
|
|
88
|
+
|
|
89
|
+
Opened directly, or when no portal answers within 1.5 s, the SDK runs in
|
|
90
|
+
`local` mode: ads are a one-second placeholder, rewards are granted, saves stay
|
|
91
|
+
in `localStorage`. Games never need to branch on the environment.
|
|
92
|
+
|
|
93
|
+
## Publishing a game
|
|
94
|
+
|
|
95
|
+
1. Upload `games/<slug>/` to the games host so it is served at
|
|
96
|
+
`https://<slug>.games.paizogames.com/`. The same host must serve this repo's
|
|
97
|
+
`sdk/` folder at `/sdk/`.
|
|
98
|
+
2. Add the game to the portal catalog: `src/data/games.js` in `paizogames-nextjs`.
|
|
99
|
+
|
|
100
|
+
Each game gets its own origin so that games cannot read each other's storage
|
|
101
|
+
or the portal's.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface PaizoUser {
|
|
2
|
+
username: string;
|
|
3
|
+
avatarUrl: string | null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface PaizoSDKApi {
|
|
7
|
+
/** SDK version, e.g. "1.0.0". */
|
|
8
|
+
readonly version: string;
|
|
9
|
+
/** "paizo" inside the portal, "local" anywhere else (stubs, still playable). */
|
|
10
|
+
readonly environment: "paizo" | "local";
|
|
11
|
+
/** Connects to the portal and restores the save. Call once, before the game starts. */
|
|
12
|
+
init(): Promise<void>;
|
|
13
|
+
/** The game has loaded and can be shown. */
|
|
14
|
+
gameLoadingFinished(): void;
|
|
15
|
+
/** The player is actively playing. */
|
|
16
|
+
gameplayStart(): void;
|
|
17
|
+
/** Play paused or ended (menu, level complete, game over). */
|
|
18
|
+
gameplayStop(): void;
|
|
19
|
+
/** Ad between rounds. The portal decides whether one is shown. Always resolves. */
|
|
20
|
+
commercialBreak(onStart?: () => void): Promise<void>;
|
|
21
|
+
/** Opt-in ad. Resolves true only if the player earned the reward. */
|
|
22
|
+
rewardedBreak(onStart?: () => void): Promise<boolean>;
|
|
23
|
+
/** The signed-in player, or null for a guest. */
|
|
24
|
+
getUser(): Promise<PaizoUser | null>;
|
|
25
|
+
/** Opens the portal's sign-in. Rejects while accounts are unavailable. */
|
|
26
|
+
login(): Promise<PaizoUser | null>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export declare const PaizoSDK: PaizoSDKApi;
|
|
30
|
+
export default PaizoSDK;
|
|
31
|
+
|
|
32
|
+
declare global {
|
|
33
|
+
interface Window {
|
|
34
|
+
PaizoSDK: PaizoSDKApi;
|
|
35
|
+
}
|
|
36
|
+
}
|
package/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paizogames/sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Paizo Games SDK: ads, saves and user identity for games running on paizogames.com.",
|
|
5
|
+
"keywords": ["paizo", "paizogames", "html5-games", "game-sdk", "web-games"],
|
|
6
|
+
"homepage": "https://paizogames.com",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/lowtechteam/paizogames-sdk.git"
|
|
10
|
+
},
|
|
11
|
+
"license": "UNLICENSED",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./index.js",
|
|
17
|
+
"types": "./index.d.ts",
|
|
18
|
+
"unpkg": "./sdk/v1/paizo-sdk.js",
|
|
19
|
+
"jsdelivr": "./sdk/v1/paizo-sdk.js",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./index.d.ts",
|
|
23
|
+
"default": "./index.js"
|
|
24
|
+
},
|
|
25
|
+
"./script": "./sdk/v1/paizo-sdk.js",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"sideEffects": true,
|
|
29
|
+
"files": [
|
|
30
|
+
"index.js",
|
|
31
|
+
"index.d.ts",
|
|
32
|
+
"sdk/v1/paizo-sdk.js",
|
|
33
|
+
"PROTOCOL.md"
|
|
34
|
+
],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"dev": "node scripts/serve.mjs",
|
|
37
|
+
"levels:glide": "node scripts/gen-glide-levels.mjs"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Paizo Games SDK v1
|
|
3
|
+
*
|
|
4
|
+
* Loaded by every game with:
|
|
5
|
+
* <script src="/sdk/v1/paizo-sdk.js"></script>
|
|
6
|
+
*
|
|
7
|
+
* The game runs in a cross-origin iframe on the portal. This file is a thin
|
|
8
|
+
* postMessage client: the portal ("host") decides whether an ad is shown,
|
|
9
|
+
* who the user is and where saves are kept. See PROTOCOL.md.
|
|
10
|
+
*/
|
|
11
|
+
(function (global) {
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
if (global.PaizoSDK) return;
|
|
15
|
+
|
|
16
|
+
var VERSION = "1.0.0";
|
|
17
|
+
var PROTOCOL = 1;
|
|
18
|
+
var HANDSHAKE_TIMEOUT_MS = 1500;
|
|
19
|
+
var AD_TIMEOUT_MS = 120000;
|
|
20
|
+
var SAVE_POLL_MS = 2000;
|
|
21
|
+
var SAVE_LIMIT_BYTES = 1024 * 1024;
|
|
22
|
+
var IGNORE_PREFIX = "paizo_ignore";
|
|
23
|
+
var META_KEY = IGNORE_PREFIX + ":meta";
|
|
24
|
+
|
|
25
|
+
// Only these parents are accepted as a host.
|
|
26
|
+
var TRUSTED_HOST = [
|
|
27
|
+
/^https:\/\/([a-z0-9-]+\.)*paizogames\.com$/,
|
|
28
|
+
/^http:\/\/localhost(:\d+)?$/,
|
|
29
|
+
/^http:\/\/127\.0\.0\.1(:\d+)?$/,
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
var environment = "local"; // "paizo" once the portal answers the handshake
|
|
33
|
+
var hostOrigin = null;
|
|
34
|
+
var user = null;
|
|
35
|
+
var initPromise = null;
|
|
36
|
+
var seq = 0;
|
|
37
|
+
var pending = {}; // id -> { resolve, reject, onStart, timer }
|
|
38
|
+
var playing = false;
|
|
39
|
+
var loadedSent = false;
|
|
40
|
+
var adInProgress = false;
|
|
41
|
+
|
|
42
|
+
function log() {
|
|
43
|
+
if (!global.console) return;
|
|
44
|
+
var args = ["[PaizoSDK]"].concat([].slice.call(arguments));
|
|
45
|
+
console.log.apply(console, args);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isTrusted(origin) {
|
|
49
|
+
for (var i = 0; i < TRUSTED_HOST.length; i++) {
|
|
50
|
+
if (TRUSTED_HOST[i].test(origin)) return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------- transport
|
|
56
|
+
|
|
57
|
+
function post(message, targetOrigin) {
|
|
58
|
+
message.paizo = PROTOCOL;
|
|
59
|
+
global.parent.postMessage(message, targetOrigin || hostOrigin || "*");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function request(type, payload, timeoutMs, onStart) {
|
|
63
|
+
return new Promise(function (resolve, reject) {
|
|
64
|
+
var id = "r" + ++seq;
|
|
65
|
+
var entry = { resolve: resolve, reject: reject, onStart: onStart };
|
|
66
|
+
if (timeoutMs) {
|
|
67
|
+
entry.timer = setTimeout(function () {
|
|
68
|
+
delete pending[id];
|
|
69
|
+
reject(new Error("PaizoSDK: '" + type + "' timed out"));
|
|
70
|
+
}, timeoutMs);
|
|
71
|
+
}
|
|
72
|
+
pending[id] = entry;
|
|
73
|
+
post({ type: type, id: id, payload: payload || {} });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function notify(type, payload) {
|
|
78
|
+
if (environment !== "paizo") return;
|
|
79
|
+
post({ type: type, payload: payload || {} });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
global.addEventListener("message", function (event) {
|
|
83
|
+
var data = event.data;
|
|
84
|
+
if (!data || data.paizo !== PROTOCOL) return;
|
|
85
|
+
if (event.source !== global.parent) return;
|
|
86
|
+
if (hostOrigin ? event.origin !== hostOrigin : !isTrusted(event.origin)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (data.event === "adStarted") {
|
|
91
|
+
var ad = pending[data.id];
|
|
92
|
+
if (ad && typeof ad.onStart === "function") {
|
|
93
|
+
try {
|
|
94
|
+
ad.onStart();
|
|
95
|
+
} catch (e) {
|
|
96
|
+
log("onStart callback threw", e);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
var entry = pending[data.id];
|
|
103
|
+
if (!entry) return;
|
|
104
|
+
delete pending[data.id];
|
|
105
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
106
|
+
if (!hostOrigin) hostOrigin = event.origin;
|
|
107
|
+
if (data.ok) entry.resolve(data.result);
|
|
108
|
+
else entry.reject(new Error(data.error || "PaizoSDK: request failed"));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// -------------------------------------------------------------------- saves
|
|
112
|
+
// Games just use localStorage. The SDK mirrors it to the portal, which keeps
|
|
113
|
+
// a copy per game (and, once accounts exist, syncs that copy to the cloud).
|
|
114
|
+
|
|
115
|
+
var saveEnabled = true;
|
|
116
|
+
var lastSnapshot = "{}";
|
|
117
|
+
|
|
118
|
+
function storage() {
|
|
119
|
+
try {
|
|
120
|
+
return global.localStorage;
|
|
121
|
+
} catch (e) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readMeta() {
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(storage().getItem(META_KEY)) || {};
|
|
129
|
+
} catch (e) {
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function writeMeta(meta) {
|
|
135
|
+
try {
|
|
136
|
+
storage().setItem(META_KEY, JSON.stringify(meta));
|
|
137
|
+
} catch (e) {}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function snapshot() {
|
|
141
|
+
var ls = storage();
|
|
142
|
+
var keys = [];
|
|
143
|
+
for (var i = 0; i < ls.length; i++) {
|
|
144
|
+
var key = ls.key(i);
|
|
145
|
+
if (key.indexOf(IGNORE_PREFIX) !== 0) keys.push(key);
|
|
146
|
+
}
|
|
147
|
+
keys.sort();
|
|
148
|
+
var data = {};
|
|
149
|
+
for (var j = 0; j < keys.length; j++) data[keys[j]] = ls.getItem(keys[j]);
|
|
150
|
+
return JSON.stringify(data);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function restore(data) {
|
|
154
|
+
var ls = storage();
|
|
155
|
+
var remove = [];
|
|
156
|
+
for (var i = 0; i < ls.length; i++) {
|
|
157
|
+
var key = ls.key(i);
|
|
158
|
+
if (key.indexOf(IGNORE_PREFIX) !== 0) remove.push(key);
|
|
159
|
+
}
|
|
160
|
+
remove.forEach(function (key) {
|
|
161
|
+
ls.removeItem(key);
|
|
162
|
+
});
|
|
163
|
+
Object.keys(data).forEach(function (key) {
|
|
164
|
+
ls.setItem(key, data[key]);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function flushSave() {
|
|
169
|
+
if (!saveEnabled || environment !== "paizo") return;
|
|
170
|
+
var current;
|
|
171
|
+
try {
|
|
172
|
+
current = snapshot();
|
|
173
|
+
} catch (e) {
|
|
174
|
+
saveEnabled = false;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (current === lastSnapshot) return;
|
|
178
|
+
if (current.length > SAVE_LIMIT_BYTES) {
|
|
179
|
+
saveEnabled = false;
|
|
180
|
+
log("save data is over 1MB, sync is now off for this player");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
lastSnapshot = current;
|
|
184
|
+
var updatedAt = Date.now();
|
|
185
|
+
writeMeta({ updatedAt: updatedAt });
|
|
186
|
+
notify("save", { updatedAt: updatedAt, data: JSON.parse(current) });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function startSaveSync(hostSave) {
|
|
190
|
+
if (!storage()) {
|
|
191
|
+
saveEnabled = false;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
var localAt = readMeta().updatedAt || 0;
|
|
196
|
+
if (hostSave && hostSave.data && hostSave.updatedAt > localAt) {
|
|
197
|
+
// The portal has a newer copy (e.g. from another device): use it.
|
|
198
|
+
restore(hostSave.data);
|
|
199
|
+
writeMeta({ updatedAt: hostSave.updatedAt });
|
|
200
|
+
lastSnapshot = snapshot();
|
|
201
|
+
} else {
|
|
202
|
+
// Local copy wins; anything the portal is missing is pushed below.
|
|
203
|
+
lastSnapshot = JSON.stringify((hostSave && hostSave.data) || {});
|
|
204
|
+
}
|
|
205
|
+
} catch (e) {
|
|
206
|
+
saveEnabled = false;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
flushSave();
|
|
210
|
+
setInterval(flushSave, SAVE_POLL_MS);
|
|
211
|
+
global.addEventListener("pagehide", flushSave);
|
|
212
|
+
document.addEventListener("visibilitychange", function () {
|
|
213
|
+
if (document.visibilityState === "hidden") flushSave();
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ------------------------------------------------------------ local fallback
|
|
218
|
+
// Outside the portal (opened directly, or on localhost during development)
|
|
219
|
+
// every call still works, so a game never has to branch on the environment.
|
|
220
|
+
|
|
221
|
+
function localAd(kind) {
|
|
222
|
+
return new Promise(function (resolve) {
|
|
223
|
+
var el = document.createElement("div");
|
|
224
|
+
el.textContent = "Paizo SDK (local) — " + kind + " ad placeholder";
|
|
225
|
+
el.style.cssText =
|
|
226
|
+
"position:fixed;inset:0;z-index:2147483647;display:flex;" +
|
|
227
|
+
"align-items:center;justify-content:center;background:rgba(0,0,0,.85);" +
|
|
228
|
+
"color:#fff;font:600 16px system-ui,sans-serif;text-align:center;padding:24px";
|
|
229
|
+
document.body.appendChild(el);
|
|
230
|
+
setTimeout(function () {
|
|
231
|
+
el.remove();
|
|
232
|
+
resolve();
|
|
233
|
+
}, 1200);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// --------------------------------------------------------------- public API
|
|
238
|
+
|
|
239
|
+
function init() {
|
|
240
|
+
if (initPromise) return initPromise;
|
|
241
|
+
initPromise = new Promise(function (resolve) {
|
|
242
|
+
function finish() {
|
|
243
|
+
log("v" + VERSION + " ready, environment: " + environment);
|
|
244
|
+
resolve();
|
|
245
|
+
}
|
|
246
|
+
if (global.parent === global) return finish();
|
|
247
|
+
|
|
248
|
+
var localAt = 0;
|
|
249
|
+
try {
|
|
250
|
+
localAt = readMeta().updatedAt || 0;
|
|
251
|
+
} catch (e) {}
|
|
252
|
+
|
|
253
|
+
request(
|
|
254
|
+
"init",
|
|
255
|
+
{ sdkVersion: VERSION, saveUpdatedAt: localAt },
|
|
256
|
+
HANDSHAKE_TIMEOUT_MS
|
|
257
|
+
).then(
|
|
258
|
+
function (result) {
|
|
259
|
+
environment = "paizo";
|
|
260
|
+
user = (result && result.user) || null;
|
|
261
|
+
startSaveSync(result && result.save);
|
|
262
|
+
finish();
|
|
263
|
+
},
|
|
264
|
+
function () {
|
|
265
|
+
hostOrigin = null;
|
|
266
|
+
finish();
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
return initPromise;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function gameLoadingFinished() {
|
|
274
|
+
return init().then(function () {
|
|
275
|
+
if (loadedSent) return;
|
|
276
|
+
loadedSent = true;
|
|
277
|
+
notify("loaded");
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function setPlaying(value) {
|
|
282
|
+
return init().then(function () {
|
|
283
|
+
if (playing === value) return;
|
|
284
|
+
playing = value;
|
|
285
|
+
notify("gameplay", { playing: value });
|
|
286
|
+
if (!value) flushSave();
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function adBreak(kind, onStart) {
|
|
291
|
+
return init().then(function () {
|
|
292
|
+
if (adInProgress) return false;
|
|
293
|
+
adInProgress = true;
|
|
294
|
+
var done = function (shown) {
|
|
295
|
+
adInProgress = false;
|
|
296
|
+
return shown;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
if (environment !== "paizo") {
|
|
300
|
+
if (typeof onStart === "function") onStart();
|
|
301
|
+
return localAd(kind).then(function () {
|
|
302
|
+
return done(true);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return request("ad", { kind: kind }, AD_TIMEOUT_MS, onStart).then(
|
|
307
|
+
function (result) {
|
|
308
|
+
return done(!!(result && result.shown));
|
|
309
|
+
},
|
|
310
|
+
function () {
|
|
311
|
+
return done(false);
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
global.PaizoSDK = {
|
|
318
|
+
version: VERSION,
|
|
319
|
+
|
|
320
|
+
/** Call once, before anything else. Never rejects. */
|
|
321
|
+
init: init,
|
|
322
|
+
|
|
323
|
+
/** "paizo" inside the portal, "local" anywhere else. Valid after init(). */
|
|
324
|
+
get environment() {
|
|
325
|
+
return environment;
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
/** The game has finished loading and is ready to be played. */
|
|
329
|
+
gameLoadingFinished: gameLoadingFinished,
|
|
330
|
+
|
|
331
|
+
/** The player is actively playing (level start, unpause). */
|
|
332
|
+
gameplayStart: function () {
|
|
333
|
+
return setPlaying(true);
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
/** The player stopped playing (level end, game over, pause, menu). */
|
|
337
|
+
gameplayStop: function () {
|
|
338
|
+
return setPlaying(false);
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Ask for an ad at a natural break. The portal decides whether one is
|
|
343
|
+
* actually shown. Resolves when the game may continue.
|
|
344
|
+
* @param {Function} [onStart] called if an ad really starts: mute here.
|
|
345
|
+
*/
|
|
346
|
+
commercialBreak: function (onStart) {
|
|
347
|
+
return adBreak("commercial", onStart).then(function () {});
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Offer a rewarded ad the player chose to watch.
|
|
352
|
+
* Resolves true only if the reward should be granted.
|
|
353
|
+
* @param {Function} [onStart] called if an ad really starts: mute here.
|
|
354
|
+
*/
|
|
355
|
+
rewardedBreak: function (onStart) {
|
|
356
|
+
return adBreak("rewarded", onStart);
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
/** Resolves { username, avatarUrl } or null for a guest. */
|
|
360
|
+
getUser: function () {
|
|
361
|
+
return init().then(function () {
|
|
362
|
+
return user;
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
/** Opens the portal's sign-in. Rejects while accounts are unavailable. */
|
|
367
|
+
login: function () {
|
|
368
|
+
return init().then(function () {
|
|
369
|
+
if (environment !== "paizo") {
|
|
370
|
+
throw new Error("PaizoSDK: login is unavailable outside the portal");
|
|
371
|
+
}
|
|
372
|
+
return request("login", {}, AD_TIMEOUT_MS).then(function (result) {
|
|
373
|
+
user = (result && result.user) || null;
|
|
374
|
+
return user;
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
})(window);
|