@ikirmikir/game-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/LICENSE +21 -0
- package/README.md +26 -2
- package/dist/ikirmikir.js +2 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +182 -0
- package/dist/protocol.d.ts +56 -0
- package/dist/protocol.js +57 -0
- package/package.json +37 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maruf (Ikirmikir)
|
|
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,27 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ikirmikir/game-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Optional SDK for browser games on [Ikirmikir](https://ikirmikir.com): real loading progress, gameplay start and stop, pause and resume, the player's sound switch, player context (language, device, touch) and sharing. Any engine that runs in a browser. Outside Ikirmikir it runs in local mode, so one build works everywhere, and no call ever throws.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @ikirmikir/game-sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { Ikirmikir } from "@ikirmikir/game-sdk";
|
|
11
|
+
|
|
12
|
+
const ik = await Ikirmikir.init({ gameId: "your-game" });
|
|
13
|
+
ik.game.loadingProgress(50);
|
|
14
|
+
ik.game.loadingDone();
|
|
15
|
+
ik.on("pause", () => game.pause());
|
|
16
|
+
ik.on("mute", muted => { audio.muted = muted; });
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Without a bundler, use `dist/ikirmikir.js` (gives `window.Ikirmikir`), from this package or the CDN:
|
|
20
|
+
|
|
21
|
+
```html
|
|
22
|
+
<script src="https://cdn.jsdelivr.net/npm/@ikirmikir/game-sdk@1.0.0/dist/ikirmikir.js"></script>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Quick start, API reference and a test tool: https://ikirmikir.com/developers/
|
|
26
|
+
|
|
27
|
+
MIT license.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/*! @ikirmikir/game-sdk 1.0.0 | MIT | https://ikirmikir.com/developers/ */
|
|
2
|
+
"use strict";(()=>{var I=["loading","gameplay","pause","mute","context","share"],h=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),P=(e,t)=>typeof e=="string"&&e.length>0&&e.length<=t,C=(e,t,i)=>Number.isInteger(e)&&e>=t&&e<=i,c=e=>typeof e=="boolean";function x(e){if(!h(e)||e.ik!==1)return null;switch(e.type){case"welcome":{let t=e.context;return!Array.isArray(e.features)||!c(e.muted)||!h(t)||!P(t.language,35)||t.device!=="mobile"&&t.device!=="desktop"||!c(t.touch)?null:{ik:1,type:"welcome",features:I.filter(u=>e.features.includes(u)),context:{language:t.language,device:t.device,touch:t.touch},muted:e.muted}}case"pause":return{ik:1,type:"pause"};case"resume":return{ik:1,type:"resume"};case"mute":return c(e.muted)?{ik:1,type:"mute",muted:e.muted}:null;case"shared":return C(e.id,0,1e9)&&c(e.shared)?{ik:1,type:"shared",id:e.id,shared:e.shared}:null;default:return null}}function b(e){return e==="https://ikirmikir.com"||e==="http://localhost:3000"||e==="http://127.0.0.1:3000"||/^https:\/\/([0-9a-f]{8}\.)?ikirmikir\.pages\.dev$/.test(e)}var M="1.0.0",R=1500,w;function T(e){return w??(w=F(e))}async function F({gameId:e}){let t={pause:new Set,resume:new Set,mute:new Set},i=(r,...a)=>t[r].forEach(o=>{try{o(...a)}catch(n){console.error(n)}}),u="local",f=A(),p=!1,g=[],s,m=new Map,S=1,k=-1,l=!1,y=!1,d=r=>{if(s)try{parent.postMessage({ik:1,...r},s)}catch{}};if(typeof window<"u"&&window.parent!==window){let r=new Promise(a=>{addEventListener("message",o=>{if(o.source!==window.parent||!b(o.origin)||s&&o.origin!==s)return;let n=x(o.data);if(n){if(n.type==="welcome")return s?void 0:(s=o.origin,u="ikirmikir",f=n.context,p=n.muted,g=n.features,a(!0));s&&(n.type==="pause"||n.type==="resume"?i(n.type):n.type==="mute"?n.muted!==p&&(p=n.muted,i("mute",p)):n.type==="shared"&&(m.get(n.id)?.(n.shared),m.delete(n.id)))}}),setTimeout(()=>a(!1),R)});try{parent.postMessage({ik:1,type:"hello",gameId:String(e).slice(0,64),sdk:M},"*")}catch{}await r}return u==="local"&&(g=["gameplay","loading","pause","context",...E()?["share"]:[]],typeof document<"u"&&document.addEventListener("visibilitychange",()=>i(document.hidden?"pause":"resume"))),{get env(){return u},get context(){return{...f}},get muted(){return p},has:r=>g.includes(r),game:{loadingProgress(r){let a=Math.max(0,Math.min(100,Math.round(Number(r)||0)));y||a===k||(k=a,d({type:"loading",progress:a}))},loadingDone(){y||(y=!0,d({type:"ready"}))},gameplayStart(){l||(l=!0,d({type:"gameplay",playing:l}))},gameplayStop(){l&&(l=!1,d({type:"gameplay",playing:l}))}},on(r,a){let o=t[r];return!o||typeof a!="function"?()=>{}:(o.add(a),()=>{o.delete(a)})},async share(){if(u==="local"){if(!E())return{shared:!1};try{return await navigator.share({url:location.href}),{shared:!0}}catch{return{shared:!1}}}let r=S++;return new Promise(a=>{m.set(r,o=>a({shared:o})),setTimeout(()=>{m.delete(r)&&a({shared:!1})},6e4),d({type:"share",id:r})})}}}function A(){let e=typeof navigator<"u"?navigator:void 0,t=!!e&&e.maxTouchPoints>0,i=typeof matchMedia<"u"&&matchMedia("(pointer: coarse)").matches;return{language:(e?.language||"en").split("-")[0],device:i?"mobile":"desktop",touch:t}}var E=()=>typeof navigator<"u"&&typeof navigator.share=="function",O={init:T,VERSION:M};globalThis.Ikirmikir=O;})();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type Context, type Feature } from "./protocol";
|
|
2
|
+
export type { Context, Feature } from "./protocol";
|
|
3
|
+
export declare const VERSION = "1.0.0";
|
|
4
|
+
type Events = {
|
|
5
|
+
pause: () => void;
|
|
6
|
+
resume: () => void;
|
|
7
|
+
mute: (muted: boolean) => void;
|
|
8
|
+
};
|
|
9
|
+
export interface Sdk {
|
|
10
|
+
/** "ikirmikir" inside the Ikirmikir player, "local" anywhere else. */
|
|
11
|
+
readonly env: "ikirmikir" | "local";
|
|
12
|
+
/** Language, device and input of the player. */
|
|
13
|
+
readonly context: Context;
|
|
14
|
+
/** True when Ikirmikir's sound switch is off: keep the game silent. */
|
|
15
|
+
readonly muted: boolean;
|
|
16
|
+
/** Whether a feature works here. */
|
|
17
|
+
has(feature: Feature): boolean;
|
|
18
|
+
game: {
|
|
19
|
+
/** Loading progress, 0 to 100. Ikirmikir shows it as its loading bar. */
|
|
20
|
+
loadingProgress(percent: number): void;
|
|
21
|
+
/** The game is ready to play. Ikirmikir hides its loader. */
|
|
22
|
+
loadingDone(): void;
|
|
23
|
+
/** Someone is actually playing (after menus). */
|
|
24
|
+
gameplayStart(): void;
|
|
25
|
+
/** Play stopped: menu, game over, between rounds. */
|
|
26
|
+
gameplayStop(): void;
|
|
27
|
+
};
|
|
28
|
+
/** Listen for "pause", "resume" or "mute". Returns a function that stops listening. */
|
|
29
|
+
on<E extends keyof Events>(event: E, listener: Events[E]): () => void;
|
|
30
|
+
/** Ask Ikirmikir to share the game's link. Resolves { shared: false } when nothing was shared. */
|
|
31
|
+
share(): Promise<{
|
|
32
|
+
shared: boolean;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
export interface InitOptions {
|
|
36
|
+
/** The game's id on Ikirmikir, for example "runbro". */
|
|
37
|
+
gameId: string;
|
|
38
|
+
}
|
|
39
|
+
/** Start the SDK. Call once, early in loading. Never rejects; calling again returns the same instance. */
|
|
40
|
+
declare function init(options: InitOptions): Promise<Sdk>;
|
|
41
|
+
export declare const Ikirmikir: {
|
|
42
|
+
init: typeof init;
|
|
43
|
+
VERSION: string;
|
|
44
|
+
};
|
|
45
|
+
export default Ikirmikir;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/*! @ikirmikir/game-sdk 1.0.0 | MIT | https://ikirmikir.com/developers/ */
|
|
2
|
+
|
|
3
|
+
// src/protocol.ts
|
|
4
|
+
var PROTOCOL = 1;
|
|
5
|
+
var FEATURES = ["loading", "gameplay", "pause", "mute", "context", "share"];
|
|
6
|
+
var obj = (d) => typeof d === "object" && d !== null && !Array.isArray(d);
|
|
7
|
+
var str = (v, max) => typeof v === "string" && v.length > 0 && v.length <= max;
|
|
8
|
+
var int = (v, min, max) => Number.isInteger(v) && v >= min && v <= max;
|
|
9
|
+
var bool = (v) => typeof v === "boolean";
|
|
10
|
+
function readPageMessage(d) {
|
|
11
|
+
if (!obj(d) || d.ik !== PROTOCOL) return null;
|
|
12
|
+
switch (d.type) {
|
|
13
|
+
case "welcome": {
|
|
14
|
+
const c = d.context;
|
|
15
|
+
if (!Array.isArray(d.features) || !bool(d.muted) || !obj(c)) return null;
|
|
16
|
+
if (!str(c.language, 35) || c.device !== "mobile" && c.device !== "desktop" || !bool(c.touch)) return null;
|
|
17
|
+
const features = FEATURES.filter((f) => d.features.includes(f));
|
|
18
|
+
return { ik: 1, type: "welcome", features, context: { language: c.language, device: c.device, touch: c.touch }, muted: d.muted };
|
|
19
|
+
}
|
|
20
|
+
case "pause":
|
|
21
|
+
return { ik: 1, type: "pause" };
|
|
22
|
+
case "resume":
|
|
23
|
+
return { ik: 1, type: "resume" };
|
|
24
|
+
case "mute":
|
|
25
|
+
return bool(d.muted) ? { ik: 1, type: "mute", muted: d.muted } : null;
|
|
26
|
+
case "shared":
|
|
27
|
+
return int(d.id, 0, 1e9) && bool(d.shared) ? { ik: 1, type: "shared", id: d.id, shared: d.shared } : null;
|
|
28
|
+
default:
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function isIkirmikirOrigin(origin) {
|
|
33
|
+
return origin === "https://ikirmikir.com" || origin === "http://localhost:3000" || origin === "http://127.0.0.1:3000" || /^https:\/\/([0-9a-f]{8}\.)?ikirmikir\.pages\.dev$/.test(origin);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/index.ts
|
|
37
|
+
var VERSION = "1.0.0";
|
|
38
|
+
var HANDSHAKE_MS = 1500;
|
|
39
|
+
var instance;
|
|
40
|
+
function init(options) {
|
|
41
|
+
return instance ?? (instance = start(options));
|
|
42
|
+
}
|
|
43
|
+
async function start({ gameId }) {
|
|
44
|
+
const listeners = { pause: /* @__PURE__ */ new Set(), resume: /* @__PURE__ */ new Set(), mute: /* @__PURE__ */ new Set() };
|
|
45
|
+
const emit = (e, ...args) => listeners[e].forEach((fn) => {
|
|
46
|
+
try {
|
|
47
|
+
fn(...args);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(err);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
let env = "local", context = localContext(), muted = false, features = [];
|
|
53
|
+
let page;
|
|
54
|
+
const shares = /* @__PURE__ */ new Map();
|
|
55
|
+
let nextShare = 1, progress = -1, playing = false, done = false;
|
|
56
|
+
const send = (m) => {
|
|
57
|
+
if (page) try {
|
|
58
|
+
parent.postMessage({ ik: PROTOCOL, ...m }, page);
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
if (typeof window !== "undefined" && window.parent !== window) {
|
|
63
|
+
const welcome = new Promise((resolve) => {
|
|
64
|
+
addEventListener("message", (e) => {
|
|
65
|
+
if (e.source !== window.parent || !isIkirmikirOrigin(e.origin) || page && e.origin !== page) return;
|
|
66
|
+
const m = readPageMessage(e.data);
|
|
67
|
+
if (!m) return;
|
|
68
|
+
if (m.type === "welcome") {
|
|
69
|
+
if (page) return;
|
|
70
|
+
page = e.origin;
|
|
71
|
+
env = "ikirmikir";
|
|
72
|
+
context = m.context;
|
|
73
|
+
muted = m.muted;
|
|
74
|
+
features = m.features;
|
|
75
|
+
return resolve(true);
|
|
76
|
+
}
|
|
77
|
+
if (!page) return;
|
|
78
|
+
if (m.type === "pause" || m.type === "resume") emit(m.type);
|
|
79
|
+
else if (m.type === "mute") {
|
|
80
|
+
if (m.muted !== muted) {
|
|
81
|
+
muted = m.muted;
|
|
82
|
+
emit("mute", muted);
|
|
83
|
+
}
|
|
84
|
+
} else if (m.type === "shared") {
|
|
85
|
+
shares.get(m.id)?.(m.shared);
|
|
86
|
+
shares.delete(m.id);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
setTimeout(() => resolve(false), HANDSHAKE_MS);
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
parent.postMessage({ ik: PROTOCOL, type: "hello", gameId: String(gameId).slice(0, 64), sdk: VERSION }, "*");
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
await welcome;
|
|
96
|
+
}
|
|
97
|
+
if (env === "local") {
|
|
98
|
+
features = ["gameplay", "loading", "pause", "context", ...canShare() ? ["share"] : []];
|
|
99
|
+
if (typeof document !== "undefined")
|
|
100
|
+
document.addEventListener("visibilitychange", () => emit(document.hidden ? "pause" : "resume"));
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
get env() {
|
|
104
|
+
return env;
|
|
105
|
+
},
|
|
106
|
+
get context() {
|
|
107
|
+
return { ...context };
|
|
108
|
+
},
|
|
109
|
+
get muted() {
|
|
110
|
+
return muted;
|
|
111
|
+
},
|
|
112
|
+
has: (f) => features.includes(f),
|
|
113
|
+
game: {
|
|
114
|
+
loadingProgress(percent) {
|
|
115
|
+
const p = Math.max(0, Math.min(100, Math.round(Number(percent) || 0)));
|
|
116
|
+
if (done || p === progress) return;
|
|
117
|
+
progress = p;
|
|
118
|
+
send({ type: "loading", progress: p });
|
|
119
|
+
},
|
|
120
|
+
loadingDone() {
|
|
121
|
+
if (!done) {
|
|
122
|
+
done = true;
|
|
123
|
+
send({ type: "ready" });
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
gameplayStart() {
|
|
127
|
+
if (!playing) {
|
|
128
|
+
playing = true;
|
|
129
|
+
send({ type: "gameplay", playing });
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
gameplayStop() {
|
|
133
|
+
if (playing) {
|
|
134
|
+
playing = false;
|
|
135
|
+
send({ type: "gameplay", playing });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
on(event, listener) {
|
|
140
|
+
const set = listeners[event];
|
|
141
|
+
if (!set || typeof listener !== "function") return () => {
|
|
142
|
+
};
|
|
143
|
+
set.add(listener);
|
|
144
|
+
return () => {
|
|
145
|
+
set.delete(listener);
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
async share() {
|
|
149
|
+
if (env === "local") {
|
|
150
|
+
if (!canShare()) return { shared: false };
|
|
151
|
+
try {
|
|
152
|
+
await navigator.share({ url: location.href });
|
|
153
|
+
return { shared: true };
|
|
154
|
+
} catch {
|
|
155
|
+
return { shared: false };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const id = nextShare++;
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
shares.set(id, (shared) => resolve({ shared }));
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
if (shares.delete(id)) resolve({ shared: false });
|
|
163
|
+
}, 6e4);
|
|
164
|
+
send({ type: "share", id });
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function localContext() {
|
|
170
|
+
const nav = typeof navigator !== "undefined" ? navigator : void 0;
|
|
171
|
+
const touch = !!nav && nav.maxTouchPoints > 0;
|
|
172
|
+
const mobile = typeof matchMedia !== "undefined" && matchMedia("(pointer: coarse)").matches;
|
|
173
|
+
return { language: (nav?.language || "en").split("-")[0], device: mobile ? "mobile" : "desktop", touch };
|
|
174
|
+
}
|
|
175
|
+
var canShare = () => typeof navigator !== "undefined" && typeof navigator.share === "function";
|
|
176
|
+
var Ikirmikir = { init, VERSION };
|
|
177
|
+
var index_default = Ikirmikir;
|
|
178
|
+
export {
|
|
179
|
+
Ikirmikir,
|
|
180
|
+
VERSION,
|
|
181
|
+
index_default as default
|
|
182
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export declare const PROTOCOL = 1;
|
|
2
|
+
export type Feature = "loading" | "gameplay" | "pause" | "mute" | "context" | "share";
|
|
3
|
+
export interface Context {
|
|
4
|
+
language: string;
|
|
5
|
+
device: "mobile" | "desktop";
|
|
6
|
+
touch: boolean;
|
|
7
|
+
}
|
|
8
|
+
export type GameMessage = {
|
|
9
|
+
ik: 1;
|
|
10
|
+
type: "hello";
|
|
11
|
+
gameId: string;
|
|
12
|
+
sdk: string;
|
|
13
|
+
} | {
|
|
14
|
+
ik: 1;
|
|
15
|
+
type: "loading";
|
|
16
|
+
progress: number;
|
|
17
|
+
} | {
|
|
18
|
+
ik: 1;
|
|
19
|
+
type: "ready";
|
|
20
|
+
} | {
|
|
21
|
+
ik: 1;
|
|
22
|
+
type: "gameplay";
|
|
23
|
+
playing: boolean;
|
|
24
|
+
} | {
|
|
25
|
+
ik: 1;
|
|
26
|
+
type: "share";
|
|
27
|
+
id: number;
|
|
28
|
+
};
|
|
29
|
+
export type PageMessage = {
|
|
30
|
+
ik: 1;
|
|
31
|
+
type: "welcome";
|
|
32
|
+
features: Feature[];
|
|
33
|
+
context: Context;
|
|
34
|
+
muted: boolean;
|
|
35
|
+
} | {
|
|
36
|
+
ik: 1;
|
|
37
|
+
type: "pause";
|
|
38
|
+
} | {
|
|
39
|
+
ik: 1;
|
|
40
|
+
type: "resume";
|
|
41
|
+
} | {
|
|
42
|
+
ik: 1;
|
|
43
|
+
type: "mute";
|
|
44
|
+
muted: boolean;
|
|
45
|
+
} | {
|
|
46
|
+
ik: 1;
|
|
47
|
+
type: "shared";
|
|
48
|
+
id: number;
|
|
49
|
+
shared: boolean;
|
|
50
|
+
};
|
|
51
|
+
/** For the Ikirmikir page: a valid message from a game, or null. */
|
|
52
|
+
export declare function readGameMessage(d: unknown): GameMessage | null;
|
|
53
|
+
/** For the SDK: a valid message from the Ikirmikir page, or null. */
|
|
54
|
+
export declare function readPageMessage(d: unknown): PageMessage | null;
|
|
55
|
+
/** Origins allowed to act as the Ikirmikir page: the site, its Cloudflare Pages previews, and local development. */
|
|
56
|
+
export declare function isIkirmikirOrigin(origin: string): boolean;
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*! @ikirmikir/game-sdk 1.0.0 | MIT | https://ikirmikir.com/developers/ */
|
|
2
|
+
|
|
3
|
+
// src/protocol.ts
|
|
4
|
+
var PROTOCOL = 1;
|
|
5
|
+
var FEATURES = ["loading", "gameplay", "pause", "mute", "context", "share"];
|
|
6
|
+
var obj = (d) => typeof d === "object" && d !== null && !Array.isArray(d);
|
|
7
|
+
var str = (v, max) => typeof v === "string" && v.length > 0 && v.length <= max;
|
|
8
|
+
var int = (v, min, max) => Number.isInteger(v) && v >= min && v <= max;
|
|
9
|
+
var bool = (v) => typeof v === "boolean";
|
|
10
|
+
function readGameMessage(d) {
|
|
11
|
+
if (!obj(d) || d.ik !== PROTOCOL) return null;
|
|
12
|
+
switch (d.type) {
|
|
13
|
+
case "hello":
|
|
14
|
+
return str(d.gameId, 64) && str(d.sdk, 32) ? { ik: 1, type: "hello", gameId: d.gameId, sdk: d.sdk } : null;
|
|
15
|
+
case "loading":
|
|
16
|
+
return int(d.progress, 0, 100) ? { ik: 1, type: "loading", progress: d.progress } : null;
|
|
17
|
+
case "ready":
|
|
18
|
+
return { ik: 1, type: "ready" };
|
|
19
|
+
case "gameplay":
|
|
20
|
+
return bool(d.playing) ? { ik: 1, type: "gameplay", playing: d.playing } : null;
|
|
21
|
+
case "share":
|
|
22
|
+
return int(d.id, 0, 1e9) ? { ik: 1, type: "share", id: d.id } : null;
|
|
23
|
+
default:
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function readPageMessage(d) {
|
|
28
|
+
if (!obj(d) || d.ik !== PROTOCOL) return null;
|
|
29
|
+
switch (d.type) {
|
|
30
|
+
case "welcome": {
|
|
31
|
+
const c = d.context;
|
|
32
|
+
if (!Array.isArray(d.features) || !bool(d.muted) || !obj(c)) return null;
|
|
33
|
+
if (!str(c.language, 35) || c.device !== "mobile" && c.device !== "desktop" || !bool(c.touch)) return null;
|
|
34
|
+
const features = FEATURES.filter((f) => d.features.includes(f));
|
|
35
|
+
return { ik: 1, type: "welcome", features, context: { language: c.language, device: c.device, touch: c.touch }, muted: d.muted };
|
|
36
|
+
}
|
|
37
|
+
case "pause":
|
|
38
|
+
return { ik: 1, type: "pause" };
|
|
39
|
+
case "resume":
|
|
40
|
+
return { ik: 1, type: "resume" };
|
|
41
|
+
case "mute":
|
|
42
|
+
return bool(d.muted) ? { ik: 1, type: "mute", muted: d.muted } : null;
|
|
43
|
+
case "shared":
|
|
44
|
+
return int(d.id, 0, 1e9) && bool(d.shared) ? { ik: 1, type: "shared", id: d.id, shared: d.shared } : null;
|
|
45
|
+
default:
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function isIkirmikirOrigin(origin) {
|
|
50
|
+
return origin === "https://ikirmikir.com" || origin === "http://localhost:3000" || origin === "http://127.0.0.1:3000" || /^https:\/\/([0-9a-f]{8}\.)?ikirmikir\.pages\.dev$/.test(origin);
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
PROTOCOL,
|
|
54
|
+
isIkirmikirOrigin,
|
|
55
|
+
readGameMessage,
|
|
56
|
+
readPageMessage
|
|
57
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikirmikir/game-sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Optional SDK for games on Ikirmikir: loading, gameplay, pause, sound, player context and share. Works standalone too.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://ikirmikir.com/developers/",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"html5-games",
|
|
9
|
+
"browser-games",
|
|
10
|
+
"game-sdk",
|
|
11
|
+
"ikirmikir"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./protocol": {
|
|
21
|
+
"types": "./dist/protocol.d.ts",
|
|
22
|
+
"default": "./dist/protocol.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"esbuild": "0.28.2",
|
|
33
|
+
"typescript": "5.9.3"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "node scripts/build.mjs",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"types": "./dist/index.d.ts"
|
|
6
40
|
}
|