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.
- package/README.md +11 -4
- package/index.js +7 -6
- package/package.json +3 -3
- package/template/PHASER-LICENSE.md +20 -0
- package/template/README.md +13 -8
- package/template/game.js +194 -66
- package/template/icon.png +0 -0
- package/template/index.html +104 -126
- package/template/okona.json +3 -3
- package/template/phaser.min.js +1 -0
package/template/index.html
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<!--
|
|
3
|
-
{{NAME}}
|
|
3
|
+
{{NAME}}: an Okona game, built on Phaser 3.90 (bundled: phaser.min.js).
|
|
4
4
|
|
|
5
|
-
This file is the LOBBY and the
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
52
|
-
var
|
|
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:
|
|
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
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
117
|
-
var
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
136
|
-
function
|
|
137
|
-
var dt = Math.min(0.05,
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
143
|
-
//
|
|
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
|
|
146
|
-
|
|
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
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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>
|
package/template/okona.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "{{NAME}}",
|
|
3
|
-
"description": "
|
|
4
|
-
"genre": "
|
|
5
|
-
"keywords": ["
|
|
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
|
}
|