create-okona-game 0.3.0 → 0.4.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 CHANGED
@@ -4,8 +4,8 @@
4
4
  npm create okona-game my-party-game
5
5
  ```
6
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.
7
+ Scaffolds a phone-controlled multiplayer 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 by scanning the code on the TV.
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-okona-game",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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",
@@ -1,8 +1,8 @@
1
1
  # {{NAME}}
2
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.
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, join code, seats and frame loop (rarely needs changes)
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'|'pairing')`, `Okona.drawQr()`, `Okona.players.setIdentity()`, `Okona.rumble()`, `Okona.track()`.
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,11 +1,12 @@
1
- // {{NAME}} — the game. index.html runs the lobby, the join code, the seats
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/start/l3/r3/up/down/left/right/home, pad.justPressed('a'),
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).
10
11
  var Game = {
11
12
  title: '{{NAME}}',
@@ -17,11 +18,11 @@ var Game = {
17
18
  ROUND_SECONDS: 45,
18
19
 
19
20
  // ---- lifecycle ------------------------------------------------------------
20
- onJoin: function (player, pad) {
21
+ onJoin: function (player, pad) { // the controller pressed A: now a player
21
22
  player.x = 300 + player.index * 260; player.y = 540; player.vx = 0; player.vy = 0;
22
23
  },
23
- onLeave: function (player, pad) {},
24
- onPark: function (player, pad) {}, // controller went away; seat held — the sample just stops moving them
24
+ onLeave: function (player, pad) {}, // gone for good (seat given away, or still away when the round ended)
25
+ onPark: function (player, pad) {}, // controller went away; seat + score held until the round ends — the sample freezes them
25
26
  onResume: function (player, pad) {},
26
27
 
27
28
  start: function (players, pads) {
@@ -38,7 +39,7 @@ var Game = {
38
39
  update: function (dt, players, pads, elapsed) {
39
40
  for (var i = 0; i < 6; i++) {
40
41
  var pad = pads[i], p = players[i];
41
- if (!pad.connected || pad.parked) continue;
42
+ if (!p.joined || !pad.connected || pad.parked) continue; // only players who are here
42
43
  var speed = pad.buttons.a ? 900 : 600;
43
44
  p.vx += pad.axes.lx * speed * dt * 6; p.vy += pad.axes.ly * speed * dt * 6;
44
45
  p.vx *= 0.88; p.vy *= 0.88;
@@ -68,7 +69,7 @@ var Game = {
68
69
  // players
69
70
  for (var i = 0; i < 6; i++) {
70
71
  var pad = pads[i], p = players[i];
71
- if (!pad.connected) continue;
72
+ if (!p.joined) continue;
72
73
  ctx.globalAlpha = pad.parked ? 0.3 : 1;
73
74
  ctx.fillStyle = p.color;
74
75
  ctx.beginPath(); ctx.arc(p.x, p.y, 40, 0, Math.PI * 2); ctx.fill();
@@ -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 scan the code on screen and become gamepads. Submit it to the
7
- Okona catalog when it's ready. 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.
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, pairing code, identity, rumble, analytics. -->
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. Okona holds a seat while its controller is away (pad.parked),
45
- // so a player who closes their phone gets the same seat back.
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
- Okona.on('join', function (pad) {
51
- var p = players[pad.index];
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
- 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); });
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
- // ---- the pairing code ---------------------------------------------------
61
- var qr = document.createElement('canvas');
62
- qr.width = qr.height = 512;
63
- Okona.on('pairing', function () { Okona.drawQr(qr); });
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 startedAt = 0;
80
+ var elapsed = 0; // round time; stops while the Okona menu is open
68
81
 
69
- function connected(pads) {
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
- ctx.fillStyle = pad.connected ? p.color : '#2A4A6F';
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((pad.connected ? '● ' : '○ ') + p.name + (pad.connected ? '' : ' open'), 80, 300 + i * 56);
103
+ ctx.fillText(label, 80, 300 + i * 56);
105
104
  }
106
- var n = connected(pads);
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 ? 'Waiting for players…' : (n < Game.minPlayers ? 'Need ' + Game.minPlayers + ' to start' : 'P1: press A to start'), 80, 680);
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('P1: press A to play again', W / 2, 900);
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
- for (var i = 0; i < 6; i++) if (pads[i].connected) players[i].joined = true;
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
- 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) });
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
- var over = Game.update(dt, players, pads, (now - startedAt) / 1000);
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((now - startedAt) / 1000) });
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 (pads[0].justPressed('a')) state = 'lobby';
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
  })();
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "title": "{{NAME}}",
3
- "description": "A phone-controlled multiplayer game. Scan the code on screen to play.",
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",