create-okona-game 0.4.0 → 0.6.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.
@@ -1,14 +1,16 @@
1
1
  <!DOCTYPE html>
2
2
  <!--
3
- {{NAME}} — an Okona game.
3
+ {{NAME}}: an Okona game, built on Phaser 3.90 (bundled: phaser.min.js).
4
4
 
5
- This file is the LOBBY and the game loop; your game lives in game.js.
5
+ This file is the LOBBY, the seats and the loop; your game lives in game.js.
6
6
  Six phones or gamepads connect through Okona, and each becomes a player by
7
7
  pressing A. It follows the Okona game standards
8
8
  (https://okonaonline.com/developers/docs.html#standards): A joins, any joined
9
9
  player starts, the game freezes while the Okona menu is open, and it draws
10
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,
11
+ The Phaser config is the one Okona recommends for a TV box
12
+ (https://okonaonline.com/developers/docs.html#phaser): keep it.
13
+ Open this file in a browser and the keyboard is P1 (arrows/WASD,
12
14
  Space = A, Shift = B) so you can play what you build.
13
15
 
14
16
  Submit: npx okona-cli create && npx okona-cli push . --publish (okona.json holds the title, icon, screenshots)
@@ -21,44 +23,34 @@
21
23
  <title>{{NAME}}</title>
22
24
  <style>
23
25
  html, body { margin: 0; height: 100%; background: #0D1B2A; overflow: hidden; }
24
- canvas { display: block; width: 100vw; height: 100vh; }
25
26
  </style>
26
27
  </head>
27
28
  <body>
28
- <canvas id="screen"></canvas>
29
- <!-- The Okona SDK: phones and gamepads, identity, rumble, analytics. -->
29
+ <!-- The Okona SDK: phones and gamepads, identity, rumble, saves, analytics. -->
30
30
  <script src="https://okonaonline.com/sdk/okona.js"></script>
31
+ <script src="phaser.min.js"></script>
31
32
  <script src="game.js"></script>
32
33
  <script>
33
34
  (function () {
34
35
  'use strict';
35
- // ---- the screen: a fixed 1920×1080 logical canvas scaled to fit ---------
36
- var canvas = document.getElementById('screen');
37
- var ctx = canvas.getContext('2d');
38
- var W = 1920, H = 1080;
39
- function resize() {
40
- var dpr = window.devicePixelRatio || 1;
41
- canvas.width = Math.floor(window.innerWidth * dpr);
42
- canvas.height = Math.floor(window.innerHeight * dpr);
43
- }
44
- window.addEventListener('resize', resize);
45
- resize();
36
+ var W = 960, H = 540; // the canvas: a 1080p TV's webview is 960x540 CSS px
46
37
 
47
- // ---- players ------------------------------------------------------------
38
+ // ---- players --------------------------------------------------------------
48
39
  // Six seats. A controller that connects is only CONNECTED; it becomes a
49
40
  // player (joined) when it presses A. Okona holds a seat while its
50
41
  // 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.
52
- var PALETTE = ['#E8A525', '#4FC3F7', '#81C784', '#F06292', '#BA68C8', '#FF8A65'];
42
+ // the same seat back when they return.
43
+ var COLORS = ['#E8A525', '#4FC3F7', '#81C784', '#F06292', '#BA68C8', '#FF8A65'];
53
44
  var players = [];
54
- for (var i = 0; i < 6; i++) players.push({ index: i, color: PALETTE[i], name: 'P' + (i + 1), score: 0, joined: false });
45
+ for (var i = 0; i < 6; i++) players.push({ index: i, color: COLORS[i], tint: parseInt(COLORS[i].slice(1), 16), name: 'P' + (i + 1), joined: false });
46
+
47
+ var state = 'lobby', elapsed = 0, scene = null, ui = null;
55
48
 
56
49
  function joinPlayer(p, pad) {
57
50
  p.joined = true;
58
- p.score = 0;
59
51
  // What this player sees on their phone: name, emoji or image, accent.
60
52
  Okona.players.setIdentity(pad, { name: p.name, icon: Game.icons[pad.index] || '🎮', accent: p.color });
61
- Game.onJoin(p, pad);
53
+ Game.onJoin(p, pad, state === 'playing');
62
54
  }
63
55
  function dropPlayer(p, pad) {
64
56
  if (!p.joined) return;
@@ -66,140 +58,126 @@
66
58
  Okona.players.clearIdentity(pad);
67
59
  Game.onLeave(p, pad);
68
60
  }
69
-
70
61
  // 'leave' = the seat went to another controller: that player is gone.
71
62
  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).
77
-
78
- // ---- lobby → playing → results -----------------------------------------
79
- var state = 'lobby';
80
- var elapsed = 0; // round time; stops while the Okona menu is open
81
63
 
82
64
  function count(pads, joinedOnly) {
83
65
  var n = 0;
84
- for (var i = 0; i < pads.length; i++) if (pads[i].connected && (!joinedOnly || players[i].joined)) n++;
66
+ for (var k = 0; k < 6; k++) if (pads[k].connected && (!joinedOnly || players[k].joined)) n++;
85
67
  return n;
86
68
  }
87
69
 
88
- function drawLobby(pads) {
89
- ctx.fillStyle = '#E8A525';
90
- ctx.textAlign = 'left';
91
- ctx.font = 'bold 72px system-ui, sans-serif';
92
- ctx.fillText(Game.title, 80, 150);
93
- ctx.fillStyle = '#A0B4C8';
94
- ctx.font = '30px system-ui, sans-serif';
95
- ctx.fillText(Game.subtitle, 80, 205);
96
- for (var i = 0; i < 6; i++) {
97
- var p = players[i], pad = pads[i];
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');
102
- ctx.font = 'bold 34px system-ui, sans-serif';
103
- ctx.fillText(label, 80, 300 + i * 56);
70
+ // ---- screens: Phaser text objects, made once; setText only changes them when the words change
71
+ function text(x, y, size, color, align) {
72
+ var t = scene.add.text(x, y, '', { fontFamily: 'system-ui, sans-serif', fontSize: size + 'px', fontStyle: 'bold', color: color });
73
+ t.setOrigin(align === 'center' ? 0.5 : 0, 0.5);
74
+ return t;
75
+ }
76
+ function buildUi() {
77
+ ui = { lobby: scene.add.container(0, 0), results: scene.add.container(0, 0), seats: [], seatColors: [], rows: [] };
78
+ ui.lobby.add(text(48, 70, 44, '#E8A525').setText(Game.title));
79
+ ui.lobby.add(text(48, 112, 20, '#A0B4C8').setText(Game.subtitle));
80
+ for (var s = 0; s < 6; s++) { ui.seats.push(text(48, 170 + s * 38, 22, '#2A4A6F')); ui.lobby.add(ui.seats[s]); }
81
+ ui.prompt = text(48, 430, 22, '#FFFFFF'); ui.lobby.add(ui.prompt);
82
+ ui.results.add(scene.add.rectangle(W / 2, H / 2, 720, 460, 0x0D1B2A, 0.88)); // over the game, so it reads
83
+ ui.results.add(text(W / 2, 90, 48, '#E8A525', 'center').setText('Results'));
84
+ for (var r = 0; r < 6; r++) { ui.rows.push(text(W / 2, 170 + r * 40, 26, '#FFFFFF', 'center')); ui.results.add(ui.rows[r]); }
85
+ ui.results.add(text(W / 2, 470, 22, '#A0B4C8', 'center').setText('Press A to play again'));
86
+ }
87
+ function show(next) {
88
+ state = next;
89
+ ui.lobby.setVisible(state === 'lobby');
90
+ ui.results.setVisible(state === 'results');
91
+ Game.show(state === 'playing' || state === 'results');
92
+ }
93
+ function updateLobby(pads) {
94
+ for (var s = 0; s < 6; s++) {
95
+ var p = players[s], pad = pads[s];
96
+ ui.seats[s].setText(p.joined ? '● ' + p.name + (pad.parked ? ' away' : ' ready')
97
+ : pad.connected ? '○ ' + p.name + ' press A to join' : '○ ' + p.name);
98
+ var color = p.joined ? p.color : (pad.connected ? '#A0B4C8' : '#2A4A6F');
99
+ if (ui.seatColors[s] !== color) { ui.seatColors[s] = color; ui.seats[s].setColor(color); } // setColor redraws the text: only on a change
104
100
  }
105
101
  var n = count(pads, true);
106
- ctx.fillStyle = '#FFFFFF';
107
- ctx.font = '30px system-ui, sans-serif';
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
- }
102
+ ui.prompt.setText(n === 0 ? (count(pads, false) ? 'Press A to join' : 'Waiting for players…')
103
+ : 'Press A to start · B to leave' + (Okona.mode === 'standalone' ? ' (keyboard: arrows + Space)' : ''));
114
104
  }
115
-
116
- function drawResults() {
117
- var ranked = players.slice().sort(function (a, b) { return b.score - a.score; });
118
- ctx.fillStyle = '#E8A525';
119
- ctx.textAlign = 'center';
120
- ctx.font = 'bold 84px system-ui, sans-serif';
121
- ctx.fillText('Results', W / 2, 200);
122
- ctx.font = 'bold 44px system-ui, sans-serif';
123
- var shown = 0;
124
- for (var i = 0; i < ranked.length && shown < 6; i++) {
125
- if (!ranked[i].joined) continue;
126
- ctx.fillStyle = ranked[i].color;
127
- ctx.fillText(ranked[i].name + ' ' + ranked[i].score, W / 2, 320 + shown * 70);
128
- shown++;
105
+ function showResults() {
106
+ var lines = Game.results(players);
107
+ for (var r = 0; r < 6; r++) {
108
+ var line = lines[r];
109
+ ui.rows[r].setText(line ? line.text : '').setColor(line ? line.color : '#FFFFFF');
129
110
  }
130
- ctx.fillStyle = '#A0B4C8';
131
- ctx.font = '30px system-ui, sans-serif';
132
- ctx.fillText('Press A to play again', W / 2, 900);
133
111
  }
134
112
 
135
- var last = performance.now();
136
- function frame(now) {
137
- var dt = Math.min(0.05, (now - last) / 1000);
138
- last = now;
139
- var pads = Okona.pads(); // exactly once per frame — a second call (or Okona.pad(i)) resets justPressed()
140
- var menuOpen = Okona.menuOpen === true;
113
+ // ---- the loop: Phaser's scene update, once per frame ------------------------
114
+ function update(time, delta) {
115
+ var dt = Math.min(0.05, delta / 1000);
116
+ var pads = Okona.pads(); // exactly once per frame: a second call (or Okona.pad(i)) resets justPressed()
117
+ if (Okona.menuOpen) return; // the Okona menu is over the game: everything freezes
141
118
 
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.
119
+ // A connected controller becomes a player by pressing A. That press only
120
+ // joins; it never also starts the race.
144
121
  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
- }
122
+ for (var j = 0; j < 6; j++) {
123
+ if (!players[j].joined && pads[j].connected && !pads[j].parked && pads[j].justPressed('a')) { joinPlayer(players[j], pads[j]); joinedNow[j] = true; }
151
124
  }
152
- // Any joined player who is here (never "P1 only": seat 1 may be empty).
125
+ // Any joined player who is here drives the flow (never "P1 only": seat 1 may be empty).
153
126
  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
- }
127
+ for (var k = 0; k < 6; k++) if (!joinedNow[k] && players[k].joined && pads[k].connected && !pads[k].parked && pads[k].justPressed(button)) return true;
157
128
  return false;
158
129
  }
159
130
 
160
- var sx = canvas.width / W, sy = canvas.height / H;
161
- ctx.setTransform(sx, 0, 0, sy, 0, 0);
162
- ctx.fillStyle = '#0D1B2A';
163
- ctx.fillRect(0, 0, W, H);
164
-
165
131
  if (state === 'lobby') {
166
- drawLobby(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
- }
132
+ for (var b = 0; b < 6; b++) if (!joinedNow[b] && players[b].joined && pads[b].connected && pads[b].justPressed('b')) dropPlayer(players[b], pads[b]);
133
+ updateLobby(pads);
134
+ if (count(pads, true) > 0 && pressed('a')) {
135
+ Game.start(players, pads);
136
+ elapsed = 0;
137
+ show('playing');
138
+ Okona.track('round_start', { players: count(pads, true) });
179
139
  }
180
140
  } else if (state === 'playing') {
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
- }
187
- Game.draw(ctx, players, pads, W, H);
188
- if (over) {
189
- state = 'results';
141
+ elapsed += dt;
142
+ if (Game.update(dt, players, pads, elapsed)) {
190
143
  Okona.track('round_end', { seconds: Math.round(elapsed) });
191
- // The round is over: let go of anyone still away.
144
+ showResults(); // everyone who raced, away or not
145
+ show('results');
146
+ // The race is over: let go of anyone still away.
192
147
  for (var m = 0; m < 6; m++) if (players[m].joined && (!pads[m].connected || pads[m].parked)) dropPlayer(players[m], pads[m]);
193
148
  }
194
- } else {
195
- drawResults();
196
- if (!menuOpen && pressed('a')) state = 'lobby';
149
+ } else if (pressed('a')) {
150
+ show('lobby');
197
151
  }
198
- requestAnimationFrame(frame);
199
152
  }
200
153
 
201
154
  Okona.ready(function () {
202
- requestAnimationFrame(frame);
155
+ new Phaser.Game({
156
+ type: Phaser.WEBGL,
157
+ width: W, height: H,
158
+ backgroundColor: '#0D1B2A',
159
+ disablePreFX: true, // top-level keys: without them Phaser reserves GPU memory
160
+ disablePostFX: true, // for effects at boot (about 90 MB at this size; 35 MB without)
161
+ banner: false,
162
+ fps: { target: 60 },
163
+ input: { keyboard: false, mouse: false, touch: false, gamepad: false }, // Okona.pads() is the input
164
+ scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH },
165
+ render: { antialiasGL: false, powerPreference: 'high-performance' },
166
+ scene: {
167
+ create: function () {
168
+ scene = this;
169
+ Game.create(scene, W, H);
170
+ buildUi();
171
+ show('lobby');
172
+ // Freeze anything Phaser runs on its own while the Okona menu is open.
173
+ Okona.on('menu', function (open) {
174
+ scene.time.paused = open;
175
+ if (open) { scene.tweens.pauseAll(); scene.sound.pauseAll(); } else { scene.tweens.resumeAll(); scene.sound.resumeAll(); }
176
+ });
177
+ },
178
+ update: update
179
+ }
180
+ });
203
181
  });
204
182
  })();
205
183
  </script>
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "title": "{{NAME}}",
3
- "description": "A multiplayer party game for phones and gamepads. Drop in any time: press A to join.",
4
- "genre": "multiplayer-party",
5
- "keywords": ["party", "multiplayer"],
3
+ "description": "Slot-car racing for one player or up to six. Hold A for gas, press left or right to change lanes, and lift before the corners or you spin out. Five laps; beat your best lap. Press A to join: new players join the next race.",
4
+ "genre": "racing",
5
+ "keywords": ["racing", "slot cars", "cars", "local multiplayer"],
6
6
  "controller": "basic",
7
7
  "icon": "icon.png"
8
8
  }