groove-dev 0.27.113 → 0.27.115

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.
Files changed (66) hide show
  1. package/CENTRAL_COMMAND_REBUILD.md +689 -0
  2. package/EMBEDDING_DIAGNOSTIC.md +197 -0
  3. package/TRAINING_DATA_v4.md +3 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/cli/src/commands/team.js +43 -1
  6. package/node_modules/@groove-dev/daemon/package.json +1 -1
  7. package/node_modules/@groove-dev/daemon/src/api.js +24 -0
  8. package/node_modules/@groove-dev/daemon/src/filewatcher.js +45 -0
  9. package/node_modules/@groove-dev/daemon/src/index.js +14 -2
  10. package/node_modules/@groove-dev/daemon/src/teams.js +100 -6
  11. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +75 -43
  12. package/node_modules/@groove-dev/gui/dist/assets/{index-BYh6iHqL.js → index-BKCiOUDb.js} +593 -593
  13. package/node_modules/@groove-dev/gui/dist/assets/index-D4Q72afD.css +1 -0
  14. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  15. package/node_modules/@groove-dev/gui/package.json +1 -1
  16. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +0 -22
  17. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +43 -45
  18. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +2 -1
  19. package/node_modules/@groove-dev/gui/src/stores/groove.js +42 -8
  20. package/node_modules/@groove-dev/gui/src/views/agents.jsx +31 -3
  21. package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -20
  22. package/node_modules/@groove-dev/gui/src/views/teams.jsx +106 -3
  23. package/package.json +1 -1
  24. package/packages/cli/package.json +1 -1
  25. package/packages/cli/src/commands/team.js +43 -1
  26. package/packages/daemon/package.json +1 -1
  27. package/packages/daemon/src/api.js +24 -0
  28. package/packages/daemon/src/filewatcher.js +45 -0
  29. package/packages/daemon/src/index.js +14 -2
  30. package/packages/daemon/src/teams.js +100 -6
  31. package/packages/daemon/src/tunnel-manager.js +75 -43
  32. package/packages/gui/dist/assets/{index-BYh6iHqL.js → index-BKCiOUDb.js} +593 -593
  33. package/packages/gui/dist/assets/index-D4Q72afD.css +1 -0
  34. package/packages/gui/dist/index.html +2 -2
  35. package/packages/gui/package.json +1 -1
  36. package/packages/gui/src/components/agents/workspace-mode.jsx +0 -22
  37. package/packages/gui/src/components/layout/status-bar.jsx +43 -45
  38. package/packages/gui/src/components/settings/quick-connect.jsx +2 -1
  39. package/packages/gui/src/stores/groove.js +42 -8
  40. package/packages/gui/src/views/agents.jsx +31 -3
  41. package/packages/gui/src/views/editor.jsx +1 -20
  42. package/packages/gui/src/views/teams.jsx +106 -3
  43. package/TRAINING_DATA_v3.md +0 -11
  44. package/codex-test/offroad-nitro-racer/dist/assets/index-CuvdKK6U.js +0 -44
  45. package/codex-test/offroad-nitro-racer/dist/assets/index-DvHn2Thu.css +0 -1
  46. package/codex-test/offroad-nitro-racer/dist/index.html +0 -23
  47. package/codex-test/offroad-nitro-racer/index.html +0 -21
  48. package/codex-test/offroad-nitro-racer/package-lock.json +0 -841
  49. package/codex-test/offroad-nitro-racer/package.json +0 -15
  50. package/codex-test/offroad-nitro-racer/src/game/AI.ts +0 -28
  51. package/codex-test/offroad-nitro-racer/src/game/Audio.ts +0 -63
  52. package/codex-test/offroad-nitro-racer/src/game/Car.ts +0 -247
  53. package/codex-test/offroad-nitro-racer/src/game/Effects.ts +0 -62
  54. package/codex-test/offroad-nitro-racer/src/game/Game.ts +0 -229
  55. package/codex-test/offroad-nitro-racer/src/game/Input.ts +0 -45
  56. package/codex-test/offroad-nitro-racer/src/game/Renderer.ts +0 -224
  57. package/codex-test/offroad-nitro-racer/src/game/Track.ts +0 -158
  58. package/codex-test/offroad-nitro-racer/src/game/UI.ts +0 -96
  59. package/codex-test/offroad-nitro-racer/src/game/math.ts +0 -42
  60. package/codex-test/offroad-nitro-racer/src/main.ts +0 -24
  61. package/codex-test/offroad-nitro-racer/src/style.css +0 -291
  62. package/codex-test/offroad-nitro-racer/src/vite-env.d.ts +0 -1
  63. package/codex-test/offroad-nitro-racer/tsconfig.json +0 -18
  64. package/codex-test/offroad-nitro-racer/vite.config.ts +0 -7
  65. package/node_modules/@groove-dev/gui/dist/assets/index-DAlSbVyK.css +0 -1
  66. package/packages/gui/dist/assets/index-DAlSbVyK.css +0 -1
@@ -1,15 +0,0 @@
1
- {
2
- "name": "offroad-nitro-racer",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vite",
8
- "build": "tsc && vite build",
9
- "preview": "vite preview"
10
- },
11
- "dependencies": {
12
- "typescript": "latest",
13
- "vite": "latest"
14
- }
15
- }
@@ -1,28 +0,0 @@
1
- import { Car, DriverInput } from './Car';
2
- import { Track } from './Track';
3
- import { angleTo, clamp, normalizeAngle } from './math';
4
-
5
- export class AIController {
6
- private aggression: number;
7
- private lookahead: number;
8
-
9
- constructor(index: number) {
10
- this.aggression = 0.84 + index * 0.06;
11
- this.lookahead = 1 + (index % 2);
12
- }
13
-
14
- update(car: Car, track: Track, playerRank: number, difficulty: number): DriverInput {
15
- const targetIndex = (car.nextCheckpoint + this.lookahead) % track.checkpoints.length;
16
- const target = track.nextCheckpoint(targetIndex);
17
- const desired = angleTo(car, target);
18
- const delta = normalizeAngle(desired - car.angle);
19
- const steer = clamp(delta * 2.2, -1, 1);
20
- const caution = Math.abs(delta) > 0.8 ? 0.45 : Math.abs(delta) > 0.45 ? 0.7 : 1;
21
- const catchup = playerRank < car.rank ? 1.08 : 0.95;
22
- const throttle = clamp(this.aggression * caution * catchup + difficulty * 0.04, 0.35, 1);
23
- const brake = Math.abs(delta) > 1.15 && car.speed > 115 ? 0.65 : 0;
24
- const nitro = car.nitroCharges > 0 && Math.abs(delta) < 0.22 && car.speed > 135 && Math.random() < 0.015 + difficulty * 0.004;
25
-
26
- return { throttle, brake, steer, nitro };
27
- }
28
- }
@@ -1,63 +0,0 @@
1
- import { clamp } from './math';
2
-
3
- export class AudioSystem {
4
- private ctx: AudioContext | null = null;
5
- private master: GainNode | null = null;
6
- private engineOsc: OscillatorNode | null = null;
7
- private engineGain: GainNode | null = null;
8
-
9
- ensure(): void {
10
- if (this.ctx) return;
11
-
12
- const Ctx = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
13
- this.ctx = new Ctx();
14
- this.master = this.ctx.createGain();
15
- this.master.gain.value = 0.18;
16
- this.master.connect(this.ctx.destination);
17
-
18
- this.engineOsc = this.ctx.createOscillator();
19
- this.engineGain = this.ctx.createGain();
20
- this.engineOsc.type = 'sawtooth';
21
- this.engineOsc.frequency.value = 70;
22
- this.engineGain.gain.value = 0;
23
- this.engineOsc.connect(this.engineGain);
24
- this.engineGain.connect(this.master);
25
- this.engineOsc.start();
26
- }
27
-
28
- updateEngine(speed: number, throttle: number, boosting: boolean): void {
29
- if (!this.ctx || !this.engineOsc || !this.engineGain) return;
30
- const now = this.ctx.currentTime;
31
- this.engineOsc.frequency.setTargetAtTime(65 + clamp(speed, 0, 240) * 1.2 + (boosting ? 70 : 0), now, 0.04);
32
- this.engineGain.gain.setTargetAtTime(0.025 + Math.abs(throttle) * 0.05 + (boosting ? 0.035 : 0), now, 0.05);
33
- }
34
-
35
- beep(kind: 'count' | 'go' | 'pickup' | 'boost' | 'hit' | 'finish'): void {
36
- this.ensure();
37
- if (!this.ctx || !this.master) return;
38
-
39
- const now = this.ctx.currentTime;
40
- const osc = this.ctx.createOscillator();
41
- const gain = this.ctx.createGain();
42
- const settings = {
43
- count: [440, 0.12, 'square'],
44
- go: [780, 0.35, 'triangle'],
45
- pickup: [920, 0.18, 'sine'],
46
- boost: [120, 0.42, 'sawtooth'],
47
- hit: [80, 0.18, 'square'],
48
- finish: [660, 0.7, 'triangle'],
49
- } as const;
50
- const [frequency, duration, type] = settings[kind];
51
-
52
- osc.type = type;
53
- osc.frequency.setValueAtTime(frequency, now);
54
- if (kind === 'boost') osc.frequency.exponentialRampToValueAtTime(240, now + duration);
55
- if (kind === 'hit') osc.frequency.exponentialRampToValueAtTime(45, now + duration);
56
- gain.gain.setValueAtTime(kind === 'hit' ? 0.13 : 0.18, now);
57
- gain.gain.exponentialRampToValueAtTime(0.001, now + duration);
58
- osc.connect(gain);
59
- gain.connect(this.master);
60
- osc.start(now);
61
- osc.stop(now + duration + 0.02);
62
- }
63
- }
@@ -1,247 +0,0 @@
1
- import { Effects } from './Effects';
2
- import { Track } from './Track';
3
- import { Vec2, clamp, dot, length, normalizeAngle } from './math';
4
-
5
- export type DriverInput = {
6
- throttle: number;
7
- brake: number;
8
- steer: number;
9
- nitro: boolean;
10
- };
11
-
12
- export type CarConfig = {
13
- name: string;
14
- color: string;
15
- accent: string;
16
- isPlayer?: boolean;
17
- };
18
-
19
- export class Car {
20
- name: string;
21
- color: string;
22
- accent: string;
23
- isPlayer: boolean;
24
- x: number;
25
- y: number;
26
- vx = 0;
27
- vy = 0;
28
- angle: number;
29
- radius = 24;
30
- width = 34;
31
- length = 54;
32
- nitroCharges = 2;
33
- maxNitro = 3;
34
- nitroTimer = 0;
35
- nextCheckpoint = 0;
36
- lap = 1;
37
- finished = false;
38
- finishTime = 0;
39
- airborne = 0;
40
- verticalVelocity = 0;
41
- driftAmount = 0;
42
- collectedPickup = false;
43
- lastProgress = 0;
44
- totalProgress = 0;
45
- rank = 1;
46
- wrongWayTimer = 0;
47
-
48
- constructor(config: CarConfig, start: Vec2 & { angle: number }) {
49
- this.name = config.name;
50
- this.color = config.color;
51
- this.accent = config.accent;
52
- this.isPlayer = Boolean(config.isPlayer);
53
- this.x = start.x;
54
- this.y = start.y;
55
- this.angle = start.angle;
56
- }
57
-
58
- get speed(): number {
59
- return Math.hypot(this.vx, this.vy);
60
- }
61
-
62
- reset(start: Vec2 & { angle: number }): void {
63
- this.x = start.x;
64
- this.y = start.y;
65
- this.vx = 0;
66
- this.vy = 0;
67
- this.angle = start.angle;
68
- this.nitroCharges = this.isPlayer ? 2 : 1;
69
- this.nitroTimer = 0;
70
- this.nextCheckpoint = 0;
71
- this.lap = 1;
72
- this.finished = false;
73
- this.finishTime = 0;
74
- this.airborne = 0;
75
- this.verticalVelocity = 0;
76
- this.totalProgress = 0;
77
- this.lastProgress = 0;
78
- this.rank = 1;
79
- }
80
-
81
- update(delta: number, input: DriverInput, track: Track, effects: Effects, raceTime: number, difficulty = 1): void {
82
- if (this.finished) {
83
- this.vx *= 1 - delta * 2;
84
- this.vy *= 1 - delta * 2;
85
- return;
86
- }
87
-
88
- this.collectedPickup = false;
89
-
90
- const terrain = track.sample(this.x, this.y);
91
- const forwardX = Math.cos(this.angle);
92
- const forwardY = Math.sin(this.angle);
93
- const rightX = -forwardY;
94
- const rightY = forwardX;
95
- const forwardSpeed = dot(this.vx, this.vy, forwardX, forwardY);
96
- const sideSpeed = dot(this.vx, this.vy, rightX, rightY);
97
- const movingFactor = clamp(Math.abs(forwardSpeed) / 90, 0.25, 1);
98
- const steerPower = (2.15 + difficulty * 0.08) * terrain.grip * movingFactor;
99
-
100
- this.angle = normalizeAngle(this.angle + input.steer * steerPower * delta * (forwardSpeed >= -15 ? 1 : -1));
101
-
102
- let acceleration = input.throttle * (230 + difficulty * 12) - input.brake * 320;
103
- const boosting = input.nitro && this.nitroCharges > 0 && this.nitroTimer <= 0 && input.throttle > 0;
104
- if (boosting) {
105
- this.nitroCharges -= 1;
106
- this.nitroTimer = 1.15;
107
- effects.add('boost', this.x, this.y, this.angle, 12);
108
- effects.impact(this.isPlayer ? 5 : 2);
109
- }
110
-
111
- if (this.nitroTimer > 0) {
112
- this.nitroTimer -= delta;
113
- acceleration += 360;
114
- if (Math.random() < 0.8) effects.add('boost', this.x - forwardX * 26, this.y - forwardY * 26, this.angle, 1);
115
- }
116
-
117
- const maxSpeed = (this.nitroTimer > 0 ? 330 : 235 + difficulty * 8) * terrain.speedMultiplier;
118
- this.vx += forwardX * acceleration * delta;
119
- this.vy += forwardY * acceleration * delta;
120
-
121
- const newForwardSpeed = dot(this.vx, this.vy, forwardX, forwardY);
122
- const limitedForward = clamp(newForwardSpeed, -95, maxSpeed);
123
- const lateralGrip = this.airborne > 0 ? 0.02 : clamp(terrain.grip * (input.brake > 0 ? 0.72 : 1), 0.08, 1);
124
- const limitedSide = sideSpeed * (1 - clamp(delta * (6.5 * lateralGrip), 0, 0.92));
125
-
126
- this.vx = forwardX * limitedForward + rightX * limitedSide;
127
- this.vy = forwardY * limitedForward + rightY * limitedSide;
128
-
129
- const drag = 1 - delta * (terrain.terrain === 'rough' ? 1.4 : 0.65);
130
- this.vx *= drag;
131
- this.vy *= drag;
132
-
133
- this.x += this.vx * delta;
134
- this.y += this.vy * delta;
135
-
136
- this.driftAmount = Math.abs(limitedSide) / 90;
137
- if (this.speed > 45 && terrain.terrain !== 'track' && Math.random() < 0.7) effects.add('dust', this.x - forwardX * 24, this.y - forwardY * 24, this.angle, 1);
138
- if (this.driftAmount > 0.42 && this.airborne <= 0 && Math.random() < 0.6) effects.add('skid', this.x - forwardX * 18, this.y - forwardY * 18, this.angle, 1);
139
-
140
- this.updateAir(delta, track, effects);
141
- this.updateCheckpoints(track, raceTime);
142
- }
143
-
144
- collideWith(other: Car, effects: Effects): void {
145
- const dx = this.x - other.x;
146
- const dy = this.y - other.y;
147
- const dist = length(dx, dy);
148
- const minDist = this.radius + other.radius;
149
- if (dist <= 0 || dist >= minDist) return;
150
-
151
- const nx = dx / dist;
152
- const ny = dy / dist;
153
- const overlap = minDist - dist;
154
- this.x += nx * overlap * 0.5;
155
- this.y += ny * overlap * 0.5;
156
- other.x -= nx * overlap * 0.5;
157
- other.y -= ny * overlap * 0.5;
158
-
159
- const impulse = dot(this.vx - other.vx, this.vy - other.vy, nx, ny) * 0.55;
160
- this.vx -= nx * impulse;
161
- this.vy -= ny * impulse;
162
- other.vx += nx * impulse;
163
- other.vy += ny * impulse;
164
- effects.add('spark', (this.x + other.x) / 2, (this.y + other.y) / 2, this.angle, 5);
165
- effects.impact(this.isPlayer || other.isPlayer ? 6 : 2);
166
- }
167
-
168
- collideObstacles(track: Track, effects: Effects): 'hit' | 'bump' | null {
169
- for (const obstacle of track.obstacles) {
170
- const dx = this.x - obstacle.x;
171
- const dy = this.y - obstacle.y;
172
- const dist = length(dx, dy);
173
- if (dist >= this.radius + obstacle.radius) continue;
174
-
175
- if (obstacle.kind === 'bump') {
176
- if (this.airborne <= 0 && this.speed > 65) {
177
- this.verticalVelocity = 180 + this.speed * 0.45;
178
- effects.add('dust', this.x, this.y, this.angle, 10);
179
- effects.impact(this.isPlayer ? 5 : 1);
180
- return 'bump';
181
- }
182
- continue;
183
- }
184
-
185
- const nx = dx / Math.max(dist, 0.01);
186
- const ny = dy / Math.max(dist, 0.01);
187
- const overlap = this.radius + obstacle.radius - dist;
188
- this.x += nx * overlap;
189
- this.y += ny * overlap;
190
- const impactSpeed = dot(this.vx, this.vy, nx, ny);
191
- this.vx -= nx * impactSpeed * 1.55;
192
- this.vy -= ny * impactSpeed * 1.55;
193
- this.vx *= 0.72;
194
- this.vy *= 0.72;
195
- effects.add('spark', this.x - nx * this.radius, this.y - ny * this.radius, this.angle, 8);
196
- effects.impact(this.isPlayer ? 9 : 2);
197
- return 'hit';
198
- }
199
- return null;
200
- }
201
-
202
- private updateAir(delta: number, track: Track, effects: Effects): void {
203
- if (this.airborne > 0 || this.verticalVelocity > 0) {
204
- this.airborne += this.verticalVelocity * delta;
205
- this.verticalVelocity -= 520 * delta;
206
- if (this.airborne <= 0) {
207
- this.airborne = 0;
208
- this.verticalVelocity = 0;
209
- if (this.speed > 75) {
210
- effects.add('dust', this.x, this.y, this.angle, 14);
211
- effects.impact(this.isPlayer ? 7 : 2);
212
- }
213
- }
214
- }
215
-
216
- if (track.consumePickup({ x: this.x, y: this.y })) {
217
- this.nitroCharges = Math.min(this.maxNitro, this.nitroCharges + 1);
218
- this.collectedPickup = true;
219
- effects.add('boost', this.x, this.y, this.angle, 10);
220
- }
221
- }
222
-
223
- private updateCheckpoints(track: Track, raceTime: number): void {
224
- const sample = track.sample(this.x, this.y);
225
- const progress = sample.progress;
226
- const checkpoint = track.nextCheckpoint(this.nextCheckpoint);
227
- const closeEnough = Math.hypot(this.x - checkpoint.x, this.y - checkpoint.y) < 155;
228
-
229
- if (closeEnough) {
230
- this.nextCheckpoint += 1;
231
- if (this.nextCheckpoint >= track.checkpoints.length) {
232
- this.nextCheckpoint = 0;
233
- this.lap += 1;
234
- if (this.lap > track.lapsToWin) {
235
- this.finished = true;
236
- this.finishTime = raceTime;
237
- this.lap = track.lapsToWin;
238
- }
239
- }
240
- }
241
-
242
- const wrappedProgress = progress < 0.15 && this.lastProgress > 0.85 ? progress + 1 : progress;
243
- const lapBase = Math.max(0, this.lap - 1);
244
- this.totalProgress = lapBase + wrappedProgress;
245
- this.lastProgress = progress;
246
- }
247
- }
@@ -1,62 +0,0 @@
1
- import { Vec2, rand } from './math';
2
-
3
- export type ParticleKind = 'dust' | 'skid' | 'spark' | 'boost';
4
-
5
- type Particle = Vec2 & {
6
- vx: number;
7
- vy: number;
8
- life: number;
9
- maxLife: number;
10
- size: number;
11
- kind: ParticleKind;
12
- };
13
-
14
- export class Effects {
15
- particles: Particle[] = [];
16
- shake = 0;
17
-
18
- add(kind: ParticleKind, x: number, y: number, angle = 0, amount = 1): void {
19
- for (let i = 0; i < amount; i++) {
20
- const speed = kind === 'boost' ? rand(80, 180) : rand(10, 95);
21
- const spread = angle + Math.PI + rand(-0.75, 0.75);
22
- this.particles.push({
23
- x: x + rand(-8, 8),
24
- y: y + rand(-8, 8),
25
- vx: Math.cos(spread) * speed + rand(-20, 20),
26
- vy: Math.sin(spread) * speed + rand(-20, 20),
27
- life: kind === 'skid' ? 1.4 : rand(0.25, 0.75),
28
- maxLife: kind === 'skid' ? 1.4 : 0.75,
29
- size: kind === 'spark' ? rand(2, 5) : rand(6, 18),
30
- kind,
31
- });
32
- }
33
- }
34
-
35
- impact(amount: number): void {
36
- this.shake = Math.max(this.shake, amount);
37
- }
38
-
39
- update(delta: number): void {
40
- this.shake = Math.max(0, this.shake - delta * 18);
41
- for (const p of this.particles) {
42
- p.x += p.vx * delta;
43
- p.y += p.vy * delta;
44
- p.vx *= 1 - delta * 2.2;
45
- p.vy *= 1 - delta * 2.2;
46
- p.life -= delta;
47
- }
48
- this.particles = this.particles.filter((p) => p.life > 0);
49
- }
50
-
51
- render(ctx: CanvasRenderingContext2D): void {
52
- for (const p of this.particles) {
53
- const alpha = Math.max(0, p.life / p.maxLife);
54
- ctx.globalAlpha = alpha * (p.kind === 'skid' ? 0.55 : 0.8);
55
- ctx.fillStyle = p.kind === 'spark' ? '#ffd166' : p.kind === 'boost' ? '#6ff7ff' : p.kind === 'skid' ? '#282018' : '#c89b66';
56
- ctx.beginPath();
57
- ctx.ellipse(p.x, p.y, p.size * (1.2 - alpha * 0.3), p.size * 0.45, 0, 0, Math.PI * 2);
58
- ctx.fill();
59
- }
60
- ctx.globalAlpha = 1;
61
- }
62
- }
@@ -1,229 +0,0 @@
1
- import { AIController } from './AI';
2
- import { AudioSystem } from './Audio';
3
- import { Car, DriverInput } from './Car';
4
- import { Effects } from './Effects';
5
- import { Input } from './Input';
6
- import { Camera, Renderer } from './Renderer';
7
- import { Track } from './Track';
8
- import { UI, UiState } from './UI';
9
- import { clamp, lerp, rand } from './math';
10
-
11
- export class Game {
12
- private renderer: Renderer;
13
- private input = new Input();
14
- private audio = new AudioSystem();
15
- private ui: UI;
16
- private track = new Track();
17
- private effects = new Effects();
18
- private cars: Car[] = [];
19
- private ai: AIController[] = [];
20
- private player!: Car;
21
- private camera: Camera = { x: 0, y: 0, zoom: 0.72, shakeX: 0, shakeY: 0 };
22
- private state: UiState = 'menu';
23
- private lastTime = 0;
24
- private raceTime = 0;
25
- private countdown = 3.4;
26
- private message = '';
27
- private messageTimer = 0;
28
- private winner = '';
29
- private countdownBeep = 4;
30
- private pausedFrom: UiState = 'racing';
31
-
32
- constructor(canvas: HTMLCanvasElement, overlay: HTMLDivElement) {
33
- this.renderer = new Renderer(canvas);
34
- this.ui = new UI(overlay);
35
- this.resetRace();
36
- }
37
-
38
- start(): void {
39
- this.ui.render(this.uiData());
40
- requestAnimationFrame((time) => this.loop(time));
41
- }
42
-
43
- private loop(time: number): void {
44
- const delta = Math.min((time - this.lastTime) / 1000 || 0, 0.033);
45
- this.lastTime = time;
46
-
47
- this.handleGlobalInput();
48
- this.update(delta);
49
- this.render();
50
- this.input.endFrame();
51
-
52
- requestAnimationFrame((nextTime) => this.loop(nextTime));
53
- }
54
-
55
- private handleGlobalInput(): void {
56
- if (this.input.consume('enter') && this.state === 'menu') {
57
- this.audio.ensure();
58
- this.beginCountdown();
59
- }
60
-
61
- if (this.input.consume('escape')) {
62
- if (this.state === 'racing' || this.state === 'countdown') {
63
- this.pausedFrom = this.state;
64
- this.state = 'paused';
65
- } else if (this.state === 'paused') this.state = this.pausedFrom;
66
- }
67
-
68
- if (this.input.consume('r')) {
69
- this.resetRace();
70
- this.beginCountdown();
71
- }
72
- }
73
-
74
- private beginCountdown(): void {
75
- this.state = 'countdown';
76
- this.countdown = 3.4;
77
- this.countdownBeep = 4;
78
- this.raceTime = 0;
79
- this.audio.beep('count');
80
- }
81
-
82
- private update(delta: number): void {
83
- if (this.state === 'menu' || this.state === 'paused' || this.state === 'finished') {
84
- this.updateCamera(delta);
85
- return;
86
- }
87
-
88
- this.track.update(delta);
89
- this.effects.update(delta);
90
-
91
- if (this.state === 'countdown') {
92
- this.countdown -= delta;
93
- const nextBeep = Math.ceil(this.countdown);
94
- if (nextBeep > 0 && nextBeep < this.countdownBeep) {
95
- this.countdownBeep = nextBeep;
96
- this.audio.beep('count');
97
- }
98
- if (this.countdown <= 0) {
99
- this.state = 'racing';
100
- this.audio.beep('go');
101
- }
102
- this.updateCamera(delta);
103
- return;
104
- }
105
-
106
- this.raceTime += delta;
107
- const difficulty = 1 + (this.player.lap - 1) * 0.12 + this.raceTime / 190;
108
- const playerInput = this.readPlayerInput();
109
- const wasBoosting = this.player.nitroTimer > 0;
110
- this.player.update(delta, playerInput, this.track, this.effects, this.raceTime, difficulty);
111
- if (!wasBoosting && this.player.nitroTimer > 0) this.audio.beep('boost');
112
-
113
- for (let i = 1; i < this.cars.length; i++) {
114
- const car = this.cars[i];
115
- car.update(delta, this.ai[i - 1].update(car, this.track, this.player.rank, difficulty), this.track, this.effects, this.raceTime, difficulty);
116
- }
117
-
118
- this.resolveCollisions();
119
- this.updateRanks();
120
- this.updateMessages(delta);
121
- this.updateFinishState();
122
- this.updateCamera(delta);
123
- this.audio.updateEngine(this.player.speed, playerInput.throttle - playerInput.brake, this.player.nitroTimer > 0);
124
- }
125
-
126
- private readPlayerInput(): DriverInput {
127
- return {
128
- throttle: this.input.isDown('arrowup', 'w') ? 1 : 0,
129
- brake: this.input.isDown('arrowdown', 's') ? 1 : 0,
130
- steer: (this.input.isDown('arrowright', 'd') ? 1 : 0) - (this.input.isDown('arrowleft', 'a') ? 1 : 0),
131
- nitro: this.input.isDown('space'),
132
- };
133
- }
134
-
135
- private resolveCollisions(): void {
136
- for (const car of this.cars) {
137
- const result = car.collideObstacles(this.track, this.effects);
138
- if (car === this.player && result === 'hit') {
139
- this.audio.beep('hit');
140
- this.flashMessage('Ouch! Watch the rocks.');
141
- }
142
- if (car === this.player && result === 'bump') this.flashMessage('Big air!');
143
-
144
- if (car === this.player && car.collectedPickup) {
145
- this.audio.beep('pickup');
146
- this.flashMessage('Nitro recharged!');
147
- }
148
- }
149
-
150
- for (let i = 0; i < this.cars.length; i++) {
151
- for (let j = i + 1; j < this.cars.length; j++) this.cars[i].collideWith(this.cars[j], this.effects);
152
- }
153
- }
154
-
155
- private updateRanks(): void {
156
- const sorted = [...this.cars].sort((a, b) => b.totalProgress - a.totalProgress || b.speed - a.speed);
157
- sorted.forEach((car, index) => {
158
- car.rank = index + 1;
159
- });
160
- }
161
-
162
- private updateMessages(delta: number): void {
163
- if (this.messageTimer > 0) {
164
- this.messageTimer -= delta;
165
- if (this.messageTimer <= 0) this.message = '';
166
- }
167
- }
168
-
169
- private updateFinishState(): void {
170
- const finished = this.cars.filter((car) => car.finished).sort((a, b) => a.finishTime - b.finishTime);
171
- if (!finished.length) return;
172
- this.winner = finished[0].name;
173
- if (this.player.finished || finished.length >= 2) {
174
- this.state = 'finished';
175
- this.audio.beep('finish');
176
- }
177
- }
178
-
179
- private updateCamera(delta: number): void {
180
- const lookX = this.player.x + this.player.vx * 0.55;
181
- const lookY = this.player.y + this.player.vy * 0.55;
182
- this.camera.x = lerp(this.camera.x, lookX, 1 - Math.pow(0.001, delta));
183
- this.camera.y = lerp(this.camera.y, lookY, 1 - Math.pow(0.001, delta));
184
- this.camera.zoom = lerp(this.camera.zoom, clamp(0.78 - this.player.speed / 950, 0.56, 0.78), 0.04);
185
- this.camera.shakeX = this.effects.shake ? rand(-this.effects.shake, this.effects.shake) : 0;
186
- this.camera.shakeY = this.effects.shake ? rand(-this.effects.shake, this.effects.shake) : 0;
187
- }
188
-
189
- private render(): void {
190
- this.renderer.render(this.track, this.cars, this.player, this.effects, this.camera);
191
- this.ui.render(this.uiData());
192
- }
193
-
194
- private uiData() {
195
- return {
196
- state: this.state,
197
- countdown: this.countdown,
198
- player: this.player,
199
- track: this.track,
200
- elapsed: this.raceTime,
201
- message: this.message,
202
- winner: this.winner,
203
- };
204
- }
205
-
206
- private resetRace(): void {
207
- this.track = new Track();
208
- this.effects = new Effects();
209
- this.raceTime = 0;
210
- this.winner = '';
211
- this.message = '';
212
- this.cars = [
213
- new Car({ name: 'You', color: '#f43f5e', accent: '#fecdd3', isPlayer: true }, this.track.startPosition(0)),
214
- new Car({ name: 'Baja Bob', color: '#f59e0b', accent: '#fde68a' }, this.track.startPosition(1)),
215
- new Car({ name: 'Dusty Diaz', color: '#22c55e', accent: '#bbf7d0' }, this.track.startPosition(2)),
216
- new Car({ name: 'Nitro Nell', color: '#3b82f6', accent: '#bfdbfe' }, this.track.startPosition(3)),
217
- ];
218
- this.player = this.cars[0];
219
- this.ai = [new AIController(0), new AIController(1), new AIController(2)];
220
- this.camera.x = this.player.x;
221
- this.camera.y = this.player.y;
222
- this.state = this.state === 'menu' ? 'menu' : 'countdown';
223
- }
224
-
225
- private flashMessage(message: string): void {
226
- this.message = message;
227
- this.messageTimer = 1.6;
228
- }
229
- }
@@ -1,45 +0,0 @@
1
- export class Input {
2
- private keys = new Set<string>();
3
- private pressed = new Set<string>();
4
-
5
- constructor() {
6
- window.addEventListener('keydown', (event) => {
7
- const key = this.normalize(event.key);
8
- if (!this.keys.has(key)) this.pressed.add(key);
9
- this.keys.add(key);
10
-
11
- if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' ', 'Space'].includes(event.key)) {
12
- event.preventDefault();
13
- }
14
- });
15
-
16
- window.addEventListener('keyup', (event) => {
17
- this.keys.delete(this.normalize(event.key));
18
- });
19
-
20
- window.addEventListener('blur', () => {
21
- this.keys.clear();
22
- this.pressed.clear();
23
- });
24
- }
25
-
26
- isDown(...keys: string[]): boolean {
27
- return keys.some((key) => this.keys.has(this.normalize(key)));
28
- }
29
-
30
- consume(key: string): boolean {
31
- const normalized = this.normalize(key);
32
- if (!this.pressed.has(normalized)) return false;
33
- this.pressed.delete(normalized);
34
- return true;
35
- }
36
-
37
- endFrame(): void {
38
- this.pressed.clear();
39
- }
40
-
41
- private normalize(key: string): string {
42
- if (key === ' ') return 'space';
43
- return key.toLowerCase();
44
- }
45
- }