create-okona-game 0.3.0 → 0.5.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/README.md +2 -2
- package/index.js +1 -1
- package/package.json +2 -2
- package/template/README.md +3 -3
- package/template/game.js +23 -8
- package/template/index.html +87 -55
- package/template/okona.json +1 -1
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
npm create okona-game my-party-game
|
|
5
5
|
```
|
|
6
6
|
|
|
7
|
-
Scaffolds a phone-controlled
|
|
7
|
+
Scaffolds a phone-controlled HTML5 game for [Okona](https://okonaonline.com): a lobby built to the [Okona game standards](https://okonaonline.com/developers/docs.html#standards) (a controller joins by pressing A, any joined player starts, the game freezes while the Okona menu is open, no join code: Okona shows it), 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
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 submit it to the Okona catalog; once it's approved, players join
|
|
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 submit it to the Okona catalog; once it's approved, players join in the Okona app with a phone or a gamepad.
|
|
10
10
|
|
|
11
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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-okona-game",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Scaffold a phone-controlled
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Scaffold a phone-controlled 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
7
|
"bugs": {
|
package/template/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# {{NAME}}
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A multiplayer game for [Okona](https://okonaonline.com). Up to six people play with their phones or gamepads; each joins by pressing A.
|
|
4
4
|
|
|
5
|
-
- `index.html` — the lobby,
|
|
5
|
+
- `index.html` — the lobby, seats and frame loop, built to the [Okona game standards](https://okonaonline.com/developers/docs.html#standards): A joins, any joined player starts, the game freezes while the Okona menu is open, and it draws no join code: Okona shows it (rarely needs changes)
|
|
6
6
|
- `game.js` — **your game**: `start`, `update`, `draw`, and the join/leave/park/resume hooks
|
|
7
7
|
- `icon.png` — the icon shown in the Okona portal (replace it)
|
|
8
8
|
- `okona.json` — the catalog record: title, description, genre, keywords, the icon, screenshots. `okona push` syncs it
|
|
@@ -24,4 +24,4 @@ npx okona-cli push . --publish # submit for review: Okona approves it into
|
|
|
24
24
|
|
|
25
25
|
Screenshots: put up to six images in a `shots/` folder and add `"screenshots": "shots"` to `okona.json` — the next push uploads them in name order (1.png, 2.png, …). `npx okona-cli update --title "…"` changes a field without a build upload; `npx okona-cli game <id>` shows what the portal has.
|
|
26
26
|
|
|
27
|
-
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'|'
|
|
27
|
+
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'|'menu')`, `Okona.menuOpen`, `Okona.players.setIdentity()`, `Okona.rumble()`, `Okona.track()`.
|
package/template/game.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
// {{NAME}} — the game. index.html runs the lobby
|
|
1
|
+
// {{NAME}} — the game. index.html runs the lobby (A joins), the seats
|
|
2
2
|
// and the frame loop; it calls the hooks below. Replace the sample (a
|
|
3
3
|
// coin-grab race) with your game. Everything is plain canvas — bring any
|
|
4
4
|
// library you like, but bundle it (a venue's network may block CDNs).
|
|
5
5
|
//
|
|
6
6
|
// Coordinates: a 1920×1080 logical screen; index.html scales it to fit.
|
|
7
7
|
// Pads: pad.axes.lx/ly (−1..1, +Y down), pad.buttons.a/b/x/y/lb/rb/lt/rt/
|
|
8
|
-
// select/
|
|
8
|
+
// select/l3/r3/up/down/left/right, pad.justPressed('a') (start + home are
|
|
9
|
+
// Okona's: they never reach the game), player.joined (pressed A: a player),
|
|
9
10
|
// pad.triggers.lt/rt (0..1), pad.parked (controller away — seat held).
|
|
11
|
+
// Saves: Okona.save.get/set keep data for the player's account, on any screen
|
|
12
|
+
// (the sample keeps its best round); read it after Okona.ready.
|
|
10
13
|
var Game = {
|
|
11
14
|
title: '{{NAME}}',
|
|
12
15
|
subtitle: 'Grab the coins. Most coins after 45 seconds wins.',
|
|
@@ -17,14 +20,15 @@ var Game = {
|
|
|
17
20
|
ROUND_SECONDS: 45,
|
|
18
21
|
|
|
19
22
|
// ---- lifecycle ------------------------------------------------------------
|
|
20
|
-
onJoin: function (player, pad) {
|
|
23
|
+
onJoin: function (player, pad) { // the controller pressed A: now a player
|
|
21
24
|
player.x = 300 + player.index * 260; player.y = 540; player.vx = 0; player.vy = 0;
|
|
22
25
|
},
|
|
23
|
-
onLeave: function (player, pad) {},
|
|
24
|
-
onPark: function (player, pad) {}, // controller went away; seat held — the sample
|
|
26
|
+
onLeave: function (player, pad) {}, // gone for good (seat given away, or still away when the round ended)
|
|
27
|
+
onPark: function (player, pad) {}, // controller went away; seat + score held until the round ends — the sample freezes them
|
|
25
28
|
onResume: function (player, pad) {},
|
|
26
29
|
|
|
27
30
|
start: function (players, pads) {
|
|
31
|
+
this.best = Okona.save.get('best', 0); // the best round on this account, kept by Okona
|
|
28
32
|
this.coins = [];
|
|
29
33
|
for (var i = 0; i < 10; i++) this.spawnCoin();
|
|
30
34
|
players.forEach(function (p) { p.x = 300 + p.index * 260; p.y = 540; p.vx = 0; p.vy = 0; });
|
|
@@ -38,7 +42,7 @@ var Game = {
|
|
|
38
42
|
update: function (dt, players, pads, elapsed) {
|
|
39
43
|
for (var i = 0; i < 6; i++) {
|
|
40
44
|
var pad = pads[i], p = players[i];
|
|
41
|
-
if (!pad.connected || pad.parked) continue;
|
|
45
|
+
if (!p.joined || !pad.connected || pad.parked) continue; // only players who are here
|
|
42
46
|
var speed = pad.buttons.a ? 900 : 600;
|
|
43
47
|
p.vx += pad.axes.lx * speed * dt * 6; p.vy += pad.axes.ly * speed * dt * 6;
|
|
44
48
|
p.vx *= 0.88; p.vy *= 0.88;
|
|
@@ -54,7 +58,16 @@ var Game = {
|
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
|
-
|
|
61
|
+
var over = elapsed >= this.ROUND_SECONDS;
|
|
62
|
+
if (over) this.keepBest(players);
|
|
63
|
+
return over;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// A new best round goes in the save (it plays on fine if the save is unavailable).
|
|
67
|
+
keepBest: function (players) {
|
|
68
|
+
var top = 0;
|
|
69
|
+
players.forEach(function (p) { if (p.joined && p.score > top) top = p.score; });
|
|
70
|
+
if (top > this.best) { this.best = top; Okona.save.set('best', top); Okona.save.flush(); }
|
|
58
71
|
},
|
|
59
72
|
|
|
60
73
|
draw: function (ctx, players, pads, W, H) {
|
|
@@ -68,7 +81,7 @@ var Game = {
|
|
|
68
81
|
// players
|
|
69
82
|
for (var i = 0; i < 6; i++) {
|
|
70
83
|
var pad = pads[i], p = players[i];
|
|
71
|
-
if (!
|
|
84
|
+
if (!p.joined) continue;
|
|
72
85
|
ctx.globalAlpha = pad.parked ? 0.3 : 1;
|
|
73
86
|
ctx.fillStyle = p.color;
|
|
74
87
|
ctx.beginPath(); ctx.arc(p.x, p.y, 40, 0, Math.PI * 2); ctx.fill();
|
|
@@ -79,5 +92,7 @@ var Game = {
|
|
|
79
92
|
// HUD: big type — it is read from across the room
|
|
80
93
|
ctx.fillStyle = '#E8A525'; ctx.textAlign = 'left'; ctx.font = 'bold 40px system-ui, sans-serif';
|
|
81
94
|
ctx.fillText(this.title, 70, 100);
|
|
95
|
+
ctx.fillStyle = '#A0B4C8'; ctx.textAlign = 'right';
|
|
96
|
+
ctx.fillText('Best ' + this.best, W - 70, 100);
|
|
82
97
|
}
|
|
83
98
|
};
|
package/template/index.html
CHANGED
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
{{NAME}} — an Okona game.
|
|
4
4
|
|
|
5
5
|
This file is the LOBBY and the game loop; your game lives in game.js.
|
|
6
|
-
Six phones
|
|
7
|
-
|
|
8
|
-
(
|
|
6
|
+
Six phones or gamepads connect through Okona, and each becomes a player by
|
|
7
|
+
pressing A. It follows the Okona game standards
|
|
8
|
+
(https://okonaonline.com/developers/docs.html#standards): A joins, any joined
|
|
9
|
+
player starts, the game freezes while the Okona menu is open, and it draws
|
|
10
|
+
no join code: Okona shows it, in the Okona app and in the sandbox.
|
|
11
|
+
Open this file in a browser and the keyboard is P1 (WASD/arrows,
|
|
12
|
+
Space = A, Shift = B) so you can play what you build.
|
|
9
13
|
|
|
10
14
|
Submit: npx okona-cli create && npx okona-cli push . --publish (okona.json holds the title, icon, screenshots)
|
|
11
15
|
Docs: https://okonaonline.com/developers/docs.html#html5
|
|
@@ -22,7 +26,7 @@
|
|
|
22
26
|
</head>
|
|
23
27
|
<body>
|
|
24
28
|
<canvas id="screen"></canvas>
|
|
25
|
-
<!-- The Okona SDK: phones
|
|
29
|
+
<!-- The Okona SDK: phones and gamepads, identity, rumble, analytics. -->
|
|
26
30
|
<script src="https://okonaonline.com/sdk/okona.js"></script>
|
|
27
31
|
<script src="game.js"></script>
|
|
28
32
|
<script>
|
|
@@ -41,54 +45,46 @@
|
|
|
41
45
|
resize();
|
|
42
46
|
|
|
43
47
|
// ---- players ------------------------------------------------------------
|
|
44
|
-
// Six seats.
|
|
45
|
-
//
|
|
48
|
+
// Six seats. A controller that connects is only CONNECTED; it becomes a
|
|
49
|
+
// player (joined) when it presses A. Okona holds a seat while its
|
|
50
|
+
// controller is away (pad.parked), so a player who closes their phone gets
|
|
51
|
+
// the same seat, and keeps their score, when they come back.
|
|
46
52
|
var PALETTE = ['#E8A525', '#4FC3F7', '#81C784', '#F06292', '#BA68C8', '#FF8A65'];
|
|
47
53
|
var players = [];
|
|
48
|
-
for (var i = 0; i < 6; i++) players.push({ index: i, color: PALETTE[i], name: 'P' + (i + 1), score: 0 });
|
|
54
|
+
for (var i = 0; i < 6; i++) players.push({ index: i, color: PALETTE[i], name: 'P' + (i + 1), score: 0, joined: false });
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
function joinPlayer(p, pad) {
|
|
57
|
+
p.joined = true;
|
|
58
|
+
p.score = 0;
|
|
52
59
|
// What this player sees on their phone: name, emoji or image, accent.
|
|
53
60
|
Okona.players.setIdentity(pad, { name: p.name, icon: Game.icons[pad.index] || '🎮', accent: p.color });
|
|
54
61
|
Game.onJoin(p, pad);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
}
|
|
63
|
+
function dropPlayer(p, pad) {
|
|
64
|
+
if (!p.joined) return;
|
|
65
|
+
p.joined = false;
|
|
66
|
+
Okona.players.clearIdentity(pad);
|
|
67
|
+
Game.onLeave(p, pad);
|
|
68
|
+
}
|
|
59
69
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
Okona.on('
|
|
70
|
+
// 'leave' = the seat went to another controller: that player is gone.
|
|
71
|
+
Okona.on('leave', function (pad) { dropPlayer(players[pad.index], pad); });
|
|
72
|
+
Okona.on('park', function (pad) { if (players[pad.index].joined) Game.onPark(players[pad.index], pad); });
|
|
73
|
+
Okona.on('resume', function (pad) { if (players[pad.index].joined) Game.onResume(players[pad.index], pad); });
|
|
74
|
+
|
|
75
|
+
// No join code here: Okona shows it (the Okona app's Scan to play and
|
|
76
|
+
// Controllers; the sandbox's join code, which Start brings back).
|
|
64
77
|
|
|
65
78
|
// ---- lobby → playing → results -----------------------------------------
|
|
66
79
|
var state = 'lobby';
|
|
67
|
-
var
|
|
80
|
+
var elapsed = 0; // round time; stops while the Okona menu is open
|
|
68
81
|
|
|
69
|
-
function
|
|
82
|
+
function count(pads, joinedOnly) {
|
|
70
83
|
var n = 0;
|
|
71
|
-
for (var i = 0; i < pads.length; i++) if (pads[i].connected) n++;
|
|
84
|
+
for (var i = 0; i < pads.length; i++) if (pads[i].connected && (!joinedOnly || players[i].joined)) n++;
|
|
72
85
|
return n;
|
|
73
86
|
}
|
|
74
87
|
|
|
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
88
|
function drawLobby(pads) {
|
|
93
89
|
ctx.fillStyle = '#E8A525';
|
|
94
90
|
ctx.textAlign = 'left';
|
|
@@ -99,14 +95,22 @@
|
|
|
99
95
|
ctx.fillText(Game.subtitle, 80, 205);
|
|
100
96
|
for (var i = 0; i < 6; i++) {
|
|
101
97
|
var p = players[i], pad = pads[i];
|
|
102
|
-
|
|
98
|
+
var label = p.joined ? '● ' + p.name + (pad.parked ? ' away' : '')
|
|
99
|
+
: pad.connected ? '○ ' + p.name + ' press A to join'
|
|
100
|
+
: '○ ' + p.name + ' open';
|
|
101
|
+
ctx.fillStyle = p.joined ? p.color : (pad.connected ? '#A0B4C8' : '#2A4A6F');
|
|
103
102
|
ctx.font = 'bold 34px system-ui, sans-serif';
|
|
104
|
-
ctx.fillText(
|
|
103
|
+
ctx.fillText(label, 80, 300 + i * 56);
|
|
105
104
|
}
|
|
106
|
-
var n =
|
|
105
|
+
var n = count(pads, true);
|
|
107
106
|
ctx.fillStyle = '#FFFFFF';
|
|
108
107
|
ctx.font = '30px system-ui, sans-serif';
|
|
109
|
-
ctx.fillText(n === 0 ?
|
|
108
|
+
ctx.fillText(n === 0 ? (count(pads, false) ? 'Press A to join' : 'Waiting for players…')
|
|
109
|
+
: (n < Game.minPlayers ? 'Need ' + Game.minPlayers + ' to start' : 'Press A to start · B to leave'), 80, 680);
|
|
110
|
+
if (Okona.mode === 'standalone') {
|
|
111
|
+
ctx.fillStyle = '#A0B4C8';
|
|
112
|
+
ctx.fillText('Keyboard: WASD + Space', 80, 730);
|
|
113
|
+
}
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
function drawResults() {
|
|
@@ -125,7 +129,7 @@
|
|
|
125
129
|
}
|
|
126
130
|
ctx.fillStyle = '#A0B4C8';
|
|
127
131
|
ctx.font = '30px system-ui, sans-serif';
|
|
128
|
-
ctx.fillText('
|
|
132
|
+
ctx.fillText('Press A to play again', W / 2, 900);
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
var last = performance.now();
|
|
@@ -133,7 +137,25 @@
|
|
|
133
137
|
var dt = Math.min(0.05, (now - last) / 1000);
|
|
134
138
|
last = now;
|
|
135
139
|
var pads = Okona.pads(); // exactly once per frame — a second call (or Okona.pad(i)) resets justPressed()
|
|
136
|
-
|
|
140
|
+
var menuOpen = Okona.menuOpen === true;
|
|
141
|
+
|
|
142
|
+
// A connected controller becomes a player by pressing A (drop-in: in the
|
|
143
|
+
// lobby or mid-round). That press only joins; it never also starts the round.
|
|
144
|
+
var joinedNow = [];
|
|
145
|
+
for (var i = 0; i < 6; i++) {
|
|
146
|
+
var pad = pads[i], p = players[i];
|
|
147
|
+
if (!p.joined && pad.connected && !pad.parked && pad.justPressed('a') && !menuOpen) {
|
|
148
|
+
joinPlayer(p, pad);
|
|
149
|
+
joinedNow[i] = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Any joined player who is here (never "P1 only": seat 1 may be empty).
|
|
153
|
+
function pressed(button) {
|
|
154
|
+
for (var j = 0; j < 6; j++) {
|
|
155
|
+
if (!joinedNow[j] && players[j].joined && pads[j].connected && !pads[j].parked && pads[j].justPressed(button)) return true;
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
137
159
|
|
|
138
160
|
var sx = canvas.width / W, sy = canvas.height / H;
|
|
139
161
|
ctx.setTransform(sx, 0, 0, sy, 0, 0);
|
|
@@ -142,31 +164,41 @@
|
|
|
142
164
|
|
|
143
165
|
if (state === 'lobby') {
|
|
144
166
|
drawLobby(pads);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
167
|
+
if (!menuOpen) {
|
|
168
|
+
// Lobby: B leaves.
|
|
169
|
+
for (var k = 0; k < 6; k++) {
|
|
170
|
+
if (!joinedNow[k] && players[k].joined && pads[k].connected && pads[k].justPressed('b')) dropPlayer(players[k], pads[k]);
|
|
171
|
+
}
|
|
172
|
+
if (count(pads, true) >= Game.minPlayers && pressed('a')) {
|
|
173
|
+
players.forEach(function (pl) { pl.score = 0; });
|
|
174
|
+
Game.start(players, pads);
|
|
175
|
+
elapsed = 0;
|
|
176
|
+
state = 'playing';
|
|
177
|
+
Okona.track('round_start', { players: count(pads, true) });
|
|
178
|
+
}
|
|
152
179
|
}
|
|
153
180
|
} else if (state === 'playing') {
|
|
154
|
-
|
|
181
|
+
// While the Okona menu is open the round freezes: no update, no clock.
|
|
182
|
+
var over = false;
|
|
183
|
+
if (!menuOpen) {
|
|
184
|
+
elapsed += dt;
|
|
185
|
+
over = Game.update(dt, players, pads, elapsed);
|
|
186
|
+
}
|
|
155
187
|
Game.draw(ctx, players, pads, W, H);
|
|
156
|
-
drawJoinPanel(pads); // late joiners are welcome
|
|
157
188
|
if (over) {
|
|
158
189
|
state = 'results';
|
|
159
|
-
Okona.track('round_end', { seconds: Math.round(
|
|
190
|
+
Okona.track('round_end', { seconds: Math.round(elapsed) });
|
|
191
|
+
// The round is over: let go of anyone still away.
|
|
192
|
+
for (var m = 0; m < 6; m++) if (players[m].joined && (!pads[m].connected || pads[m].parked)) dropPlayer(players[m], pads[m]);
|
|
160
193
|
}
|
|
161
194
|
} else {
|
|
162
195
|
drawResults();
|
|
163
|
-
if (
|
|
196
|
+
if (!menuOpen && pressed('a')) state = 'lobby';
|
|
164
197
|
}
|
|
165
198
|
requestAnimationFrame(frame);
|
|
166
199
|
}
|
|
167
200
|
|
|
168
201
|
Okona.ready(function () {
|
|
169
|
-
Okona.drawQr(qr);
|
|
170
202
|
requestAnimationFrame(frame);
|
|
171
203
|
});
|
|
172
204
|
})();
|
package/template/okona.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "{{NAME}}",
|
|
3
|
-
"description": "A
|
|
3
|
+
"description": "A multiplayer party game for phones and gamepads. Drop in any time: press A to join.",
|
|
4
4
|
"genre": "multiplayer-party",
|
|
5
5
|
"keywords": ["party", "multiplayer"],
|
|
6
6
|
"controller": "basic",
|