create-okona-game 0.5.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 +6 -5
- package/package.json +3 -3
- package/template/PHASER-LICENSE.md +20 -0
- package/template/README.md +13 -8
- package/template/game.js +194 -80
- 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/README.md
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
# create-okona-game
|
|
2
2
|
|
|
3
3
|
```
|
|
4
|
-
npm create okona-game my-
|
|
4
|
+
npm create okona-game my-game
|
|
5
5
|
```
|
|
6
6
|
|
|
7
|
-
Scaffolds a phone-controlled HTML5 game for [Okona](https://okonaonline.com)
|
|
7
|
+
Scaffolds a phone-controlled HTML5 game for [Okona](https://okonaonline.com), for one player or up to six, on [Phaser](https://phaser.io) 3.90 (bundled). It includes:
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- 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, and the game freezes while the Okona menu is open. There's no join code in the game: Okona shows it.
|
|
10
|
+
- Six seats, with names and colours pushed to each phone.
|
|
11
|
+
- The Phaser setup Okona recommends for a TV box ([Phaser on Okona](https://okonaonline.com/developers/docs.html#phaser)): a 960x540 canvas scaled to fit, and the effects pipelines off.
|
|
12
|
+
- A `game.js` holding a small slot-car race to replace with your game: hold A for gas, left and right to change lanes, lift before the corners. The best lap is kept with `Okona.save`.
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
Open `index.html` in a browser and the keyboard is P1. There's no build step and 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.
|
|
15
|
+
|
|
16
|
+
Zero dependencies to install. Docs: https://okonaonline.com/developers/docs.html#html5 · SDK reference: https://okonaonline.com/developers/docs.html#js-sdk
|
|
17
|
+
|
|
18
|
+
Phaser is © Richard Davey, Phaser Studio Inc., MIT licensed (`PHASER-LICENSE.md` in the scaffolded folder).
|
package/index.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
//
|
|
4
4
|
// npm create okona-game my-game (or: npx create-okona-game my-game)
|
|
5
5
|
//
|
|
6
|
-
// Copies the template (lobby
|
|
7
|
-
// game.js
|
|
8
|
-
//
|
|
6
|
+
// Copies the template (the lobby, seats and Phaser setup in index.html, your
|
|
7
|
+
// game in game.js: a slot-car race to replace; Phaser 3.90 bundled as
|
|
8
|
+
// phaser.min.js; an icon, an okona.json catalog record, a README) into
|
|
9
|
+
// ./my-game with the name filled in. Zero
|
|
9
10
|
// dependencies, Node ≥ 18. `--force` writes into a non-empty directory.
|
|
10
11
|
import { mkdir, readdir, readFile, writeFile, copyFile, stat } from "node:fs/promises";
|
|
11
12
|
import { join, resolve, dirname, basename } from "node:path";
|
|
@@ -36,7 +37,7 @@ export async function scaffold(target, { name, force = false } = {}) {
|
|
|
36
37
|
const src = join(TEMPLATE, f);
|
|
37
38
|
if ((await stat(src)).isDirectory()) continue;
|
|
38
39
|
const dest = join(dir, f === "gitignore" ? ".gitignore" : f);
|
|
39
|
-
if (/\.(html|js|md|json)$/.test(f) || f === "gitignore") {
|
|
40
|
+
if ((/\.(html|js|md|json)$/.test(f) && !/\.min\.js$/.test(f)) || f === "gitignore") { // phaser.min.js is copied as is
|
|
40
41
|
const text = (await readFile(src, "utf8")).split("{{NAME}}").join(title);
|
|
41
42
|
await writeFile(dest, text);
|
|
42
43
|
} else {
|
|
@@ -58,7 +59,7 @@ if (isMain) {
|
|
|
58
59
|
scaffold(target, { name, force }).then((r) => {
|
|
59
60
|
console.log(`\n Created ${r.title} in ${r.dir}\n`);
|
|
60
61
|
console.log(" Next:");
|
|
61
|
-
console.log(" open index.html in a browser — the keyboard is P1 (
|
|
62
|
+
console.log(" open index.html in a browser — the keyboard is P1 (arrows + Space; Shift = B)");
|
|
62
63
|
console.log(" edit game.js — start / update / draw are yours");
|
|
63
64
|
console.log(" edit okona.json — title, description, keywords (the icon is icon.png; add \"screenshots\": \"shots\" for a folder of them)");
|
|
64
65
|
console.log(" npx okona-cli login && npx okona-cli create # reads okona.json, writes the new game id back into it");
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-okona-game",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Scaffold a phone-controlled HTML5 game for Okona: lobby,
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Scaffold a phone-controlled HTML5 game for Okona on Phaser 3.90: lobby, six seats, the TV-safe Phaser setup, and a slot-car race in game.js to make yours.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://okonaonline.com/developers/docs.html#html5",
|
|
7
7
|
"bugs": {
|
|
8
8
|
"email": "support@okonaonline.com"
|
|
9
9
|
},
|
|
10
|
-
"keywords": ["okona", "create", "scaffold", "game", "html5", "phone-controller", "multiplayer", "
|
|
10
|
+
"keywords": ["okona", "create", "scaffold", "game", "html5", "phaser", "phone-controller", "multiplayer", "tv"],
|
|
11
11
|
"type": "module",
|
|
12
12
|
"bin": {
|
|
13
13
|
"create-okona-game": "index.js"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Richard Davey, Phaser Studio Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/template/README.md
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
# {{NAME}}
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A game for [Okona](https://okonaonline.com), for one player or up to six. Each person plays with a phone or a gamepad and joins by pressing A. Built on [Phaser](https://phaser.io) 3.90, the HTML5 engine Okona recommends.
|
|
4
4
|
|
|
5
|
-
- `index.html
|
|
6
|
-
- `game.js
|
|
7
|
-
- `
|
|
8
|
-
- `
|
|
5
|
+
- `index.html`: the lobby, seats and 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), plus the Phaser config Okona recommends for a TV box. It rarely needs changes.
|
|
6
|
+
- `game.js`: **your game**. The sample is a slot-car race (hold A for gas, left and right to change lanes, lift before the corners) to replace with your own: `create`, `start`, `update`, `results`, and the join/leave hooks.
|
|
7
|
+
- `phaser.min.js` + `PHASER-LICENSE.md`: Phaser 3.90.0 (MIT), bundled so no network can block it.
|
|
8
|
+
- `icon.png`: the icon shown in the Okona portal (replace it).
|
|
9
|
+
- `okona.json`: the catalog record: title, description, genre, keywords, the icon, screenshots. `okona push` syncs it.
|
|
9
10
|
|
|
10
11
|
## Play it locally
|
|
11
12
|
|
|
12
|
-
Open `index.html` in a browser (or `npx serve .`). The keyboard is P1:
|
|
13
|
+
Open `index.html` in a browser (or `npx serve .`). The keyboard is P1: arrows or WASD, Space is A, Shift is B. Phones need the real thing, below.
|
|
14
|
+
|
|
15
|
+
## Keep it fast on a TV
|
|
16
|
+
|
|
17
|
+
Okona plays on TV boxes with 2 GB shared by the CPU and the GPU. The setup in `index.html` is the one in [Phaser on Okona](https://okonaonline.com/developers/docs.html#phaser): a 960x540 canvas scaled to fit, and Phaser's effects pipelines off (`disablePreFX` / `disablePostFX`, or Phaser reserves GPU memory you never use). Keep both. Paint textures once (`textures.addImage`), reuse sprites, and change text only when its words change. Then play your sandbox on a real TV box for ten minutes before you submit.
|
|
13
18
|
|
|
14
19
|
## Submit to Okona
|
|
15
20
|
|
|
@@ -22,6 +27,6 @@ npx okona-cli push . --sandbox # catalog + icon + build in one go; try it w
|
|
|
22
27
|
npx okona-cli push . --publish # submit for review: Okona approves it into the catalog or sends it back
|
|
23
28
|
```
|
|
24
29
|
|
|
25
|
-
Screenshots: put up to six images in a `shots/` folder and add `"screenshots": "shots"` to `okona.json
|
|
30
|
+
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
31
|
|
|
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()
|
|
32
|
+
Docs: https://okonaonline.com/developers/docs.html#html5 · Phaser on Okona: https://okonaonline.com/developers/docs.html#phaser · SDK reference: https://okonaonline.com/developers/docs.html#js-sdk · The contract in one line: `Okona.pads()` once per frame, `Okona.on('join'|'leave'|'park'|'resume'|'menu')`, `Okona.menuOpen`, `Okona.players.setIdentity()`, `Okona.save`, `Okona.rumble()`, `Okona.track()`.
|
package/template/game.js
CHANGED
|
@@ -1,98 +1,212 @@
|
|
|
1
|
-
// {{NAME}}
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// {{NAME}}: the game. index.html runs the lobby (A joins), the seats and the
|
|
2
|
+
// loop, and calls the hooks below. The sample is a slot-car race: every car
|
|
3
|
+
// runs in a lane of an oval. Hold A for gas, B brakes, left and right change
|
|
4
|
+
// lanes (left = outward, right = inward: the race runs clockwise). Each lane
|
|
5
|
+
// has a safe corner speed: the inside lane is shorter but tighter. Go into a
|
|
6
|
+
// corner too fast and you spin. Five laps; new players join the next race.
|
|
7
|
+
// Replace it with your game.
|
|
5
8
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// Drawing: Phaser 3.90 (WebGL) on a 960x540 canvas that Phaser scales to fit.
|
|
10
|
+
// Textures are painted ONCE on a plain canvas and added with
|
|
11
|
+
// textures.addImage (never addCanvas: it reads every pixel back); sprites
|
|
12
|
+
// are made once and reused; text changes only when its words change.
|
|
13
|
+
// https://okonaonline.com/developers/docs.html#phaser
|
|
14
|
+
// Pads: pad.axes.lx/ly (-1..1, +Y down), pad.buttons.a/b/x/y/lb/rb/lt/rt/
|
|
15
|
+
// select/l3/r3/up/down/left/right, pad.justPressed('a'), pad.triggers.lt/rt
|
|
16
|
+
// (0..1), pad.parked (controller away: seat held). Start and Home are
|
|
17
|
+
// Okona's and never reach the game.
|
|
18
|
+
// Saves: Okona.save.get/set keep data for the player's account on any screen
|
|
19
|
+
// (the sample keeps the best lap). https://okonaonline.com/developers/docs.html#saves
|
|
13
20
|
var Game = {
|
|
14
21
|
title: '{{NAME}}',
|
|
15
|
-
subtitle: '
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
subtitle: 'Hold A for gas, left and right to change lanes. Lift before the corners!',
|
|
23
|
+
// Emoji (or a path to an image in this folder) shown on each player's phone.
|
|
24
|
+
icons: ['🏎️', '🏎️', '🏎️', '🏎️', '🏎️', '🏎️'],
|
|
25
|
+
LAPS: 5,
|
|
26
|
+
TOP_SPEED: 330, // px/s
|
|
27
|
+
GRIP: 360, // safe corner speed = sqrt(GRIP * lane radius)
|
|
28
|
+
CX: 480, CY: 290, L: 400, R: 160, // the oval: straight length and corner radius
|
|
29
|
+
LANES: [-26, 0, 26], // lane offsets from the centre line (negative = inside)
|
|
21
30
|
|
|
22
|
-
// ----
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
// ---- set up once: textures, the track, a car sprite per seat --------------
|
|
32
|
+
create: function (scene, W, H) {
|
|
33
|
+
this.scene = scene;
|
|
34
|
+
this.P = 2 * this.L + 2 * Math.PI * this.R; // centre-line length of one lap
|
|
35
|
+
var self = this;
|
|
36
|
+
this.paint('track', W, H, function (g) { self.paintTrack(g, W, H); });
|
|
37
|
+
this.paint('car', 30, 16, function (g) { // white: each car is tinted with its player's colour
|
|
38
|
+
g.fillStyle = '#FFFFFF'; g.fillRect(2, 1, 26, 14);
|
|
39
|
+
g.fillStyle = '#20262E'; g.fillRect(17, 3, 6, 10); g.fillRect(0, 0, 4, 4); g.fillRect(0, 12, 4, 4); g.fillRect(24, 0, 5, 3); g.fillRect(24, 13, 5, 3);
|
|
40
|
+
});
|
|
41
|
+
this.layer = scene.add.container(0, 0);
|
|
42
|
+
this.layer.add(scene.add.image(0, 0, 'track').setOrigin(0, 0));
|
|
43
|
+
this.cars = [];
|
|
44
|
+
for (var i = 0; i < 6; i++) {
|
|
45
|
+
var sprite = scene.add.image(0, 0, 'car').setVisible(false);
|
|
46
|
+
this.layer.add(sprite);
|
|
47
|
+
this.cars.push({ sprite: sprite });
|
|
48
|
+
}
|
|
49
|
+
// The infield: the countdown, a line per racer, the best lap. Text objects are made once.
|
|
50
|
+
var font = { fontFamily: 'system-ui, sans-serif', fontSize: '18px', fontStyle: 'bold', color: '#FFFFFF' };
|
|
51
|
+
this.banner = scene.add.text(this.CX, this.CY - 88, '', { fontFamily: 'system-ui, sans-serif', fontSize: '34px', fontStyle: 'bold', color: '#FFFFFF' }).setOrigin(0.5);
|
|
52
|
+
this.info = scene.add.text(this.CX, this.CY + 84, '', { fontFamily: 'system-ui, sans-serif', fontSize: '16px', color: '#A0B4C8', align: 'center' }).setOrigin(0.5);
|
|
53
|
+
this.layer.add([this.banner, this.info]);
|
|
54
|
+
this.board = [];
|
|
55
|
+
for (var k = 0; k < 6; k++) { this.board.push(scene.add.text(this.CX, this.CY - 52 + k * 22, '', font).setOrigin(0.5)); this.layer.add(this.board[k]); }
|
|
56
|
+
},
|
|
57
|
+
// A texture painted once with Canvas 2D, then handed to the GPU.
|
|
58
|
+
paint: function (key, w, h, fn) {
|
|
59
|
+
var c = document.createElement('canvas');
|
|
60
|
+
c.width = w; c.height = h;
|
|
61
|
+
fn(c.getContext('2d'));
|
|
62
|
+
this.scene.textures.addImage(key, c);
|
|
63
|
+
},
|
|
64
|
+
ovalPath: function (g, d) { // the oval at lane offset d
|
|
65
|
+
var r = this.R + d, x0 = this.CX - this.L / 2, x1 = this.CX + this.L / 2;
|
|
66
|
+
g.beginPath();
|
|
67
|
+
g.moveTo(x0, this.CY - r); g.lineTo(x1, this.CY - r);
|
|
68
|
+
g.arc(x1, this.CY, r, -Math.PI / 2, Math.PI / 2);
|
|
69
|
+
g.lineTo(x0, this.CY + r);
|
|
70
|
+
g.arc(x0, this.CY, r, Math.PI / 2, Math.PI * 1.5);
|
|
71
|
+
g.closePath();
|
|
72
|
+
},
|
|
73
|
+
paintTrack: function (g, W, H) {
|
|
74
|
+
g.fillStyle = '#2E5E3A'; g.fillRect(0, 0, W, H);
|
|
75
|
+
g.lineJoin = 'round';
|
|
76
|
+
g.strokeStyle = '#D9D9D9'; g.lineWidth = 104; this.ovalPath(g, 0); g.stroke(); // kerbs
|
|
77
|
+
g.strokeStyle = '#3A3F47'; g.lineWidth = 94; this.ovalPath(g, 0); g.stroke(); // tarmac
|
|
78
|
+
g.strokeStyle = '#59606B'; g.lineWidth = 2; g.setLineDash([10, 12]);
|
|
79
|
+
this.ovalPath(g, -13); g.stroke(); this.ovalPath(g, 13); g.stroke(); // lane lines
|
|
80
|
+
g.setLineDash([]);
|
|
81
|
+
for (var k = 0; k < 10; k++) { // the start line
|
|
82
|
+
g.fillStyle = k % 2 ? '#FFFFFF' : '#111111';
|
|
83
|
+
g.fillRect(this.CX - 4, this.CY - this.R - 47 + k * 9.4, 8, 9.4);
|
|
84
|
+
}
|
|
25
85
|
},
|
|
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
|
|
28
|
-
onResume: function (player, pad) {},
|
|
29
86
|
|
|
87
|
+
// ---- players ---------------------------------------------------------------
|
|
88
|
+
onJoin: function (player, pad, racing) { player.waiting = racing; }, // mid-race: joins the next race
|
|
89
|
+
onLeave: function (player, pad) { this.cars[player.index].sprite.setVisible(false); },
|
|
90
|
+
|
|
91
|
+
// ---- a race ------------------------------------------------------------------
|
|
30
92
|
start: function (players, pads) {
|
|
31
|
-
this.best = Okona.save.get('
|
|
32
|
-
this.
|
|
33
|
-
|
|
34
|
-
|
|
93
|
+
this.best = Okona.save.get('bestLap', 0); // the best lap on this account, kept by Okona
|
|
94
|
+
this.finished = 0; this.winnerAt = 0;
|
|
95
|
+
var grid = 0;
|
|
96
|
+
for (var i = 0; i < 6; i++) {
|
|
97
|
+
var car = this.cars[i], p = players[i];
|
|
98
|
+
p.waiting = false;
|
|
99
|
+
car.racing = p.joined;
|
|
100
|
+
car.sprite.setVisible(car.racing).setTint(p.tint).setAlpha(1);
|
|
101
|
+
if (!car.racing) continue;
|
|
102
|
+
car.lane = grid % 3; car.d = this.LANES[car.lane];
|
|
103
|
+
car.s = this.P - 40 - Math.floor(grid / 3) * 44; // the grid, behind the line: rows of three
|
|
104
|
+
car.lap = 0; car.v = 0; car.spin = 0; car.turn = 0; car.held = 0;
|
|
105
|
+
car.lapStart = 2; car.bestLap = 0; car.finishTime = 0;
|
|
106
|
+
grid++;
|
|
107
|
+
this.place(car);
|
|
108
|
+
}
|
|
109
|
+
for (var b = 0; b < 6; b++) this.board[b].setText('');
|
|
35
110
|
},
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
111
|
+
// Where a car is: the centre line runs clockwise from the start line (top middle).
|
|
112
|
+
place: function (car) {
|
|
113
|
+
var s = ((car.s % this.P) + this.P) % this.P, L = this.L, R = this.R, r = R + car.d, x, y, a;
|
|
114
|
+
var half = L / 2, arc = Math.PI * R;
|
|
115
|
+
if (s < half) { x = this.CX + s; y = this.CY - r; a = 0; }
|
|
116
|
+
else if (s < half + arc) { a = (s - half) / R; x = this.CX + half + r * Math.sin(a); y = this.CY - r * Math.cos(a); }
|
|
117
|
+
else if (s < half + arc + L) { x = this.CX + half - (s - half - arc); y = this.CY + r; a = Math.PI; }
|
|
118
|
+
else if (s < half + 2 * arc + L) { a = (s - half - arc - L) / R; x = this.CX - half - r * Math.sin(a); y = this.CY + r * Math.cos(a); a += Math.PI; }
|
|
119
|
+
else { x = this.CX - half + (s - half - 2 * arc - L); y = this.CY - r; a = 0; }
|
|
120
|
+
car.inCorner = (s >= half && s < half + arc) || (s >= half + arc + L && s < half + 2 * arc + L);
|
|
121
|
+
car.sprite.setPosition(x, y).setRotation(a + car.turn);
|
|
39
122
|
},
|
|
40
123
|
|
|
41
|
-
// Return true when the
|
|
124
|
+
// Return true when the race is over.
|
|
42
125
|
update: function (dt, players, pads, elapsed) {
|
|
126
|
+
var GO = 2, racers = 0, home = 0, time = elapsed - GO;
|
|
43
127
|
for (var i = 0; i < 6; i++) {
|
|
44
|
-
var pad = pads[i], p = players[i];
|
|
45
|
-
if (!
|
|
46
|
-
var
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
128
|
+
var car = this.cars[i], pad = pads[i], p = players[i];
|
|
129
|
+
if (!car.racing || !p.joined) continue;
|
|
130
|
+
var here = pad.connected && !pad.parked;
|
|
131
|
+
car.sprite.setAlpha(here ? 1 : 0.35); // away: a ghost that waits; it never holds up the race
|
|
132
|
+
if (here) racers++;
|
|
133
|
+
if (car.finishTime) { home += here ? 1 : 0; continue; }
|
|
134
|
+
if (!here || time < 0) continue;
|
|
135
|
+
// lanes: hold left/right to keep moving over, one lane every 0.25 s
|
|
136
|
+
var dir = pad.buttons.right || pad.axes.lx > 0.5 ? -1 : pad.buttons.left || pad.axes.lx < -0.5 ? 1 : 0;
|
|
137
|
+
car.held = dir ? car.held - dt : 0;
|
|
138
|
+
if (dir && car.held <= 0 && car.spin <= 0) { car.lane = Math.max(0, Math.min(2, car.lane + dir)); car.held = 0.25; }
|
|
139
|
+
var want = this.LANES[car.lane];
|
|
140
|
+
car.d += Math.max(-dt * 140, Math.min(dt * 140, want - car.d));
|
|
141
|
+
// speed: gas, brake, or the motor drags it down
|
|
142
|
+
var gas = Math.max(pad.buttons.a ? 1 : 0, pad.triggers.rt);
|
|
143
|
+
if (car.spin > 0) { car.spin -= dt; car.turn += dt * 14; car.v *= Math.pow(0.2, dt); if (car.spin <= 0) car.turn = 0; }
|
|
144
|
+
else if (pad.buttons.b) car.v = Math.max(0, car.v - 600 * dt);
|
|
145
|
+
else if (gas) car.v = Math.min(this.TOP_SPEED, car.v + 260 * gas * dt);
|
|
146
|
+
else car.v = Math.max(0, car.v - 220 * dt);
|
|
147
|
+
// too fast for this lane's corner: spin
|
|
148
|
+
if (car.inCorner && car.spin <= 0 && car.v > Math.sqrt(this.GRIP * (this.R + car.d)) * 1.08) {
|
|
149
|
+
car.spin = 0.9; car.v *= 0.4;
|
|
150
|
+
Okona.rumble(pad, 0.8, 0.4);
|
|
59
151
|
}
|
|
152
|
+
// move along the centre line: an inside lane covers ground faster in the corners
|
|
153
|
+
var before = car.s;
|
|
154
|
+
car.s += car.v * dt * (car.inCorner ? this.R / (this.R + car.d) : 1);
|
|
155
|
+
if (Math.floor(car.s / this.P) > Math.floor(before / this.P)) this.lapDone(car, p, time);
|
|
156
|
+
this.place(car);
|
|
60
157
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
158
|
+
this.drawBoard(players, pads);
|
|
159
|
+
this.banner.setText(time < 0 ? String(Math.ceil(-time)) : time < 1 ? 'GO!' : '');
|
|
160
|
+
this.info.setText(time < 0 ? this.LAPS + ' laps' : 'Best lap ' + (this.best ? this.best.toFixed(2) + ' s' : '-') + this.waitingLine(players));
|
|
161
|
+
// over when every racer who is here is home, or 15 s after the winner
|
|
162
|
+
return (racers > 0 && home === racers) || (this.winnerAt && time - this.winnerAt > 15) || racers === 0 && time > 0;
|
|
64
163
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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(); }
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
draw: function (ctx, players, pads, W, H) {
|
|
74
|
-
ctx.strokeStyle = '#2A4A6F'; ctx.lineWidth = 6; ctx.strokeRect(30, 30, W - 60, H - 60);
|
|
75
|
-
// coins
|
|
76
|
-
for (var c = 0; c < this.coins.length; c++) {
|
|
77
|
-
var coin = this.coins[c];
|
|
78
|
-
ctx.fillStyle = '#F5C04A';
|
|
79
|
-
ctx.beginPath(); ctx.arc(coin.x, coin.y, coin.r, 0, Math.PI * 2); ctx.fill();
|
|
80
|
-
}
|
|
81
|
-
// players
|
|
164
|
+
// One line per racer, in seat order; setText only redraws a line whose words changed.
|
|
165
|
+
drawBoard: function (players, pads) {
|
|
166
|
+
var row = 0;
|
|
82
167
|
for (var i = 0; i < 6; i++) {
|
|
83
|
-
var
|
|
84
|
-
if (!p.joined) continue;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
168
|
+
var car = this.cars[i], p = players[i];
|
|
169
|
+
if (!car.racing || !p.joined) continue;
|
|
170
|
+
var here = pads[i].connected && !pads[i].parked;
|
|
171
|
+
this.board[row].setText(p.name + ' ' + (car.finishTime ? 'home ' + car.finishTime.toFixed(2) + ' s'
|
|
172
|
+
: !here ? 'away' : 'lap ' + Math.max(1, Math.min(car.lap, this.LAPS)) + ' / ' + this.LAPS));
|
|
173
|
+
if (this.board[row].style.color !== p.color) this.board[row].setColor(p.color); // setColor redraws: only on a change
|
|
174
|
+
row++;
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
waitingLine: function (players) {
|
|
178
|
+
var names = players.filter(function (p) { return p.joined && p.waiting; }).map(function (p) { return p.name; });
|
|
179
|
+
return names.length ? '\n' + names.join(', ') + ' joining the next race' : '';
|
|
180
|
+
},
|
|
181
|
+
lapDone: function (car, player, time) {
|
|
182
|
+
car.lap++;
|
|
183
|
+
if (car.lap === 1) { car.lapStart = time; return; } // crossing the line from the grid starts lap 1
|
|
184
|
+
var lapTime = time - car.lapStart;
|
|
185
|
+
car.lapStart = time;
|
|
186
|
+
if (!car.bestLap || lapTime < car.bestLap) car.bestLap = lapTime;
|
|
187
|
+
if (!this.best || lapTime < this.best) { this.best = lapTime; Okona.save.set('bestLap', Math.round(lapTime * 100) / 100); }
|
|
188
|
+
if (car.lap > this.LAPS) {
|
|
189
|
+
car.finishTime = time; car.v = 0;
|
|
190
|
+
if (!this.winnerAt) this.winnerAt = time;
|
|
191
|
+
Okona.save.flush();
|
|
91
192
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
// ---- results: one line per racer, best first ---------------------------------
|
|
196
|
+
results: function (players) {
|
|
197
|
+
var cars = this.cars, rows = [];
|
|
198
|
+
for (var i = 0; i < 6; i++) if (cars[i].racing && players[i].joined) rows.push(i);
|
|
199
|
+
rows.sort(function (a, b) {
|
|
200
|
+
var A = cars[a], B = cars[b];
|
|
201
|
+
if (A.finishTime && B.finishTime) return A.finishTime - B.finishTime;
|
|
202
|
+
if (A.finishTime || B.finishTime) return A.finishTime ? -1 : 1;
|
|
203
|
+
return B.s - A.s; // unfinished: placed by distance
|
|
204
|
+
});
|
|
205
|
+
return rows.map(function (i, place) {
|
|
206
|
+
var c = cars[i];
|
|
207
|
+
return { color: players[i].color, text: (place + 1) + '. ' + players[i].name + ' ' +
|
|
208
|
+
(c.finishTime ? c.finishTime.toFixed(2) + ' s' : 'did not finish') + (c.bestLap ? ' best lap ' + c.bestLap.toFixed(2) + ' s' : '') };
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
show: function (on) { this.layer.setVisible(on); }
|
|
98
212
|
};
|
package/template/icon.png
CHANGED
|
Binary file
|