create-okona-game 0.1.0 → 0.1.2
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 +11 -11
- package/index.js +71 -69
- package/package.json +3 -5
- package/template/README.md +24 -22
- package/template/index.html +175 -175
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# create-okona-game
|
|
2
|
-
|
|
3
|
-
```
|
|
4
|
-
npm create okona-game my-party-game
|
|
5
|
-
```
|
|
6
|
-
|
|
7
|
-
Scaffolds a phone-controlled multiplayer HTML5 game for [Okona](https://okonaonline.com): a lobby that shows the join code until the room is full, six seats with names and colours pushed to each phone, the frame loop, and a `game.js` with `start` / `update` / `draw` hooks holding a small sample (a coin-grab race) to replace with your game.
|
|
8
|
-
|
|
9
|
-
Open `index.html` in a browser and the keyboard is P1.
|
|
10
|
-
|
|
11
|
-
Zero dependencies. Docs: https://okonaonline.com/developers/docs.html#html5
|
|
1
|
+
# create-okona-game
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
npm create okona-game my-party-game
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Scaffolds a phone-controlled multiplayer HTML5 game for [Okona](https://okonaonline.com): a lobby that shows the join code until the room is full, six seats with names and colours pushed to each phone, the frame loop, and a `game.js` with `start` / `update` / `draw` hooks holding a small sample (a coin-grab race) to replace with your game.
|
|
8
|
+
|
|
9
|
+
Open `index.html` in a browser and the keyboard is P1 — no build step, no `package.json`. Ship it with the [`okona` CLI](https://www.npmjs.com/package/okona-cli) (`npx okona-cli`, Node 18.17+): push to your sandbox first to try it with real phones, then publish, and phones join by scanning the code on the TV.
|
|
10
|
+
|
|
11
|
+
Zero dependencies. Docs: https://okonaonline.com/developers/docs.html#html5 · SDK reference: https://okonaonline.com/developers/docs.html#js-sdk
|
package/index.js
CHANGED
|
@@ -1,69 +1,71 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// create-okona-game — scaffold a phone-controlled multiplayer game for Okona.
|
|
3
|
-
//
|
|
4
|
-
// npm create okona-game my-game (or: npx create-okona-game my-game)
|
|
5
|
-
//
|
|
6
|
-
// Copies the template (lobby + join code + seats in index.html, your game in
|
|
7
|
-
// game.js, an icon, a README) into ./my-game with the name filled in. Zero
|
|
8
|
-
// dependencies, Node ≥ 18. `--force` writes into a non-empty directory.
|
|
9
|
-
import { mkdir, readdir, readFile, writeFile, copyFile, stat } from "node:fs/promises";
|
|
10
|
-
import { join, resolve, dirname, basename } from "node:path";
|
|
11
|
-
import { fileURLToPath } from "node:url";
|
|
12
|
-
|
|
13
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
const TEMPLATE = join(here, "template");
|
|
15
|
-
|
|
16
|
-
export function titleFrom(dirName) {
|
|
17
|
-
return String(dirName || "My Okona Game")
|
|
18
|
-
.replace(/[-_]+/g, " ")
|
|
19
|
-
.replace(/\s+/g, " ")
|
|
20
|
-
.trim()
|
|
21
|
-
.replace(/\b\w/g, (c) => c.toUpperCase()) || "My Okona Game";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function scaffold(target, { name, force = false } = {}) {
|
|
25
|
-
const dir = resolve(target);
|
|
26
|
-
const title = name || titleFrom(basename(dir));
|
|
27
|
-
await mkdir(dir, { recursive: true });
|
|
28
|
-
const existing = await readdir(dir);
|
|
29
|
-
if (existing.length && !force) {
|
|
30
|
-
throw new Error(dir + " is not empty (use --force to write into it anyway).");
|
|
31
|
-
}
|
|
32
|
-
const files = await readdir(TEMPLATE);
|
|
33
|
-
const written = [];
|
|
34
|
-
for (const f of files) {
|
|
35
|
-
const src = join(TEMPLATE, f);
|
|
36
|
-
if ((await stat(src)).isDirectory()) continue;
|
|
37
|
-
const dest = join(dir, f === "gitignore" ? ".gitignore" : f);
|
|
38
|
-
if (/\.(html|js|md)$/.test(f) || f === "gitignore") {
|
|
39
|
-
const text = (await readFile(src, "utf8")).split("{{NAME}}").join(title);
|
|
40
|
-
await writeFile(dest, text);
|
|
41
|
-
} else {
|
|
42
|
-
await copyFile(src, dest);
|
|
43
|
-
}
|
|
44
|
-
written.push(basename(dest));
|
|
45
|
-
}
|
|
46
|
-
return { dir, title, files: written };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ---- CLI ---------------------------------------------------------------------
|
|
50
|
-
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
51
|
-
if (isMain) {
|
|
52
|
-
const args = process.argv.slice(2);
|
|
53
|
-
const force = args.includes("--force");
|
|
54
|
-
const nameIdx = args.indexOf("--name");
|
|
55
|
-
const name = nameIdx >= 0 ? args[nameIdx + 1] : undefined;
|
|
56
|
-
const target = args.find((a, i) => !a.startsWith("--") && (nameIdx < 0 || i !== nameIdx + 1)) || "my-okona-game";
|
|
57
|
-
scaffold(target, { name, force }).then((r) => {
|
|
58
|
-
console.log(`\n Created ${r.title} in ${r.dir}\n`);
|
|
59
|
-
console.log(" Next:");
|
|
60
|
-
console.log(" open index.html in a browser — the keyboard is P1 (WASD + Space)");
|
|
61
|
-
console.log(" edit game.js — start / update / draw are yours");
|
|
62
|
-
console.log(" npx okona-cli login && npx okona-cli create --title \"" + r.title + "\" --description \"…\" --runtime html5");
|
|
63
|
-
console.log(" npx okona-cli push . --game <id> --icon icon.png --
|
|
64
|
-
console.log("
|
|
65
|
-
|
|
66
|
-
console.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// create-okona-game — scaffold a phone-controlled multiplayer game for Okona.
|
|
3
|
+
//
|
|
4
|
+
// npm create okona-game my-game (or: npx create-okona-game my-game)
|
|
5
|
+
//
|
|
6
|
+
// Copies the template (lobby + join code + seats in index.html, your game in
|
|
7
|
+
// game.js, an icon, a README) into ./my-game with the name filled in. Zero
|
|
8
|
+
// dependencies, Node ≥ 18. `--force` writes into a non-empty directory.
|
|
9
|
+
import { mkdir, readdir, readFile, writeFile, copyFile, stat } from "node:fs/promises";
|
|
10
|
+
import { join, resolve, dirname, basename } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const TEMPLATE = join(here, "template");
|
|
15
|
+
|
|
16
|
+
export function titleFrom(dirName) {
|
|
17
|
+
return String(dirName || "My Okona Game")
|
|
18
|
+
.replace(/[-_]+/g, " ")
|
|
19
|
+
.replace(/\s+/g, " ")
|
|
20
|
+
.trim()
|
|
21
|
+
.replace(/\b\w/g, (c) => c.toUpperCase()) || "My Okona Game";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function scaffold(target, { name, force = false } = {}) {
|
|
25
|
+
const dir = resolve(target);
|
|
26
|
+
const title = name || titleFrom(basename(dir));
|
|
27
|
+
await mkdir(dir, { recursive: true });
|
|
28
|
+
const existing = await readdir(dir);
|
|
29
|
+
if (existing.length && !force) {
|
|
30
|
+
throw new Error(dir + " is not empty (use --force to write into it anyway).");
|
|
31
|
+
}
|
|
32
|
+
const files = await readdir(TEMPLATE);
|
|
33
|
+
const written = [];
|
|
34
|
+
for (const f of files) {
|
|
35
|
+
const src = join(TEMPLATE, f);
|
|
36
|
+
if ((await stat(src)).isDirectory()) continue;
|
|
37
|
+
const dest = join(dir, f === "gitignore" ? ".gitignore" : f);
|
|
38
|
+
if (/\.(html|js|md)$/.test(f) || f === "gitignore") {
|
|
39
|
+
const text = (await readFile(src, "utf8")).split("{{NAME}}").join(title);
|
|
40
|
+
await writeFile(dest, text);
|
|
41
|
+
} else {
|
|
42
|
+
await copyFile(src, dest);
|
|
43
|
+
}
|
|
44
|
+
written.push(basename(dest));
|
|
45
|
+
}
|
|
46
|
+
return { dir, title, files: written };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---- CLI ---------------------------------------------------------------------
|
|
50
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
51
|
+
if (isMain) {
|
|
52
|
+
const args = process.argv.slice(2);
|
|
53
|
+
const force = args.includes("--force");
|
|
54
|
+
const nameIdx = args.indexOf("--name");
|
|
55
|
+
const name = nameIdx >= 0 ? args[nameIdx + 1] : undefined;
|
|
56
|
+
const target = args.find((a, i) => !a.startsWith("--") && (nameIdx < 0 || i !== nameIdx + 1)) || "my-okona-game";
|
|
57
|
+
scaffold(target, { name, force }).then((r) => {
|
|
58
|
+
console.log(`\n Created ${r.title} in ${r.dir}\n`);
|
|
59
|
+
console.log(" Next:");
|
|
60
|
+
console.log(" open index.html in a browser — the keyboard is P1 (WASD + Space)");
|
|
61
|
+
console.log(" edit game.js — start / update / draw are yours");
|
|
62
|
+
console.log(" npx okona-cli login && npx okona-cli create --title \"" + r.title + "\" --description \"…\" --runtime html5");
|
|
63
|
+
console.log(" npx okona-cli push . --game <id> --icon icon.png --sandbox # try it with real phones first");
|
|
64
|
+
console.log(" npx okona-cli push . --game <id> --icon icon.png --publish # → https://play.okonaonline.com/games/?id=<id>");
|
|
65
|
+
console.log(" (npx okona-cli runs the okona command without installing it; Node 18.17+)\n");
|
|
66
|
+
console.log(" Docs: https://okonaonline.com/developers/docs.html#html5\n");
|
|
67
|
+
}).catch((e) => {
|
|
68
|
+
console.error("error: " + (e && e.message ? e.message : e));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-okona-game",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Scaffold a phone-controlled multiplayer HTML5 game for Okona: lobby, join code, six seats, and a game.js to make yours.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://okonaonline.com/developers/docs.html#html5",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"url": "https://github.com/earthcrosser/okona-platform",
|
|
10
|
-
"directory": "create-okona-game"
|
|
7
|
+
"bugs": {
|
|
8
|
+
"email": "support@okonaonline.com"
|
|
11
9
|
},
|
|
12
10
|
"keywords": ["okona", "create", "scaffold", "game", "html5", "phone-controller", "multiplayer", "party-game"],
|
|
13
11
|
"type": "module",
|
package/template/README.md
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
# {{NAME}}
|
|
2
|
-
|
|
3
|
-
A phone-controlled multiplayer game for [Okona](https://okonaonline.com). Up to six people scan the code on screen and their phones become the gamepads.
|
|
4
|
-
|
|
5
|
-
- `index.html` — the lobby, join code, seats and frame loop (rarely needs changes)
|
|
6
|
-
- `game.js` — **your game**: `start`, `update`, `draw`, and the join/leave/park/resume hooks
|
|
7
|
-
- `icon.png` — the icon shown in the Okona portal (replace it)
|
|
8
|
-
|
|
9
|
-
## Play it locally
|
|
10
|
-
|
|
11
|
-
Open `index.html` in a browser (or `npx serve .`). The keyboard is P1: WASD/arrows move, Space is A, Shift is B. Phones need the real thing, below.
|
|
12
|
-
|
|
13
|
-
## Publish
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
npx okona-cli
|
|
19
|
-
npx okona-cli
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
# {{NAME}}
|
|
2
|
+
|
|
3
|
+
A phone-controlled multiplayer game for [Okona](https://okonaonline.com). Up to six people scan the code on screen and their phones become the gamepads.
|
|
4
|
+
|
|
5
|
+
- `index.html` — the lobby, join code, seats and frame loop (rarely needs changes)
|
|
6
|
+
- `game.js` — **your game**: `start`, `update`, `draw`, and the join/leave/park/resume hooks
|
|
7
|
+
- `icon.png` — the icon shown in the Okona portal (replace it)
|
|
8
|
+
|
|
9
|
+
## Play it locally
|
|
10
|
+
|
|
11
|
+
Open `index.html` in a browser (or `npx serve .`). The keyboard is P1: WASD/arrows move, Space is A, Shift is B. Phones need the real thing, below.
|
|
12
|
+
|
|
13
|
+
## Publish
|
|
14
|
+
|
|
15
|
+
The `okona` CLI is the npm package `okona-cli` (Node 18.17+); `npx` runs it without installing.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
npx okona-cli login # once: approve a code in the portal
|
|
19
|
+
npx okona-cli create --title "{{NAME}}" --description "…" --runtime html5
|
|
20
|
+
npx okona-cli push . --game <id> --icon icon.png --sandbox # try it with real phones first
|
|
21
|
+
npx okona-cli push . --game <id> --icon icon.png --publish # → https://play.okonaonline.com/games/?id=<id>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Docs: https://okonaonline.com/developers/docs.html#html5 · SDK reference: https://okonaonline.com/developers/docs.html#js-sdk · The contract in one line: `Okona.pads()`, `Okona.on('join'|'leave'|'park'|'resume'|'pairing')`, `Okona.drawQr()`, `Okona.players.setIdentity()`, `Okona.rumble()`, `Okona.track()` — and, for a choices-on-the-phone game (the RPG controller, Okona Pro), `Okona.choices.show()` / `picked()` / `clear()`.
|
package/template/index.html
CHANGED
|
@@ -1,175 +1,175 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<!--
|
|
3
|
-
{{NAME}} — an Okona game.
|
|
4
|
-
|
|
5
|
-
This file is the LOBBY and the game loop; your game lives in game.js.
|
|
6
|
-
Six phones scan the code on screen and become gamepads. Okona hosts the
|
|
7
|
-
game at one link. Open this file in a browser and the keyboard is P1
|
|
8
|
-
(WASD/arrows, Space = A, Shift = B) so you can play what you build.
|
|
9
|
-
|
|
10
|
-
Publish: npx okona-cli push . --game <id> --icon icon.png --publish
|
|
11
|
-
Docs: https://okonaonline.com/developers/docs.html#html5
|
|
12
|
-
-->
|
|
13
|
-
<html lang="en">
|
|
14
|
-
<head>
|
|
15
|
-
<meta charset="utf-8">
|
|
16
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
17
|
-
<title>{{NAME}}</title>
|
|
18
|
-
<style>
|
|
19
|
-
html, body { margin: 0; height: 100%; background: #0D1B2A; overflow: hidden; }
|
|
20
|
-
canvas { display: block; width: 100vw; height: 100vh; }
|
|
21
|
-
</style>
|
|
22
|
-
</head>
|
|
23
|
-
<body>
|
|
24
|
-
<canvas id="screen"></canvas>
|
|
25
|
-
<!-- The Okona SDK: phones, pairing code, identity, rumble, analytics. -->
|
|
26
|
-
<script src="https://okonaonline.com/sdk/okona.js"></script>
|
|
27
|
-
<script src="game.js"></script>
|
|
28
|
-
<script>
|
|
29
|
-
(function () {
|
|
30
|
-
'use strict';
|
|
31
|
-
// ---- the screen: a fixed 1920×1080 logical canvas scaled to fit ---------
|
|
32
|
-
var canvas = document.getElementById('screen');
|
|
33
|
-
var ctx = canvas.getContext('2d');
|
|
34
|
-
var W = 1920, H = 1080;
|
|
35
|
-
function resize() {
|
|
36
|
-
var dpr = window.devicePixelRatio || 1;
|
|
37
|
-
canvas.width = Math.floor(window.innerWidth * dpr);
|
|
38
|
-
canvas.height = Math.floor(window.innerHeight * dpr);
|
|
39
|
-
}
|
|
40
|
-
window.addEventListener('resize', resize);
|
|
41
|
-
resize();
|
|
42
|
-
|
|
43
|
-
// ---- players ------------------------------------------------------------
|
|
44
|
-
// Six seats. Okona holds a seat while its controller is away (pad.parked),
|
|
45
|
-
// so a player who closes their phone gets the same seat back.
|
|
46
|
-
var PALETTE = ['#E8A525', '#4FC3F7', '#81C784', '#F06292', '#BA68C8', '#FF8A65'];
|
|
47
|
-
var players = [];
|
|
48
|
-
for (var i = 0; i < 6; i++) players.push({ index: i, color: PALETTE[i], name: 'P' + (i + 1), score: 0 });
|
|
49
|
-
|
|
50
|
-
Okona.on('join', function (pad) {
|
|
51
|
-
var p = players[pad.index];
|
|
52
|
-
// What this player sees on their phone: name, emoji or image, accent.
|
|
53
|
-
Okona.players.setIdentity(pad, { name: p.name, icon: Game.icons[pad.index] || '🎮', accent: p.color });
|
|
54
|
-
Game.onJoin(p, pad);
|
|
55
|
-
});
|
|
56
|
-
Okona.on('leave', function (pad) { Game.onLeave(players[pad.index], pad); });
|
|
57
|
-
Okona.on('park', function (pad) { Game.onPark(players[pad.index], pad); });
|
|
58
|
-
Okona.on('resume', function (pad) { Game.onResume(players[pad.index], pad); });
|
|
59
|
-
|
|
60
|
-
// ---- the pairing code ---------------------------------------------------
|
|
61
|
-
var qr = document.createElement('canvas');
|
|
62
|
-
qr.width = qr.height = 512;
|
|
63
|
-
Okona.on('pairing', function () { Okona.drawQr(qr); });
|
|
64
|
-
|
|
65
|
-
// ---- lobby → playing → results -----------------------------------------
|
|
66
|
-
var state = 'lobby';
|
|
67
|
-
var startedAt = 0;
|
|
68
|
-
|
|
69
|
-
function connected(pads) {
|
|
70
|
-
var n = 0;
|
|
71
|
-
for (var i = 0; i < pads.length; i++) if (pads[i].connected) n++;
|
|
72
|
-
return n;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function drawJoinPanel(pads) {
|
|
76
|
-
var n = connected(pads);
|
|
77
|
-
if (n >= 6) return;
|
|
78
|
-
var size = state === 'lobby' ? 420 : 200;
|
|
79
|
-
var x = W - size - 70, y = 70;
|
|
80
|
-
ctx.fillStyle = '#FFFFFF';
|
|
81
|
-
ctx.fillRect(x - 16, y - 16, size + 32, size + 32);
|
|
82
|
-
ctx.drawImage(qr, x, y, size, size);
|
|
83
|
-
ctx.fillStyle = '#FFFFFF';
|
|
84
|
-
ctx.textAlign = 'center';
|
|
85
|
-
ctx.font = 'bold ' + (state === 'lobby' ? 40 : 24) + 'px system-ui, sans-serif';
|
|
86
|
-
ctx.fillText('Scan to join', x + size / 2, y + size + 66);
|
|
87
|
-
ctx.fillStyle = '#A0B4C8';
|
|
88
|
-
ctx.font = (state === 'lobby' ? 22 : 16) + 'px system-ui, sans-serif';
|
|
89
|
-
ctx.fillText(Okona.pairing.caption || (Okona.mode === 'standalone' ? 'Keyboard: WASD + Space' : ''), x + size / 2, y + size + 98);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function drawLobby(pads) {
|
|
93
|
-
ctx.fillStyle = '#E8A525';
|
|
94
|
-
ctx.textAlign = 'left';
|
|
95
|
-
ctx.font = 'bold 72px system-ui, sans-serif';
|
|
96
|
-
ctx.fillText(Game.title, 80, 150);
|
|
97
|
-
ctx.fillStyle = '#A0B4C8';
|
|
98
|
-
ctx.font = '30px system-ui, sans-serif';
|
|
99
|
-
ctx.fillText(Game.subtitle, 80, 205);
|
|
100
|
-
for (var i = 0; i < 6; i++) {
|
|
101
|
-
var p = players[i], pad = pads[i];
|
|
102
|
-
ctx.fillStyle = pad.connected ? p.color : '#2A4A6F';
|
|
103
|
-
ctx.font = 'bold 34px system-ui, sans-serif';
|
|
104
|
-
ctx.fillText((pad.connected ? '● ' : '○ ') + p.name + (pad.connected ? '' : ' open'), 80, 300 + i * 56);
|
|
105
|
-
}
|
|
106
|
-
var n = connected(pads);
|
|
107
|
-
ctx.fillStyle = '#FFFFFF';
|
|
108
|
-
ctx.font = '30px system-ui, sans-serif';
|
|
109
|
-
ctx.fillText(n === 0 ? 'Waiting for players…' : (n < Game.minPlayers ? 'Need ' + Game.minPlayers + ' to start' : 'P1: press A to start'), 80, 680);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function drawResults() {
|
|
113
|
-
var ranked = players.slice().sort(function (a, b) { return b.score - a.score; });
|
|
114
|
-
ctx.fillStyle = '#E8A525';
|
|
115
|
-
ctx.textAlign = 'center';
|
|
116
|
-
ctx.font = 'bold 84px system-ui, sans-serif';
|
|
117
|
-
ctx.fillText('Results', W / 2, 200);
|
|
118
|
-
ctx.font = 'bold 44px system-ui, sans-serif';
|
|
119
|
-
var shown = 0;
|
|
120
|
-
for (var i = 0; i < ranked.length && shown < 6; i++) {
|
|
121
|
-
if (!ranked[i].joined) continue;
|
|
122
|
-
ctx.fillStyle = ranked[i].color;
|
|
123
|
-
ctx.fillText(ranked[i].name + ' ' + ranked[i].score, W / 2, 320 + shown * 70);
|
|
124
|
-
shown++;
|
|
125
|
-
}
|
|
126
|
-
ctx.fillStyle = '#A0B4C8';
|
|
127
|
-
ctx.font = '30px system-ui, sans-serif';
|
|
128
|
-
ctx.fillText('P1: press A to play again', W / 2, 900);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
var last = performance.now();
|
|
132
|
-
function frame(now) {
|
|
133
|
-
var dt = Math.min(0.05, (now - last) / 1000);
|
|
134
|
-
last = now;
|
|
135
|
-
var pads = Okona.pads(); // once per frame —
|
|
136
|
-
for (var i = 0; i < 6; i++) if (pads[i].connected) players[i].joined = true;
|
|
137
|
-
|
|
138
|
-
var sx = canvas.width / W, sy = canvas.height / H;
|
|
139
|
-
ctx.setTransform(sx, 0, 0, sy, 0, 0);
|
|
140
|
-
ctx.fillStyle = '#0D1B2A';
|
|
141
|
-
ctx.fillRect(0, 0, W, H);
|
|
142
|
-
|
|
143
|
-
if (state === 'lobby') {
|
|
144
|
-
drawLobby(pads);
|
|
145
|
-
drawJoinPanel(pads);
|
|
146
|
-
if (connected(pads) >= Game.minPlayers && pads[0].justPressed('a')) {
|
|
147
|
-
players.forEach(function (p) { p.score = 0; });
|
|
148
|
-
Game.start(players, pads);
|
|
149
|
-
startedAt = now;
|
|
150
|
-
state = 'playing';
|
|
151
|
-
Okona.track('round_start', { players: connected(pads) });
|
|
152
|
-
}
|
|
153
|
-
} else if (state === 'playing') {
|
|
154
|
-
var over = Game.update(dt, players, pads, (now - startedAt) / 1000);
|
|
155
|
-
Game.draw(ctx, players, pads, W, H);
|
|
156
|
-
drawJoinPanel(pads); // late joiners are welcome
|
|
157
|
-
if (over) {
|
|
158
|
-
state = 'results';
|
|
159
|
-
Okona.track('round_end', { seconds: Math.round((now - startedAt) / 1000) });
|
|
160
|
-
}
|
|
161
|
-
} else {
|
|
162
|
-
drawResults();
|
|
163
|
-
if (pads[0].justPressed('a')) state = 'lobby';
|
|
164
|
-
}
|
|
165
|
-
requestAnimationFrame(frame);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
Okona.ready(function () {
|
|
169
|
-
Okona.drawQr(qr);
|
|
170
|
-
requestAnimationFrame(frame);
|
|
171
|
-
});
|
|
172
|
-
})();
|
|
173
|
-
</script>
|
|
174
|
-
</body>
|
|
175
|
-
</html>
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<!--
|
|
3
|
+
{{NAME}} — an Okona game.
|
|
4
|
+
|
|
5
|
+
This file is the LOBBY and the game loop; your game lives in game.js.
|
|
6
|
+
Six phones scan the code on screen and become gamepads. Okona hosts the
|
|
7
|
+
game at one link. Open this file in a browser and the keyboard is P1
|
|
8
|
+
(WASD/arrows, Space = A, Shift = B) so you can play what you build.
|
|
9
|
+
|
|
10
|
+
Publish: npx okona-cli push . --game <id> --icon icon.png --publish
|
|
11
|
+
Docs: https://okonaonline.com/developers/docs.html#html5
|
|
12
|
+
-->
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8">
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
17
|
+
<title>{{NAME}}</title>
|
|
18
|
+
<style>
|
|
19
|
+
html, body { margin: 0; height: 100%; background: #0D1B2A; overflow: hidden; }
|
|
20
|
+
canvas { display: block; width: 100vw; height: 100vh; }
|
|
21
|
+
</style>
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<canvas id="screen"></canvas>
|
|
25
|
+
<!-- The Okona SDK: phones, pairing code, identity, rumble, analytics. -->
|
|
26
|
+
<script src="https://okonaonline.com/sdk/okona.js"></script>
|
|
27
|
+
<script src="game.js"></script>
|
|
28
|
+
<script>
|
|
29
|
+
(function () {
|
|
30
|
+
'use strict';
|
|
31
|
+
// ---- the screen: a fixed 1920×1080 logical canvas scaled to fit ---------
|
|
32
|
+
var canvas = document.getElementById('screen');
|
|
33
|
+
var ctx = canvas.getContext('2d');
|
|
34
|
+
var W = 1920, H = 1080;
|
|
35
|
+
function resize() {
|
|
36
|
+
var dpr = window.devicePixelRatio || 1;
|
|
37
|
+
canvas.width = Math.floor(window.innerWidth * dpr);
|
|
38
|
+
canvas.height = Math.floor(window.innerHeight * dpr);
|
|
39
|
+
}
|
|
40
|
+
window.addEventListener('resize', resize);
|
|
41
|
+
resize();
|
|
42
|
+
|
|
43
|
+
// ---- players ------------------------------------------------------------
|
|
44
|
+
// Six seats. Okona holds a seat while its controller is away (pad.parked),
|
|
45
|
+
// so a player who closes their phone gets the same seat back.
|
|
46
|
+
var PALETTE = ['#E8A525', '#4FC3F7', '#81C784', '#F06292', '#BA68C8', '#FF8A65'];
|
|
47
|
+
var players = [];
|
|
48
|
+
for (var i = 0; i < 6; i++) players.push({ index: i, color: PALETTE[i], name: 'P' + (i + 1), score: 0 });
|
|
49
|
+
|
|
50
|
+
Okona.on('join', function (pad) {
|
|
51
|
+
var p = players[pad.index];
|
|
52
|
+
// What this player sees on their phone: name, emoji or image, accent.
|
|
53
|
+
Okona.players.setIdentity(pad, { name: p.name, icon: Game.icons[pad.index] || '🎮', accent: p.color });
|
|
54
|
+
Game.onJoin(p, pad);
|
|
55
|
+
});
|
|
56
|
+
Okona.on('leave', function (pad) { Game.onLeave(players[pad.index], pad); });
|
|
57
|
+
Okona.on('park', function (pad) { Game.onPark(players[pad.index], pad); });
|
|
58
|
+
Okona.on('resume', function (pad) { Game.onResume(players[pad.index], pad); });
|
|
59
|
+
|
|
60
|
+
// ---- the pairing code ---------------------------------------------------
|
|
61
|
+
var qr = document.createElement('canvas');
|
|
62
|
+
qr.width = qr.height = 512;
|
|
63
|
+
Okona.on('pairing', function () { Okona.drawQr(qr); });
|
|
64
|
+
|
|
65
|
+
// ---- lobby → playing → results -----------------------------------------
|
|
66
|
+
var state = 'lobby';
|
|
67
|
+
var startedAt = 0;
|
|
68
|
+
|
|
69
|
+
function connected(pads) {
|
|
70
|
+
var n = 0;
|
|
71
|
+
for (var i = 0; i < pads.length; i++) if (pads[i].connected) n++;
|
|
72
|
+
return n;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function drawJoinPanel(pads) {
|
|
76
|
+
var n = connected(pads);
|
|
77
|
+
if (n >= 6) return;
|
|
78
|
+
var size = state === 'lobby' ? 420 : 200;
|
|
79
|
+
var x = W - size - 70, y = 70;
|
|
80
|
+
ctx.fillStyle = '#FFFFFF';
|
|
81
|
+
ctx.fillRect(x - 16, y - 16, size + 32, size + 32);
|
|
82
|
+
ctx.drawImage(qr, x, y, size, size);
|
|
83
|
+
ctx.fillStyle = '#FFFFFF';
|
|
84
|
+
ctx.textAlign = 'center';
|
|
85
|
+
ctx.font = 'bold ' + (state === 'lobby' ? 40 : 24) + 'px system-ui, sans-serif';
|
|
86
|
+
ctx.fillText('Scan to join', x + size / 2, y + size + 66);
|
|
87
|
+
ctx.fillStyle = '#A0B4C8';
|
|
88
|
+
ctx.font = (state === 'lobby' ? 22 : 16) + 'px system-ui, sans-serif';
|
|
89
|
+
ctx.fillText(Okona.pairing.caption || (Okona.mode === 'standalone' ? 'Keyboard: WASD + Space' : ''), x + size / 2, y + size + 98);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function drawLobby(pads) {
|
|
93
|
+
ctx.fillStyle = '#E8A525';
|
|
94
|
+
ctx.textAlign = 'left';
|
|
95
|
+
ctx.font = 'bold 72px system-ui, sans-serif';
|
|
96
|
+
ctx.fillText(Game.title, 80, 150);
|
|
97
|
+
ctx.fillStyle = '#A0B4C8';
|
|
98
|
+
ctx.font = '30px system-ui, sans-serif';
|
|
99
|
+
ctx.fillText(Game.subtitle, 80, 205);
|
|
100
|
+
for (var i = 0; i < 6; i++) {
|
|
101
|
+
var p = players[i], pad = pads[i];
|
|
102
|
+
ctx.fillStyle = pad.connected ? p.color : '#2A4A6F';
|
|
103
|
+
ctx.font = 'bold 34px system-ui, sans-serif';
|
|
104
|
+
ctx.fillText((pad.connected ? '● ' : '○ ') + p.name + (pad.connected ? '' : ' open'), 80, 300 + i * 56);
|
|
105
|
+
}
|
|
106
|
+
var n = connected(pads);
|
|
107
|
+
ctx.fillStyle = '#FFFFFF';
|
|
108
|
+
ctx.font = '30px system-ui, sans-serif';
|
|
109
|
+
ctx.fillText(n === 0 ? 'Waiting for players…' : (n < Game.minPlayers ? 'Need ' + Game.minPlayers + ' to start' : 'P1: press A to start'), 80, 680);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function drawResults() {
|
|
113
|
+
var ranked = players.slice().sort(function (a, b) { return b.score - a.score; });
|
|
114
|
+
ctx.fillStyle = '#E8A525';
|
|
115
|
+
ctx.textAlign = 'center';
|
|
116
|
+
ctx.font = 'bold 84px system-ui, sans-serif';
|
|
117
|
+
ctx.fillText('Results', W / 2, 200);
|
|
118
|
+
ctx.font = 'bold 44px system-ui, sans-serif';
|
|
119
|
+
var shown = 0;
|
|
120
|
+
for (var i = 0; i < ranked.length && shown < 6; i++) {
|
|
121
|
+
if (!ranked[i].joined) continue;
|
|
122
|
+
ctx.fillStyle = ranked[i].color;
|
|
123
|
+
ctx.fillText(ranked[i].name + ' ' + ranked[i].score, W / 2, 320 + shown * 70);
|
|
124
|
+
shown++;
|
|
125
|
+
}
|
|
126
|
+
ctx.fillStyle = '#A0B4C8';
|
|
127
|
+
ctx.font = '30px system-ui, sans-serif';
|
|
128
|
+
ctx.fillText('P1: press A to play again', W / 2, 900);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
var last = performance.now();
|
|
132
|
+
function frame(now) {
|
|
133
|
+
var dt = Math.min(0.05, (now - last) / 1000);
|
|
134
|
+
last = now;
|
|
135
|
+
var pads = Okona.pads(); // exactly once per frame — a second call (or Okona.pad(i)) resets justPressed()
|
|
136
|
+
for (var i = 0; i < 6; i++) if (pads[i].connected) players[i].joined = true;
|
|
137
|
+
|
|
138
|
+
var sx = canvas.width / W, sy = canvas.height / H;
|
|
139
|
+
ctx.setTransform(sx, 0, 0, sy, 0, 0);
|
|
140
|
+
ctx.fillStyle = '#0D1B2A';
|
|
141
|
+
ctx.fillRect(0, 0, W, H);
|
|
142
|
+
|
|
143
|
+
if (state === 'lobby') {
|
|
144
|
+
drawLobby(pads);
|
|
145
|
+
drawJoinPanel(pads);
|
|
146
|
+
if (connected(pads) >= Game.minPlayers && pads[0].justPressed('a')) {
|
|
147
|
+
players.forEach(function (p) { p.score = 0; });
|
|
148
|
+
Game.start(players, pads);
|
|
149
|
+
startedAt = now;
|
|
150
|
+
state = 'playing';
|
|
151
|
+
Okona.track('round_start', { players: connected(pads) });
|
|
152
|
+
}
|
|
153
|
+
} else if (state === 'playing') {
|
|
154
|
+
var over = Game.update(dt, players, pads, (now - startedAt) / 1000);
|
|
155
|
+
Game.draw(ctx, players, pads, W, H);
|
|
156
|
+
drawJoinPanel(pads); // late joiners are welcome
|
|
157
|
+
if (over) {
|
|
158
|
+
state = 'results';
|
|
159
|
+
Okona.track('round_end', { seconds: Math.round((now - startedAt) / 1000) });
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
drawResults();
|
|
163
|
+
if (pads[0].justPressed('a')) state = 'lobby';
|
|
164
|
+
}
|
|
165
|
+
requestAnimationFrame(frame);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
Okona.ready(function () {
|
|
169
|
+
Okona.drawQr(qr);
|
|
170
|
+
requestAnimationFrame(frame);
|
|
171
|
+
});
|
|
172
|
+
})();
|
|
173
|
+
</script>
|
|
174
|
+
</body>
|
|
175
|
+
</html>
|