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.
- package/CENTRAL_COMMAND_REBUILD.md +689 -0
- package/EMBEDDING_DIAGNOSTIC.md +197 -0
- package/TRAINING_DATA_v4.md +3 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/team.js +43 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +24 -0
- 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/teams.js +100 -6
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +75 -43
- package/node_modules/@groove-dev/gui/dist/assets/{index-BYh6iHqL.js → index-BKCiOUDb.js} +593 -593
- package/node_modules/@groove-dev/gui/dist/assets/index-D4Q72afD.css +1 -0
- 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/stores/groove.js +42 -8
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +31 -3
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +1 -20
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +106 -3
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/team.js +43 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +24 -0
- package/packages/daemon/src/filewatcher.js +45 -0
- package/packages/daemon/src/index.js +14 -2
- package/packages/daemon/src/teams.js +100 -6
- package/packages/daemon/src/tunnel-manager.js +75 -43
- package/packages/gui/dist/assets/{index-BYh6iHqL.js → index-BKCiOUDb.js} +593 -593
- package/packages/gui/dist/assets/index-D4Q72afD.css +1 -0
- 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/stores/groove.js +42 -8
- package/packages/gui/src/views/agents.jsx +31 -3
- package/packages/gui/src/views/editor.jsx +1 -20
- package/packages/gui/src/views/teams.jsx +106 -3
- 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,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
|
-
}
|