HF-Racing / www /js /main.js
1024m's picture
Upload folder using huggingface_hub
0820aaf verified
Raw History Blame Contribute Delete
6.76 kB
import * as THREE from 'three';
import { GameMenu } from './ui-menu.js';
import { NetClient, SPAWN_OFFSETS } from './net.js';
import { DriveControls } from './controls.js';
import { LocalCar } from './vehicles.js';
import { Track } from './track.js';
import { DEFAULT_CAR, CAR_LABELS } from './cars.js';
const SPACE_URL = 'https://1024m-hf-racing.hf.space';
const footer = document.getElementById('footer-text');
const hud = document.getElementById('hud');
const speedEl = document.getElementById('speed-value');
const playerHud = document.getElementById('player-hud');
const playerName = document.getElementById('player-name');
const playerAvatar = document.getElementById('player-avatar');
const spectateBar = document.getElementById('spectate-bar');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87a0b5);
scene.fog = new THREE.Fog(0x87a0b5, 80, 400);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 800);
camera.position.set(0, 6, -12);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.domElement.id = 'game-canvas';
document.body.prepend(renderer.domElement);
const hemi = new THREE.HemisphereLight(0xddeeff, 0x443322, 0.85);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xfff2dd, 1.1);
sun.position.set(30, 50, 10);
sun.castShadow = true;
scene.add(sun);
const track = new Track(scene);
const controls = new DriveControls();
let car = null;
let inRace = false;
let spectating = false;
let specIndex = 0;
let trackReady = null;
const net = new NetClient({
scene,
onStatus: (msg) => {
if (footer) footer.textContent = msg;
},
onLobbyUpdate: (board, mode, err) => menu.renderBoard(board, mode, err),
onMatchStart: enterRace,
onMatchEnd: () => {
if (footer) footer.textContent = 'Race finished — Esc for menu';
},
onMatchConnectionLost: () => {
leaveRaceToMenu();
},
});
const menu = new GameMenu({
root: document.getElementById('menu-root'),
onOpenMode: (mode) => net.startLobbyPoll(mode),
onBack: async () => {
net.stopLobbyPoll();
await net.leaveLobby();
},
onClaim: (mode, lobbyId, seat) => net.claim(mode, lobbyId, seat),
onLeaveSeat: () => net.leaveLobby(),
onEnterMatch: () => net.connectMatch(),
onSpectate: (mode, lobbyId) => {
menu.hide();
hud.hidden = false;
net.connectSpectate(mode, lobbyId);
},
});
function setIdentityHud(identity) {
if (!identity?.username) return;
if (playerName) playerName.textContent = identity.username;
if (playerAvatar && identity.avatarUrl) {
playerAvatar.src = identity.avatarUrl;
playerAvatar.hidden = false;
}
if (playerHud) playerHud.hidden = false;
}
async function ensureTrack() {
if (!trackReady) {
trackReady = track.loadShanghai();
}
return trackReady;
}
async function enterRace(info) {
inRace = true;
spectating = !!info.spectating;
menu.hide();
hud.hidden = false;
if (spectateBar) spectateBar.hidden = !spectating;
const mapOk = await ensureTrack();
const grid = track.getGridPose(info.spawnIndex || 0);
if (!spectating) {
const carId = info.carId || (info.mode === 'sandbox' ? DEFAULT_CAR : DEFAULT_CAR);
if (car) {
car.dispose();
car = null;
}
car = new LocalCar(scene);
const loaded = await car.loadModel(carId);
car.setPose(grid.x, grid.y, grid.z, grid.yaw);
const label = CAR_LABELS[carId] || carId;
if (footer) {
footer.textContent = loaded
? `Race live · ${label}${mapOk ? ' · Shanghai' : ' · map fallback'}`
: `Race live · car missing (${carId})`;
}
} else if (footer) {
footer.textContent = `Spectating · ${info.mode}`;
}
}
function leaveRaceToMenu() {
inRace = false;
spectating = false;
net.disconnectMatch();
if (car) {
car.dispose();
car = null;
}
hud.hidden = true;
if (spectateBar) spectateBar.hidden = true;
menu.show();
net.startLobbyPoll(menu.mode || 'sandbox');
}
document.addEventListener('keydown', (e) => {
if (e.code === 'Escape' && inRace) {
leaveRaceToMenu();
}
});
document.getElementById('spec-prev')?.addEventListener('click', () => {
specIndex = Math.max(0, specIndex - 1);
});
document.getElementById('spec-next')?.addEventListener('click', () => {
specIndex += 1;
});
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
/** GTA-style chase cam — behind and above, looking at car rear/cabin. */
function followCar(target, delta) {
if (!target) return;
const yaw = target.yaw ?? target.mesh?.rotation.y ?? 0;
const pos = target.position;
const dist = 9.5;
const height = 3.2;
const lookLift = 1.15;
const behind = new THREE.Vector3(
-Math.sin(yaw) * dist,
height,
-Math.cos(yaw) * dist,
);
const desired = pos.clone().add(behind);
camera.position.lerp(desired, Math.min(1, delta * 6));
const look = new THREE.Vector3(pos.x, pos.y + lookLift, pos.z);
camera.lookAt(look);
}
let last = performance.now();
function frame(now) {
const delta = Math.min(0.05, (now - last) / 1000);
last = now;
if (inRace && !spectating && car) {
car.update(delta, controls.sample());
if (speedEl) speedEl.textContent = String(car.speedKmh());
followCar(car, delta);
} else if (inRace && spectating) {
const names = net.peers.list();
if (names.length) {
specIndex = ((specIndex % names.length) + names.length) % names.length;
const peer = net.peers.get(names[specIndex]);
if (peer) {
followCar({ position: peer.mesh.position, yaw: peer.mesh.rotation.y, mesh: peer.mesh }, delta);
const nameEl = document.getElementById('spec-name');
if (nameEl) nameEl.textContent = names[specIndex];
}
}
}
net.update(delta, car);
renderer.render(scene, camera);
requestAnimationFrame(frame);
}
async function boot() {
if (footer) footer.textContent = `Space: ${SPACE_URL} · loading…`;
ensureTrack(); // warm map load in background
const identity = await net.initLocal();
menu.setIdentity({
username: identity.username,
spaceUrl: identity.spaceUrl || SPACE_URL,
authError: identity.ok ? null : identity.error,
playAllowed: identity.playAllowed !== false,
});
if (identity.ok) setIdentityHud(identity);
if (footer) {
footer.textContent = identity.ok
? `Signed in · Space ${SPACE_URL}`
: (identity.error || 'Set HF_TOKEN in .env.local');
}
requestAnimationFrame(frame);
}
boot();