| import * as THREE from 'three'; |
|
|
| export const START_GRID_Y = 0.28; |
| export const START_GRID_LATERAL_OFFSET = 2.35; |
| export const START_GRID_ROW_SPACING = 6.0; |
| export const START_GRID_INITIAL_OFFSET = 8.0; |
| export const MULTIPLAYER_COUNTDOWN_MS = 3600; |
| export const START_SYNC_GRACE_MS = 5000; |
| export const START_SYNC_ANCHOR_TOLERANCE = 24; |
| export const CENTER_FALLBACK_RADIUS = 18; |
|
|
| const TRACK_POINTS = [ |
| new THREE.Vector3(0, 0, 100), |
| new THREE.Vector3(50, 0, 90), |
| new THREE.Vector3(80, 0, 60), |
| new THREE.Vector3(120, 0, 40), |
| new THREE.Vector3(120, 0, -20), |
| new THREE.Vector3(105, 0, -50), |
| new THREE.Vector3(120, 0, -80), |
| new THREE.Vector3(120, 0, -140), |
| new THREE.Vector3(70, 0, -140), |
| new THREE.Vector3(70, 0, -70), |
| new THREE.Vector3(10, 0, -70), |
| new THREE.Vector3(10, 0, -140), |
| new THREE.Vector3(-40, 0, -140), |
| new THREE.Vector3(-80, 0, -120), |
| new THREE.Vector3(-140, 0, -80), |
| new THREE.Vector3(-150, 0, -20), |
| new THREE.Vector3(-110, 0, 30), |
| new THREE.Vector3(-140, 0, 70), |
| new THREE.Vector3(-90, 0, 110), |
| new THREE.Vector3(-40, 0, 100), |
| ]; |
|
|
| const SHARED_CURVE = new THREE.CatmullRomCurve3(TRACK_POINTS, true, 'centripetal', 0.5); |
| const SHARED_TRACK_LENGTH = SHARED_CURVE.getLength(); |
| const UP = new THREE.Vector3(0, 1, 0); |
|
|
| function normalizeSlotIndex(slotIndex = 0) { |
| if (!Number.isFinite(slotIndex)) return 0; |
| return Math.max(0, Math.floor(slotIndex)); |
| } |
|
|
| export function getStartTransformForSlot(slotIndex = 0) { |
| const normalizedSlot = normalizeSlotIndex(slotIndex); |
| const row = Math.floor(normalizedSlot / 2); |
| const side = normalizedSlot % 2 === 0 ? -1 : 1; |
| const baseT = (1 - ((row * START_GRID_ROW_SPACING + START_GRID_INITIAL_OFFSET) / SHARED_TRACK_LENGTH) + 1) % 1; |
| const point = SHARED_CURVE.getPointAt(baseT); |
| const tangent = SHARED_CURVE.getTangentAt(baseT).normalize(); |
| const lateral = new THREE.Vector3().crossVectors(UP, tangent).normalize(); |
| const heading = Math.atan2(tangent.x, tangent.z); |
| const position = point.clone().addScaledVector(lateral, side * START_GRID_LATERAL_OFFSET).setY(START_GRID_Y); |
| return { |
| slotIndex: normalizedSlot, |
| progress: baseT, |
| heading, |
| position: { x: position.x, y: position.y, z: position.z }, |
| }; |
| } |
|
|
| export function isNearCenterPosition(position, radius = CENTER_FALLBACK_RADIUS) { |
| if (!position) return true; |
| const x = Number(position.x ?? 0); |
| const z = Number(position.z ?? 0); |
| return Number.isFinite(x) && Number.isFinite(z) ? Math.hypot(x, z) <= radius : true; |
| } |
|
|
| export function distanceBetweenPositions(a, b) { |
| if (!a || !b) return Number.POSITIVE_INFINITY; |
| const dx = Number(a.x ?? 0) - Number(b.x ?? 0); |
| const dy = Number(a.y ?? 0) - Number(b.y ?? 0); |
| const dz = Number(a.z ?? 0) - Number(b.z ?? 0); |
| return Math.hypot(dx, dy, dz); |
| } |
|
|