groove-dev 0.27.113 → 0.27.116
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/CENTRAL_COMMAND_REBUILD.md +689 -0
- package/EMBEDDING_DIAGNOSTIC.md +197 -0
- package/TRAINING_DATA_v4.md +6 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/team.js +59 -2
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +27 -2
- package/node_modules/@groove-dev/daemon/src/filewatcher.js +45 -0
- package/node_modules/@groove-dev/daemon/src/index.js +14 -2
- package/node_modules/@groove-dev/daemon/src/process.js +254 -208
- package/node_modules/@groove-dev/daemon/src/teams.js +143 -20
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +78 -45
- package/node_modules/@groove-dev/gui/dist/assets/index-DdN9RVnC.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-BYh6iHqL.js → index-fq--PD7_.js} +1731 -1731
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +0 -22
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +43 -45
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/components/teams/team-removal-dialog.jsx +156 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +57 -12
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +23 -4
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -20
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +84 -5
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/team.js +59 -2
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +27 -2
- package/packages/daemon/src/filewatcher.js +45 -0
- package/packages/daemon/src/index.js +14 -2
- package/packages/daemon/src/process.js +254 -208
- package/packages/daemon/src/teams.js +143 -20
- package/packages/daemon/src/tunnel-manager.js +78 -45
- package/packages/gui/dist/assets/index-DdN9RVnC.css +1 -0
- package/packages/gui/dist/assets/{index-BYh6iHqL.js → index-fq--PD7_.js} +1731 -1731
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/workspace-mode.jsx +0 -22
- package/packages/gui/src/components/layout/status-bar.jsx +43 -45
- package/packages/gui/src/components/settings/quick-connect.jsx +2 -1
- package/packages/gui/src/components/teams/team-removal-dialog.jsx +156 -0
- package/packages/gui/src/stores/groove.js +57 -12
- package/packages/gui/src/views/agents.jsx +23 -4
- package/packages/gui/src/views/editor.jsx +1 -20
- package/packages/gui/src/views/teams.jsx +84 -5
- package/TRAINING_DATA_v3.md +0 -11
- package/codex-test/offroad-nitro-racer/dist/assets/index-CuvdKK6U.js +0 -44
- package/codex-test/offroad-nitro-racer/dist/assets/index-DvHn2Thu.css +0 -1
- package/codex-test/offroad-nitro-racer/dist/index.html +0 -23
- package/codex-test/offroad-nitro-racer/index.html +0 -21
- package/codex-test/offroad-nitro-racer/package-lock.json +0 -841
- package/codex-test/offroad-nitro-racer/package.json +0 -15
- package/codex-test/offroad-nitro-racer/src/game/AI.ts +0 -28
- package/codex-test/offroad-nitro-racer/src/game/Audio.ts +0 -63
- package/codex-test/offroad-nitro-racer/src/game/Car.ts +0 -247
- package/codex-test/offroad-nitro-racer/src/game/Effects.ts +0 -62
- package/codex-test/offroad-nitro-racer/src/game/Game.ts +0 -229
- package/codex-test/offroad-nitro-racer/src/game/Input.ts +0 -45
- package/codex-test/offroad-nitro-racer/src/game/Renderer.ts +0 -224
- package/codex-test/offroad-nitro-racer/src/game/Track.ts +0 -158
- package/codex-test/offroad-nitro-racer/src/game/UI.ts +0 -96
- package/codex-test/offroad-nitro-racer/src/game/math.ts +0 -42
- package/codex-test/offroad-nitro-racer/src/main.ts +0 -24
- package/codex-test/offroad-nitro-racer/src/style.css +0 -291
- package/codex-test/offroad-nitro-racer/src/vite-env.d.ts +0 -1
- package/codex-test/offroad-nitro-racer/tsconfig.json +0 -18
- package/codex-test/offroad-nitro-racer/vite.config.ts +0 -7
- package/node_modules/@groove-dev/gui/dist/assets/index-DAlSbVyK.css +0 -1
- package/packages/gui/dist/assets/index-DAlSbVyK.css +0 -1
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { Car } from './Car';
|
|
2
|
-
import { Effects } from './Effects';
|
|
3
|
-
import { Track } from './Track';
|
|
4
|
-
import { roundedRect } from './math';
|
|
5
|
-
|
|
6
|
-
export type Camera = { x: number; y: number; zoom: number; shakeX: number; shakeY: number };
|
|
7
|
-
|
|
8
|
-
export class Renderer {
|
|
9
|
-
private canvas: HTMLCanvasElement;
|
|
10
|
-
private ctx: CanvasRenderingContext2D;
|
|
11
|
-
private pixelRatio = 1;
|
|
12
|
-
|
|
13
|
-
constructor(canvas: HTMLCanvasElement) {
|
|
14
|
-
this.canvas = canvas;
|
|
15
|
-
const ctx = canvas.getContext('2d');
|
|
16
|
-
if (!ctx) throw new Error('Canvas 2D context unavailable');
|
|
17
|
-
this.ctx = ctx;
|
|
18
|
-
this.resize();
|
|
19
|
-
window.addEventListener('resize', () => this.resize());
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
get width(): number {
|
|
23
|
-
return this.canvas.clientWidth;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
get height(): number {
|
|
27
|
-
return this.canvas.clientHeight;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
resize(): void {
|
|
31
|
-
this.pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
|
|
32
|
-
const width = window.innerWidth;
|
|
33
|
-
const height = window.innerHeight;
|
|
34
|
-
this.canvas.width = Math.floor(width * this.pixelRatio);
|
|
35
|
-
this.canvas.height = Math.floor(height * this.pixelRatio);
|
|
36
|
-
this.canvas.style.width = `${width}px`;
|
|
37
|
-
this.canvas.style.height = `${height}px`;
|
|
38
|
-
this.ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
render(track: Track, cars: Car[], player: Car, effects: Effects, camera: Camera): void {
|
|
42
|
-
const ctx = this.ctx;
|
|
43
|
-
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
|
|
44
|
-
ctx.clearRect(0, 0, this.width, this.height);
|
|
45
|
-
|
|
46
|
-
const gradient = ctx.createLinearGradient(0, 0, this.width, this.height);
|
|
47
|
-
gradient.addColorStop(0, '#70451f');
|
|
48
|
-
gradient.addColorStop(0.45, '#b67635');
|
|
49
|
-
gradient.addColorStop(1, '#6c3b1f');
|
|
50
|
-
ctx.fillStyle = gradient;
|
|
51
|
-
ctx.fillRect(0, 0, this.width, this.height);
|
|
52
|
-
|
|
53
|
-
ctx.save();
|
|
54
|
-
ctx.translate(this.width / 2 + camera.shakeX, this.height / 2 + camera.shakeY);
|
|
55
|
-
ctx.scale(camera.zoom, camera.zoom * 0.86);
|
|
56
|
-
ctx.translate(-camera.x, -camera.y);
|
|
57
|
-
|
|
58
|
-
this.drawGroundNoise(ctx, camera);
|
|
59
|
-
this.drawTrack(ctx, track);
|
|
60
|
-
this.drawPickups(ctx, track);
|
|
61
|
-
this.drawObstacles(ctx, track);
|
|
62
|
-
effects.render(ctx);
|
|
63
|
-
|
|
64
|
-
const sortedCars = [...cars].sort((a, b) => a.y + a.airborne - (b.y + b.airborne));
|
|
65
|
-
for (const car of sortedCars) this.drawCar(ctx, car, car === player);
|
|
66
|
-
|
|
67
|
-
ctx.restore();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private drawGroundNoise(ctx: CanvasRenderingContext2D, camera: Camera): void {
|
|
71
|
-
ctx.save();
|
|
72
|
-
ctx.globalAlpha = 0.22;
|
|
73
|
-
for (let i = 0; i < 130; i++) {
|
|
74
|
-
const x = Math.floor((camera.x - 900) / 90) * 90 + ((i * 277) % 1900);
|
|
75
|
-
const y = Math.floor((camera.y - 650) / 70) * 70 + ((i * 163) % 1400);
|
|
76
|
-
ctx.fillStyle = i % 6 === 0 ? '#5c341f' : '#d29554';
|
|
77
|
-
ctx.beginPath();
|
|
78
|
-
ctx.ellipse(x, y, 18 + (i % 4) * 8, 5, (i % 9) * 0.5, 0, Math.PI * 2);
|
|
79
|
-
ctx.fill();
|
|
80
|
-
}
|
|
81
|
-
ctx.restore();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
private drawTrack(ctx: CanvasRenderingContext2D, track: Track): void {
|
|
85
|
-
ctx.lineJoin = 'round';
|
|
86
|
-
ctx.lineCap = 'round';
|
|
87
|
-
|
|
88
|
-
ctx.strokeStyle = '#5b3d2c';
|
|
89
|
-
ctx.lineWidth = track.roughWidth;
|
|
90
|
-
this.strokeLoop(ctx, track.points);
|
|
91
|
-
|
|
92
|
-
ctx.strokeStyle = '#9a6131';
|
|
93
|
-
ctx.lineWidth = track.width;
|
|
94
|
-
this.strokeLoop(ctx, track.points);
|
|
95
|
-
|
|
96
|
-
ctx.setLineDash([34, 34]);
|
|
97
|
-
ctx.strokeStyle = 'rgba(255, 220, 137, 0.34)';
|
|
98
|
-
ctx.lineWidth = 5;
|
|
99
|
-
this.strokeLoop(ctx, track.points);
|
|
100
|
-
ctx.setLineDash([]);
|
|
101
|
-
|
|
102
|
-
for (let i = 0; i < track.checkpoints.length; i++) {
|
|
103
|
-
const p = track.checkpoints[i];
|
|
104
|
-
ctx.save();
|
|
105
|
-
ctx.translate(p.x, p.y);
|
|
106
|
-
ctx.fillStyle = i === 0 ? 'rgba(80,255,190,0.35)' : 'rgba(255,255,255,0.14)';
|
|
107
|
-
ctx.fillRect(-95, -5, 190, 10);
|
|
108
|
-
ctx.restore();
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
private drawPickups(ctx: CanvasRenderingContext2D, track: Track): void {
|
|
113
|
-
for (const pickup of track.pickups) {
|
|
114
|
-
if (!pickup.active) continue;
|
|
115
|
-
ctx.save();
|
|
116
|
-
ctx.translate(pickup.x, pickup.y);
|
|
117
|
-
ctx.rotate(performance.now() * 0.004);
|
|
118
|
-
ctx.fillStyle = '#32f5ff';
|
|
119
|
-
ctx.shadowColor = '#32f5ff';
|
|
120
|
-
ctx.shadowBlur = 18;
|
|
121
|
-
ctx.beginPath();
|
|
122
|
-
ctx.moveTo(0, -26);
|
|
123
|
-
ctx.lineTo(20, 0);
|
|
124
|
-
ctx.lineTo(0, 26);
|
|
125
|
-
ctx.lineTo(-20, 0);
|
|
126
|
-
ctx.closePath();
|
|
127
|
-
ctx.fill();
|
|
128
|
-
ctx.shadowBlur = 0;
|
|
129
|
-
ctx.fillStyle = '#06343b';
|
|
130
|
-
ctx.font = 'bold 20px sans-serif';
|
|
131
|
-
ctx.textAlign = 'center';
|
|
132
|
-
ctx.textBaseline = 'middle';
|
|
133
|
-
ctx.fillText('N', 0, 1);
|
|
134
|
-
ctx.restore();
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private drawObstacles(ctx: CanvasRenderingContext2D, track: Track): void {
|
|
139
|
-
for (const obstacle of track.obstacles) {
|
|
140
|
-
ctx.save();
|
|
141
|
-
ctx.translate(obstacle.x, obstacle.y);
|
|
142
|
-
if (obstacle.kind === 'bump') {
|
|
143
|
-
ctx.fillStyle = '#c88a4c';
|
|
144
|
-
ctx.strokeStyle = '#5f3925';
|
|
145
|
-
ctx.lineWidth = 5;
|
|
146
|
-
ctx.beginPath();
|
|
147
|
-
ctx.ellipse(0, 0, obstacle.radius, obstacle.radius * 0.36, 0, 0, Math.PI * 2);
|
|
148
|
-
ctx.fill();
|
|
149
|
-
ctx.stroke();
|
|
150
|
-
} else if (obstacle.kind === 'cactus') {
|
|
151
|
-
ctx.fillStyle = '#216941';
|
|
152
|
-
ctx.fillRect(-8, -34, 16, 68);
|
|
153
|
-
ctx.fillRect(-25, -12, 50, 13);
|
|
154
|
-
} else if (obstacle.kind === 'barrel') {
|
|
155
|
-
ctx.fillStyle = '#c14c32';
|
|
156
|
-
ctx.strokeStyle = '#621f1c';
|
|
157
|
-
ctx.lineWidth = 5;
|
|
158
|
-
ctx.beginPath();
|
|
159
|
-
ctx.arc(0, 0, obstacle.radius, 0, Math.PI * 2);
|
|
160
|
-
ctx.fill();
|
|
161
|
-
ctx.stroke();
|
|
162
|
-
} else {
|
|
163
|
-
ctx.fillStyle = '#4b4038';
|
|
164
|
-
ctx.beginPath();
|
|
165
|
-
ctx.ellipse(0, 0, obstacle.radius, obstacle.radius * 0.8, 0.4, 0, Math.PI * 2);
|
|
166
|
-
ctx.fill();
|
|
167
|
-
}
|
|
168
|
-
ctx.restore();
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
private drawCar(ctx: CanvasRenderingContext2D, car: Car, isPlayer: boolean): void {
|
|
173
|
-
ctx.save();
|
|
174
|
-
ctx.translate(car.x, car.y);
|
|
175
|
-
ctx.globalAlpha = 0.35;
|
|
176
|
-
ctx.fillStyle = '#000';
|
|
177
|
-
ctx.beginPath();
|
|
178
|
-
ctx.ellipse(0, 18 + car.airborne * 0.18, car.width * 0.76, car.length * 0.42, car.angle, 0, Math.PI * 2);
|
|
179
|
-
ctx.fill();
|
|
180
|
-
ctx.globalAlpha = 1;
|
|
181
|
-
ctx.translate(0, -car.airborne * 0.2);
|
|
182
|
-
ctx.rotate(car.angle);
|
|
183
|
-
|
|
184
|
-
ctx.fillStyle = '#171717';
|
|
185
|
-
roundedRect(ctx, -car.length * 0.46, -car.width * 0.72, car.length * 0.92, car.width * 1.44, 8);
|
|
186
|
-
ctx.fill();
|
|
187
|
-
|
|
188
|
-
ctx.fillStyle = car.color;
|
|
189
|
-
roundedRect(ctx, -car.length * 0.5, -car.width * 0.5, car.length, car.width, 10);
|
|
190
|
-
ctx.fill();
|
|
191
|
-
|
|
192
|
-
ctx.fillStyle = car.accent;
|
|
193
|
-
roundedRect(ctx, -2, -car.width * 0.42, car.length * 0.26, car.width * 0.84, 6);
|
|
194
|
-
ctx.fill();
|
|
195
|
-
|
|
196
|
-
ctx.fillStyle = '#dbeafe';
|
|
197
|
-
roundedRect(ctx, -car.length * 0.2, -car.width * 0.32, car.length * 0.22, car.width * 0.64, 5);
|
|
198
|
-
ctx.fill();
|
|
199
|
-
|
|
200
|
-
ctx.fillStyle = '#24211f';
|
|
201
|
-
for (const y of [-car.width * 0.62, car.width * 0.62]) {
|
|
202
|
-
roundedRect(ctx, -car.length * 0.38, y - 5, 22, 10, 3);
|
|
203
|
-
ctx.fill();
|
|
204
|
-
roundedRect(ctx, car.length * 0.14, y - 5, 22, 10, 3);
|
|
205
|
-
ctx.fill();
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (isPlayer) {
|
|
209
|
-
ctx.strokeStyle = '#ffffff';
|
|
210
|
-
ctx.lineWidth = 4;
|
|
211
|
-
ctx.strokeRect(-car.length * 0.58, -car.width * 0.58, car.length * 1.16, car.width * 1.16);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
ctx.restore();
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private strokeLoop(ctx: CanvasRenderingContext2D, points: { x: number; y: number }[]): void {
|
|
218
|
-
ctx.beginPath();
|
|
219
|
-
ctx.moveTo(points[0].x, points[0].y);
|
|
220
|
-
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i].x, points[i].y);
|
|
221
|
-
ctx.closePath();
|
|
222
|
-
ctx.stroke();
|
|
223
|
-
}
|
|
224
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { TAU, Vec2, clamp, distance } from './math';
|
|
2
|
-
|
|
3
|
-
export type Terrain = 'track' | 'sand' | 'rough' | 'mud';
|
|
4
|
-
|
|
5
|
-
export type TrackSample = {
|
|
6
|
-
terrain: Terrain;
|
|
7
|
-
grip: number;
|
|
8
|
-
speedMultiplier: number;
|
|
9
|
-
nearest: Vec2;
|
|
10
|
-
distanceFromCenter: number;
|
|
11
|
-
progress: number;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export type Obstacle = Vec2 & { radius: number; kind: 'rock' | 'cactus' | 'barrel' | 'bump' };
|
|
15
|
-
export type Pickup = Vec2 & { radius: number; active: boolean; respawn: number };
|
|
16
|
-
|
|
17
|
-
const CHECKPOINTS = 12;
|
|
18
|
-
|
|
19
|
-
export class Track {
|
|
20
|
-
readonly width = 230;
|
|
21
|
-
readonly roughWidth = 390;
|
|
22
|
-
readonly lapsToWin = 3;
|
|
23
|
-
readonly center: Vec2 = { x: 0, y: 0 };
|
|
24
|
-
readonly points: Vec2[] = [];
|
|
25
|
-
readonly checkpoints: Vec2[] = [];
|
|
26
|
-
readonly obstacles: Obstacle[] = [];
|
|
27
|
-
readonly pickups: Pickup[] = [];
|
|
28
|
-
|
|
29
|
-
constructor() {
|
|
30
|
-
for (let i = 0; i < 220; i++) {
|
|
31
|
-
const t = i / 220;
|
|
32
|
-
const a = t * TAU;
|
|
33
|
-
const wobble = Math.sin(a * 3) * 55 + Math.cos(a * 5) * 24;
|
|
34
|
-
this.points.push({
|
|
35
|
-
x: Math.cos(a) * (890 + wobble) + Math.cos(a * 2.2) * 75,
|
|
36
|
-
y: Math.sin(a) * (550 + Math.sin(a * 4) * 50) + Math.sin(a * 1.5) * 60,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
for (let i = 0; i < CHECKPOINTS; i++) {
|
|
41
|
-
this.checkpoints.push(this.pointAt(i / CHECKPOINTS));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
this.obstacles.push(
|
|
45
|
-
{ ...this.offsetPoint(0.11, -170), radius: 32, kind: 'rock' },
|
|
46
|
-
{ ...this.offsetPoint(0.19, 145), radius: 30, kind: 'cactus' },
|
|
47
|
-
{ ...this.offsetPoint(0.27, -60), radius: 58, kind: 'bump' },
|
|
48
|
-
{ ...this.offsetPoint(0.36, 150), radius: 34, kind: 'barrel' },
|
|
49
|
-
{ ...this.offsetPoint(0.48, -160), radius: 36, kind: 'rock' },
|
|
50
|
-
{ ...this.offsetPoint(0.57, 60), radius: 60, kind: 'bump' },
|
|
51
|
-
{ ...this.offsetPoint(0.68, 165), radius: 34, kind: 'cactus' },
|
|
52
|
-
{ ...this.offsetPoint(0.78, -145), radius: 36, kind: 'barrel' },
|
|
53
|
-
{ ...this.offsetPoint(0.88, 75), radius: 54, kind: 'bump' },
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
for (const t of [0.14, 0.31, 0.45, 0.62, 0.82]) {
|
|
57
|
-
this.pickups.push({ ...this.offsetPoint(t, 0), radius: 28, active: true, respawn: 0 });
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
update(delta: number): void {
|
|
62
|
-
for (const pickup of this.pickups) {
|
|
63
|
-
if (!pickup.active) {
|
|
64
|
-
pickup.respawn -= delta;
|
|
65
|
-
if (pickup.respawn <= 0) pickup.active = true;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
startPosition(index: number): Vec2 & { angle: number } {
|
|
71
|
-
const base = this.pointAt(0.985);
|
|
72
|
-
const next = this.pointAt(0.995);
|
|
73
|
-
const angle = Math.atan2(next.y - base.y, next.x - base.x);
|
|
74
|
-
const side = index % 2 === 0 ? -1 : 1;
|
|
75
|
-
return {
|
|
76
|
-
x: base.x - Math.sin(angle) * side * 45 - Math.cos(angle) * index * 42,
|
|
77
|
-
y: base.y + Math.cos(angle) * side * 45 - Math.sin(angle) * index * 42,
|
|
78
|
-
angle,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
sample(x: number, y: number): TrackSample {
|
|
83
|
-
let nearest = this.points[0];
|
|
84
|
-
let nearestIndex = 0;
|
|
85
|
-
let best = Infinity;
|
|
86
|
-
|
|
87
|
-
for (let i = 0; i < this.points.length; i++) {
|
|
88
|
-
const d = Math.hypot(x - this.points[i].x, y - this.points[i].y);
|
|
89
|
-
if (d < best) {
|
|
90
|
-
best = d;
|
|
91
|
-
nearest = this.points[i];
|
|
92
|
-
nearestIndex = i;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const mud = Math.sin((x + 260) * 0.006) * Math.cos((y - 180) * 0.008) > 0.72;
|
|
97
|
-
const terrain: Terrain = best < this.width / 2 ? (mud && best > 46 ? 'mud' : 'track') : best < this.roughWidth / 2 ? 'sand' : 'rough';
|
|
98
|
-
|
|
99
|
-
const traits = {
|
|
100
|
-
track: { grip: 1, speedMultiplier: 1 },
|
|
101
|
-
mud: { grip: 0.62, speedMultiplier: 0.72 },
|
|
102
|
-
sand: { grip: 0.72, speedMultiplier: 0.82 },
|
|
103
|
-
rough: { grip: 0.46, speedMultiplier: 0.56 },
|
|
104
|
-
}[terrain];
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
terrain,
|
|
108
|
-
grip: traits.grip,
|
|
109
|
-
speedMultiplier: traits.speedMultiplier,
|
|
110
|
-
nearest,
|
|
111
|
-
distanceFromCenter: best,
|
|
112
|
-
progress: nearestIndex / this.points.length,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
nextCheckpoint(current: number): Vec2 {
|
|
117
|
-
return this.checkpoints[current % this.checkpoints.length];
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
consumePickup(pos: Vec2): boolean {
|
|
121
|
-
for (const pickup of this.pickups) {
|
|
122
|
-
if (pickup.active && distance(pos, pickup) < pickup.radius + 26) {
|
|
123
|
-
pickup.active = false;
|
|
124
|
-
pickup.respawn = 9;
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
pointAt(progress: number): Vec2 {
|
|
132
|
-
const wrapped = ((progress % 1) + 1) % 1;
|
|
133
|
-
const index = wrapped * this.points.length;
|
|
134
|
-
const a = Math.floor(index) % this.points.length;
|
|
135
|
-
const b = (a + 1) % this.points.length;
|
|
136
|
-
const t = index - Math.floor(index);
|
|
137
|
-
return {
|
|
138
|
-
x: this.points[a].x + (this.points[b].x - this.points[a].x) * t,
|
|
139
|
-
y: this.points[a].y + (this.points[b].y - this.points[a].y) * t,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
private offsetPoint(progress: number, offset: number): Vec2 {
|
|
144
|
-
const p = this.pointAt(progress);
|
|
145
|
-
const n = this.pointAt(progress + 0.006);
|
|
146
|
-
const angle = Math.atan2(n.y - p.y, n.x - p.x);
|
|
147
|
-
return {
|
|
148
|
-
x: p.x - Math.sin(angle) * offset,
|
|
149
|
-
y: p.y + Math.cos(angle) * offset,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function checkpointDistance(progress: number, checkpointIndex: number): number {
|
|
155
|
-
const target = checkpointIndex / CHECKPOINTS;
|
|
156
|
-
const ahead = target >= progress ? target - progress : target + 1 - progress;
|
|
157
|
-
return clamp(ahead, 0, 1);
|
|
158
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { Car } from './Car';
|
|
2
|
-
import { Track } from './Track';
|
|
3
|
-
|
|
4
|
-
export type UiState = 'menu' | 'countdown' | 'racing' | 'paused' | 'finished';
|
|
5
|
-
|
|
6
|
-
export type UiData = {
|
|
7
|
-
state: UiState;
|
|
8
|
-
countdown: number;
|
|
9
|
-
player: Car;
|
|
10
|
-
track: Track;
|
|
11
|
-
elapsed: number;
|
|
12
|
-
message: string;
|
|
13
|
-
winner: string;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export class UI {
|
|
17
|
-
private root: HTMLDivElement;
|
|
18
|
-
|
|
19
|
-
constructor(root: HTMLDivElement) {
|
|
20
|
-
this.root = root;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
render(data: UiData): void {
|
|
24
|
-
if (data.state === 'menu') {
|
|
25
|
-
this.root.className = 'overlay overlay--active';
|
|
26
|
-
this.root.innerHTML = `
|
|
27
|
-
<section class="panel hero">
|
|
28
|
-
<p class="eyebrow">Desert cup</p>
|
|
29
|
-
<h1>Offroad Nitro Racer</h1>
|
|
30
|
-
<p class="lede">Chunky arcade dirt racing with drifting, boosts, bumps, and ruthless AI trucks.</p>
|
|
31
|
-
<div class="controls-grid">
|
|
32
|
-
<span>Drive</span><strong>Arrows / WASD</strong>
|
|
33
|
-
<span>Nitro</span><strong>Space</strong>
|
|
34
|
-
<span>Pause</span><strong>Esc</strong>
|
|
35
|
-
<span>Restart</span><strong>R</strong>
|
|
36
|
-
</div>
|
|
37
|
-
<button class="primary">Press Enter to Race</button>
|
|
38
|
-
</section>
|
|
39
|
-
`;
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (data.state === 'paused') {
|
|
44
|
-
this.root.className = 'overlay overlay--active';
|
|
45
|
-
this.root.innerHTML = `
|
|
46
|
-
<section class="panel compact">
|
|
47
|
-
<p class="eyebrow">Pit stop</p>
|
|
48
|
-
<h2>Paused</h2>
|
|
49
|
-
<p>Press Esc to resume or R to restart.</p>
|
|
50
|
-
</section>
|
|
51
|
-
`;
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (data.state === 'finished') {
|
|
56
|
-
const won = data.winner === data.player.name;
|
|
57
|
-
this.root.className = 'overlay overlay--active';
|
|
58
|
-
this.root.innerHTML = `
|
|
59
|
-
<section class="panel hero">
|
|
60
|
-
<p class="eyebrow">Race complete</p>
|
|
61
|
-
<h1>${won ? 'You Win!' : 'You Lose'}</h1>
|
|
62
|
-
<p class="lede">${won ? 'Nitro glory across the desert.' : `${data.winner} reached the flag first.`}</p>
|
|
63
|
-
<div class="result-row"><span>Final position</span><strong>${ordinal(data.player.rank)}</strong></div>
|
|
64
|
-
<div class="result-row"><span>Time</span><strong>${formatTime(data.elapsed)}</strong></div>
|
|
65
|
-
<button class="primary">Press R to Restart</button>
|
|
66
|
-
</section>
|
|
67
|
-
`;
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
this.root.className = 'overlay';
|
|
72
|
-
this.root.innerHTML = `
|
|
73
|
-
<div class="hud hud--top">
|
|
74
|
-
<div class="hud-card"><span>Lap</span><strong>${Math.min(data.player.lap, data.track.lapsToWin)} / ${data.track.lapsToWin}</strong></div>
|
|
75
|
-
<div class="hud-card"><span>Position</span><strong>${ordinal(data.player.rank)}</strong></div>
|
|
76
|
-
<div class="hud-card"><span>Speed</span><strong>${Math.round(data.player.speed * 0.42)} mph</strong></div>
|
|
77
|
-
<div class="hud-card"><span>Timer</span><strong>${formatTime(data.elapsed)}</strong></div>
|
|
78
|
-
</div>
|
|
79
|
-
<div class="nitro-stack">${Array.from({ length: data.player.maxNitro }, (_, i) => `<i class="${i < data.player.nitroCharges ? 'full' : ''}"></i>`).join('')}</div>
|
|
80
|
-
${data.state === 'countdown' ? `<div class="countdown">${data.countdown > 0.25 ? Math.ceil(data.countdown) : 'GO!'}</div>` : ''}
|
|
81
|
-
${data.message ? `<div class="toast">${data.message}</div>` : ''}
|
|
82
|
-
`;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function formatTime(seconds: number): string {
|
|
87
|
-
const minutes = Math.floor(seconds / 60);
|
|
88
|
-
const secs = Math.floor(seconds % 60).toString().padStart(2, '0');
|
|
89
|
-
const tenths = Math.floor((seconds % 1) * 10);
|
|
90
|
-
return `${minutes}:${secs}.${tenths}`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function ordinal(value: number): string {
|
|
94
|
-
const suffix = value === 1 ? 'st' : value === 2 ? 'nd' : value === 3 ? 'rd' : 'th';
|
|
95
|
-
return `${value}${suffix}`;
|
|
96
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
export type Vec2 = { x: number; y: number };
|
|
2
|
-
|
|
3
|
-
export const TAU = Math.PI * 2;
|
|
4
|
-
|
|
5
|
-
export function clamp(value: number, min: number, max: number): number {
|
|
6
|
-
return Math.max(min, Math.min(max, value));
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function lerp(a: number, b: number, t: number): number {
|
|
10
|
-
return a + (b - a) * t;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function length(x: number, y: number): number {
|
|
14
|
-
return Math.hypot(x, y);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function distance(a: Vec2, b: Vec2): number {
|
|
18
|
-
return Math.hypot(a.x - b.x, a.y - b.y);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function normalizeAngle(angle: number): number {
|
|
22
|
-
while (angle > Math.PI) angle -= TAU;
|
|
23
|
-
while (angle < -Math.PI) angle += TAU;
|
|
24
|
-
return angle;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function angleTo(from: Vec2, to: Vec2): number {
|
|
28
|
-
return Math.atan2(to.y - from.y, to.x - from.x);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function dot(ax: number, ay: number, bx: number, by: number): number {
|
|
32
|
-
return ax * bx + ay * by;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function rand(min: number, max: number): number {
|
|
36
|
-
return min + Math.random() * (max - min);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function roundedRect(ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number): void {
|
|
40
|
-
ctx.beginPath();
|
|
41
|
-
ctx.roundRect(x, y, w, h, r);
|
|
42
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import './style.css';
|
|
2
|
-
import { Game } from './game/Game';
|
|
3
|
-
|
|
4
|
-
const app = document.querySelector<HTMLDivElement>('#app');
|
|
5
|
-
|
|
6
|
-
if (!app) {
|
|
7
|
-
throw new Error('Missing #app root');
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
app.innerHTML = `
|
|
11
|
-
<div class="shell">
|
|
12
|
-
<canvas id="game" aria-label="Offroad Nitro Racer game canvas"></canvas>
|
|
13
|
-
<div id="overlay" class="overlay"></div>
|
|
14
|
-
</div>
|
|
15
|
-
`;
|
|
16
|
-
|
|
17
|
-
const canvas = document.querySelector<HTMLCanvasElement>('#game');
|
|
18
|
-
const overlay = document.querySelector<HTMLDivElement>('#overlay');
|
|
19
|
-
|
|
20
|
-
if (!canvas || !overlay) {
|
|
21
|
-
throw new Error('Missing game canvas or overlay');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
new Game(canvas, overlay).start();
|